node package manager
Stop writing boring code. Discover, share, and reuse within your team. Create a free org »

caf_iot

CAF.js (Cloud Assistant Framework)

Co-design permanent, active, stateful, reliable cloud proxies with your web app and gadgets.

See http://www.cafjs.com

CAF for IoT

Build Status

IoT platform that runs on the device and pairs with a Cloud Assistant.

The IoT device programming model is very similar to programming a CA (see {@link external:caf_ca}). In fact, most of the code is reused.

Method execution is serialized by a queue, no global state, similar plugins, and transactional changes to local state can be rolled backed on abort.

However, we do not checkpoint state, and there is only one "CA", i.e., the device. We are not that concerned about long term consistency either, since we reset the device every reboot; therefore, some plugins may not delay external actions, improving responsiveness.

We have also simplified framework methods and config files. For instance, the file iot.json describes both the framework, i.e., framework.json, and the device, i.e., ca.json, component hierarchies. Also, the naming of methods is more "Arduino-friendly".

Hello World (see examples/helloworld)

exports.methods = {
    __iot_setup__: function(cb) {
        this.state.counter = this.toCloud.get('counter') || 0;
        cb(null);
    },
    __iot_loop__ : function(cb) {
        var msg = this.fromCloud.get('msg') || 'Counter:';
        this.$.log && this.$.log.debug(msg + this.state.counter);
        this.state.counter = this.state.counter + 1;
        this.toCloud.set('counter', this.state.counter);
        cb(null);
    }
};

Defines two methods called by the IoT framework:

  • __iot_setup__: initializes the state of the device everytime it resets.
  • __iot_loop__: similar to a __ca_pulse__ CA method, it executes periodically. See {@link module:caf_iot/plug_iot_handler} for details.

Device data is described with:

  • this.state: similar to a CA's this.state but not checkpointed.
  • this.scratch: similar to a CA's this.scratch.
  • this.toCloud: A SharedMap (see {@link external:caf_sharing}) written by the device and read by its CA. When the device resets, it also downloads the latest contents of this map that reached the cloud.
  • this.fromCloud: A SharedMap written by the CA and read by the device. This is the main mechanism to configure the device or trigger actions.

In the previous example this.toCloud has two purposes:

  • checkpoint the last value of counter, so that it is remembered after a reset.
  • communicate this value to the CA, making it visible to external clients (see examples/helloworld/client.js).

The purpose of this.fromCloud is to allow a client to indirectly modify the behavior of the device by communicating with its CA.

The CA impersonates the device, enabling seamless interaction when the device is offline, or behind a firewall.

Hello Cron (see examples/hellocron)

Similar to CA plugins, device plugins are exposed to application code with proxies in this.$.

An interesting plugin is cron (see {@link module:caf_iot/proxy_iot_cron}), which allows calls to arbitrary methods at regular intervals.

exports.methods = {
    __iot_setup__: function(cb) {
        this.state.counter = this.toCloud.get('counter') || 0;
        this.$.cron.addCron('helloCron', 'greetings', ['Hello:'], 2000);
        this.$.cron.addCron('byeCron', 'greetings', ['Bye:'], 3000);
        cb(null);
    },
    greetings: function(greet, cb) {
        var now = (new Date()).getTime();
        this.$.log && this.$.log.debug(greet + now);
        cb(null);
    },
    ...
};

How are errors and exceptions handled?

The default behavior is rather crude, just log and do a full reset.

It is recommended to override that behavior by adding a method __iot_error__.

This method could avoid the reset by not propagating the error in the callback, see {@link module:caf_iot/plug_iot_handler#iot_error} and examples/hellocron/iot/iot_methods.js.

Hello Bundles (see examples/hellobundle)

The CA can invoke device methods by using timed bundles of commands.

CAF.js synchronizes device clocks with the cloud, coordinating soft real-time actions across the globe with UTC time. Given a few seconds to propagate commands, millions of devices could blink within a hundred milliseconds of each other.

Why bundles and not just separate commands?

Safety. Think of controlling a drone. One command to dive as fast as it can. Second command to gracefully recover. Lost network connection between them. Oops...

If we bundle commands, both are cached in the drone before anything happens. If execution is based on UTC time, not on arrival time, we can pipeline bundles, ensuring smooth movement. And the CA can keep generating these bundles based on a higher goal. See {@link module:caf_iot/plug_iot_bundles} for details.

Extra time is added to bundles for network propagation but, when a bundle arrives to the device late, it gets ignored. The CA can detect that by monitoring responses in this.state.acks; this is an array of max size this.state.maxAcks, and elements of type:

{response: boolean, index: number}

where:

  • response: False if the bundle was late, True otherwise.
  • index: An identifier for the bundle. It matches the one previously returned by {@link module:caf_iot/ca/proxy_iot#sendBundle}.

Let's look at an example.

The device code defines three simple commands for our "drone": up, down, or take a recovery action if we lose connectivity.

exports.methods = {
...
    down: function(speed, cb) {
        var now = (new Date()).getTime();
        this.$.log && this.$.log.debug('Down:' +  now + ' speed: ' + speed);
        cb(null);
    },
    up: function(speed, cb) {
        var now = (new Date()).getTime();
        this.$.log && this.$.log.debug('Up:  ' +  now + ' speed: ' + speed);
        cb(null);
    },
    recover: function(msg, cb) {
        var now = (new Date()).getTime();
        this.$.log && this.$.log.debug('RECOVERING:' +  now + ' msg: ' + msg);
        cb(null);
    },
};

The CA code is a bit more interesting:

var MARGIN=100;
exports.methods = {
    __ca_init__: function(cb) {
        this.state.maxAcks = 1;
        cb(null);
    },
    __ca_pulse__: function(cb) {
        if ((this.state.acks && (this.state.acks.length > 0) &&
             (!this.state.acks[0].result))) {
            this.$.log && this.$.log.debug('Last bundle was late');
        }
        var bundle = this.$.iot.newBundle(MARGIN);
        bundle.down(0, [1]).up(300, [1]).recover(5000, ['go home']);
        this.$.iot.sendBundle(bundle);
        // `notify` improves responsiveness.
        this.$.session.notify(['new bundle'], 'iot');
        cb(null);
    },
  ...
};

If you are wondering how bundle gets methods down, up, and recover, the framework instrospects the device code, and generates them at run time. I love JavaScript.

__ca_pulse__ gets called every interval msec. If interval is less than 5000, we have two cases:

  • Normal case: the next bundle starts before the recovery action of the previous bundle. Only one bundle can be active, and the device skips recovery.

  • Network partition for more than five seconds: no new bundles, and the device triggers recovery.

Why notify after sendBundle? To save energy and bandwidth the device typically syncs with the cloud every few seconds, and we are providing a margin of only 100 msec to execute the bundle. If the device is currently connected, notify uses the websocket to force the device to sync.

Hello Cloud (see examples/hellocloud)

The device can also call CA methods.

The cloud plugin provides a standard client Session (see {@link external:caf_cli}). It can also receive session notifications (see {@link external:caf_session}), and process them as conventional method calls.

Notifications improve responsiveness, because otherwise the device waits until the next __iot_loop__ to synchronize with the cloud.

For example:

exports.methods = {
    __iot_setup__: function(cb) {
        var self = this;
        this.$.cloud.registerHandler(function(msg) {
            var args = self.$.cloud.getMethodArgs(msg);
            self.$.queue.process('greetings', args);
        });
        cb(null);
    },
    greetings: function(msg, cb) {
        var self = this;
        var now = (new Date()).getTime();
        this.$.log && this.$.log.debug(msg + now);
        this.$.cloud.cli.getCounter(function(err, value) {
            if (err) {
                cb(err);
            } else {
                self.$.log && self.$.log.debug('Got ' + value);
                cb(null);
            }
        });
    },
    ...
};

Every time the CA notifies the device, a method call for greetings gets queued, and eventually, that method will call back the CA, reading its counter. The default session identifier for a device is iot, but it can be changed with the property session (see {@link module:caf_iot/plug_iot_cloud}).

Much more...

We have a few RPi plugins in caf/extra that do real IoT stuff. Controlling gpio pins, managing external RTC/power boards, distance sensors...

The long term strategy is not to duplicate the great work of other JavaScript IoT libraries supporting zillions of sensors/actuators/devices. Instead, wrap these libraries with local plugins, and focus on cloud/client integration. This is consistent with our web client strategy, i.e., integrating with other libraries such as React/Redux.