09 August 2016

Node-Red is pretty useful for wiring together IOF (Internet of Things) devices, but its currently tied to node.js. Here is a way to run it clientside (on a browser) for those things that aren't allowed to run node.js.

Situation

I had devices that could only run JS through a chrome-like browser. Yet I needed a way to quickly deploy similar workflows to these devices. Each device would also act as a hub for other hardware on the system, this other hardware was easiest to connect through node-red.

Thus node-red didn't need any UI, but it did need a way to run the given flows on a browser. The following needed to be done:

  • Add a route to get node-red configuration for this device
    • flows
    • credentials
  • Removing node-red's filesystem dependancy
  • Polyfilling any node.js modules node-red uses (that aren't browser compatible)

Node-Red

Checkout my node-red repo to see the changes I had to make in node-red.

I didn't have to make too many changes to node-red itself to support this. I simply added a flag: settings.noFileSystem, and made sure any code that auto-loaded data from the filesystem on startup didn't get a chance to run if the flag was on. Thus in the browser, I would add this flag, but on the main node-red server, I would not.

(see the commit here)

None of this is release-ready yet, as there's a lot more to examine, add and test before any pull-request

Second, you can't set the active flow (since it's supposed to be loaded by the filesystem), so I needed a way to set that up too. This was quite easy as well.

    // red/nodes/flows.js
    setActiveFlow: function(flow) {
        activeFlow = flow;
    },

Finally, there was a problem with the credentials requiring way too many

var needsPermission = require("../api/auth").needsPermission;

While I was at it, I realize it would be nice to be able to use inject nodes in the browser as well, so I added an easy function to do so. (see the commit here)

    // Usage:
    nodeRedInject('node-name');

Browser Usage

To use node-red in the browser I needed to get the configuration, this basically means a bunch of flows to run.

Getting each flow was easy enough, added a route to the server, and do an Ajax call

/**
 * Gets a subflow
 *
 * @param flowId
 * @param callback
 * @returns subflow
 */
function getSubflow(flowId, callback) {
    jQuery.ajax('//' + window.location.hostname + '/subflow/' + flowId, {
        data: {},
    }).error(function(xhr) {
        return callback("Server Error: " + xhr.status + ". Can't get subflow(" + flowId + ")");
    }).done(function(subflow) {
        if (!_.isArray(subflow)) {
            return callback("Server Error: Invalid return");
        }
        return callback(null, subflow);
    });
}

Of course we can load more than one flow, so this waits for all the data to come back, and merges them all into one flow.

/**
 * Loads the given subflow Ids
 * @param flowIds
 */
function initSubflows(flowIds, callback) {
    var syncs = [];

    for (var i=0; i < flowIds.length; i++) {
        var flowId = flowIds[i];
        var defer = jQuery.Deferred();
        (function scope(defer, flowId) {
            getSubflow(flowId, function(err, subflow) {
                if (err) {
                    defer.resolve([]);
                    return console.error(err);
                }

                defer.resolve(subflow);
            });
        })(defer, flowId);

        syncs.push(defer.promise());
    }

    jQuery.when.apply(null, syncs).then(function() {
        var flowData = [];

        // Merge the subflows
        var args = Array.prototype.slice.call(arguments);
        for (var i in args) {
            flowData = flowData.concat(args[i]);
        }

        callback(null, flowData);
    });
}

Finally here is the way the entire thing is called. Note that the last callback is the one that loads the flows into node-red itself, starting it.

    // Load the flows that we need
    var flowIds = params.flows;
    if (flowIds) {
        flowIds = flowIds.split(",");
        initSubflows(flowIds,  function onSubflowData(err, flowData) {
            RED.loadFlowData(flowData);
        });
    }

Reference



comments powered by Disqus