diff --git a/hardware/BBB/145-BBB-hardware.html b/hardware/BBB/145-BBB-hardware.html
new file mode 100644
index 00000000..bad622aa
--- /dev/null
+++ b/hardware/BBB/145-BBB-hardware.html
@@ -0,0 +1,636 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hardware/BBB/145-BBB-hardware.js b/hardware/BBB/145-BBB-hardware.js
new file mode 100644
index 00000000..9f18c82b
--- /dev/null
+++ b/hardware/BBB/145-BBB-hardware.js
@@ -0,0 +1,508 @@
+/**
+ * Copyright 2014 Maxwell R Hadley
+ *
+ * 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.
+ **/
+
+// Require main module
+var RED = require(process.env.NODE_RED_HOME + "/red/red");
+
+// Require bonescript
+try {
+ var bonescript = require("bonescript");
+} catch (err) {
+ require("util").log("[145-BBB-hardware] Error: cannot find module 'bonescript'");
+}
+
+// Node constructor for analogue-in
+function AnalogInputNode(n) {
+ // Create a RED node
+ RED.nodes.createNode(this, n);
+
+ // Store local copies of the node configuration (as defined in the .html)
+ this.topic = n.topic;
+ this.pin = n.pin;
+ this.breakpoints = n.breakpoints;
+ this.averaging = n.averaging;
+ if (this.averaging) {
+ this.averages = 10;
+ } else {
+ this.averages = 1;
+ }
+
+ // Define 'node' to allow us to access 'this' from within callbacks
+ var node = this;
+
+ // Variables used for input averaging
+ var sum; // accumulates the input readings to be averaged
+ var count; // keep track of the number of measurements made
+
+ // The callback function for analogRead. Accumulates the required number of
+ // measurements, then divides the total number, applies output scaling and
+ // sends the result
+ var analogReadCallback = function (x) {
+ sum = sum + x.value;
+ count = count - 1;
+ if (count > 0) {
+ bonescript.analogRead(node.pin, analogReadCallback);
+ } else {
+ var msg = {};
+ msg.topic = node.topic;
+ sum = sum/node.averages;
+ // i is the index of the first breakpoint where the 'input' value is strictly
+ // greater than the measurement (note: a measurement can never be == 1)
+ var i = node.breakpoints.map(function (breakpoint) { return sum >= breakpoint.input; }).indexOf(false);
+ msg.payload = node.breakpoints[i-1].output + (node.breakpoints[i].output - node.breakpoints[i-1].output) *
+ (sum - node.breakpoints[i-1].input)/(node.breakpoints[i].input - node.breakpoints[i-1].input);
+ node.send(msg);
+ }
+ };
+
+ // If we have a valid pin, set the input event handler to Bonescript's analogRead
+ if (["P9_39", "P9_40", "P9_37", "P9_38", "P9_33", "P9_36", "P9_35"].indexOf(node.pin) >= 0) {
+ node.on("input", function (msg) {
+ sum = 0;
+ count = node.averages;
+ bonescript.analogRead(node.pin, analogReadCallback);
+ });
+ } else {
+ node.error("Unconfigured input pin");
+ }
+}
+
+// Node constructor for discrete-in
+function DiscreteInputNode(n) {
+ RED.nodes.createNode(this, n);
+
+ // Store local copies of the node configuration (as defined in the .html)
+ this.topic = n.topic; // the topic is not currently used
+ this.pin = n.pin; // The Beaglebone Black pin identifying string
+ if (n.activeLow) // Set the 'active' state 0 or 1 as appropriate
+ this.activeState = 0;
+ else
+ this.activeState = 1;
+ this.updateInterval = n.updateInterval * 1000; // How often to send totalActiveTime messages
+ this.debounce = n.debounce; // Enable switch contact debouncing algorithm
+ if (n.outputOn === "rising") {
+ this.activeEdges = [false, true];
+ } else if (n.outputOn === "falling") {
+ this.activeEdges = [true, false];
+ } else if (n.outputOn === "both") {
+ this.activeEdges = [true, true];
+ } else {
+ node.error("Invalid edge type: " + n.outputOn);
+ }
+
+ // Working variables
+ this.interruptAttached = false; // Flag: should we detach interrupt when we are closed?
+ this.intervalId = null; // Remember the timer ID so we can delete it when we are closed
+ this.currentState = 0; // The pin input state "1" or "0"
+ this.lastActiveTime = NaN; // The date (in ms since epoch) when the pin last went high
+ // switch to process.hrtime()
+ this.totalActiveTime = 0; // The total time in ms that the pin has been high (since reset)
+ this.starting = true;
+ this.debouncing = false; // True after a change of state while waiting for the 7ms debounce time to elapse
+ this.debounceTimer = null;
+
+ // Define 'node' to allow us to access 'this' from within callbacks
+ var node = this;
+
+ // This function is called by the input pin change-of-state interrupt. If
+ // debounce is disabled, send the output message. Otherwise, if we are
+ // currently debouncing, ignore this interrupt. If we are not debouncing,
+ // schedule a re-read of the input pin in 7ms time, and set the debouncing flag
+ // Note: this function gets called spuriously when the interrupt is first enabled:
+ // in this case x.value is undefined - we must test for this
+ var interruptCallback = function (x) {
+ if (x.value !== undefined && node.currentState !== Number(x.value)) {
+ if (node.debounce) {
+ if (node.debouncing === false) {
+ node.debouncing = true;
+ node.debounceTimer = setTimeout(function () { bonescript.digitalRead(node.pin, debounceCallback); }, 7);
+ }
+ } else {
+ sendStateMessage(x);
+ }
+ }
+ };
+
+ // This function is called approx 7ms after a potential change-of-state which is
+ // being debounced. Terminate the debounce, and send a message if the state has
+ // actually changed
+ var debounceCallback = function (x) {
+ node.debounceTimer = null;
+ node.debouncing = false;
+ if (x.value !== undefined && node.currentState !== Number(x.value)) {
+ sendStateMessage(x);
+ }
+ };
+
+ // This function is called when either the interruptCallback or the debounceCallback
+ // have determined we have a 'genuine' change of state. Update the currentState and
+ // ActiveTime variables, and send a message on the first output with the new state
+ var sendStateMessage = function (x) {
+ node.currentState = Number(x.value);
+ var now = Date.now();
+ if (node.currentState === node.activeState) {
+ node.lastActiveTime = now;
+ } else if (!isNaN(node.lastActiveTime)) {
+ node.totalActiveTime += now - node.lastActiveTime;
+ }
+ if (node.activeEdges[node.currentState]) {
+ var msg = {};
+ msg.topic = node.topic;
+ msg.payload = node.currentState;
+ node.send([msg, null]);
+ }
+ };
+
+ // This function is called by the timer. It updates the ActiveTime variables, and sends a
+ // message on the second output with the latest value of the total active time, in seconds
+ var timerCallback = function () {
+ if (node.currentState === node.activeState) {
+ var now = Date.now();
+ node.totalActiveTime += now - node.lastActiveTime;
+ node.lastActiveTime = now;
+ }
+ var msg = {};
+ msg.topic = node.topic;
+ msg.payload = node.totalActiveTime / 1000;
+ node.send([null, msg]);
+ // Re-synchronise the pin state if we have missed a state change interrupt for some
+ // reason, and we are not in the process of debouncing one
+ if (node.debouncing === false) {
+ bonescript.digitalRead(node.pin, interruptCallback);
+ }
+ };
+
+ // This function is called when we receive an input message. If the topic contains
+ // 'load' (case insensitive) set the totalActiveTime to the numeric value of the
+ // payload, if possible. Otherwise clear the totalActiveTime (so we start counting
+ // from zero again)
+ var inputCallback = function (ipMsg) {
+ if (String(ipMsg.topic).search(/load/i) < 0 || isFinite(ipMsg.payload) == false) {
+ node.totalActiveTime = 0;
+ } else {
+ node.totalActiveTime = Number(ipMsg.payload);
+ }
+ if (node.currentState === node.activeState) {
+ node.lastActiveTime = Date.now();
+ }
+ // On startup, send an initial activeTime message, but only send an
+ // initial currentState message if we are in both edges active mode
+ if (node.starting) {
+ node.starting = false;
+ var msg;
+ if (node.activeEdges[0] && node.activeEdges[1]) {
+ msg = [{topic:node.topic}, {topic:node.topic}];
+ msg[0].payload = node.currentState;
+ } else {
+ msg = [null, {topic:node.topic}];
+ }
+ msg[1].payload = node.totalActiveTime;
+ node.send(msg);
+ }
+ };
+
+ // If we have a valid pin, set it as an input and read the (digital) state
+ if (["P8_7", "P8_8", "P8_9", "P8_10", "P8_11", "P8_12", "P8_13", "P8_14", "P8_15",
+ "P8_16", "P8_17", "P8_18", "P8_19", "P8_26", "P9_11", "P9_12", "P9_13", "P9_14",
+ "P9_15", "P9_16", "P9_17", "P9_18", "P9_21", "P9_22", "P9_23", "P9_24", "P9_26",
+ "P9_27", "P9_30", "P9_41", "P9_42"].indexOf(node.pin) >= 0) {
+ // Don't set up interrupts & intervals until after the close event handler has been installed
+ bonescript.detachInterrupt(node.pin);
+ process.nextTick(function () {
+ bonescript.pinMode(node.pin, bonescript.INPUT);
+ bonescript.digitalRead(node.pin, function (x) {
+ // Initialise the currentState and lastActveTime variables based on the value read
+ node.currentState = Number(x.value);
+ if (node.currentState === node.activeState) {
+ node.lastActiveTime = Date.now();
+ // switch to process.hrtime()
+ }
+ // Attempt to attach a change-of-state interrupt handler to the pin. If we succeed,
+ // set the input event and interval handlers, then send an initial message with the
+ // pin state on the first output
+ if (bonescript.attachInterrupt(node.pin, true, bonescript.CHANGE, interruptCallback)) {
+ node.interruptAttached = true;
+ node.on("input", inputCallback);
+ node.intervalId = setInterval(timerCallback, node.updateInterval);
+ } else {
+ node.error("Failed to attach interrupt");
+ }
+ setTimeout(function () { node.emit("input", {}); }, 50);
+ });
+ });
+ } else {
+ node.error("Unconfigured input pin");
+ }
+}
+
+// Node constructor for pulse-in
+function PulseInputNode(n) {
+ RED.nodes.createNode(this, n);
+
+ // Store local copies of the node configuration (as defined in the .html)
+ this.topic = n.topic; // the topic is not currently used
+ this.pin = n.pin; // The Beaglebone Black pin identifying string
+ this.updateInterval = n.updateInterval * 1000; // How often to send output messages
+ this.countType = n.countType; // Sets either 'edge' or 'pulse' counting
+ this.countUnit = n.countUnit; // Scaling appling to count output
+ this.countRate = n.countRate; // Scaling applied to rate output
+
+ // Working variables
+ this.interruptAttached = false; // Flag: should we detach interrupt when we are closed?
+ this.intervalId = null; // Remember the timer ID so we can delete it when we are closed
+ this.pulseCount = 0; // (Unscaled) total pulse count
+ // Hold the hrtime of the last two pulses (with ns resolution)
+ this.pulseTime = [[NaN, NaN], [NaN, NaN]];
+
+ // Define 'node' to allow us to access 'this' from within callbacks
+ var node = this;
+
+ // Called by the edge or pulse interrupt. If this is a valid interrupt, record the
+ // pulse time and count the pulse
+ var interruptCallback = function (x) {
+ if (x.value !== undefined) {
+ node.pulseTime = [node.pulseTime[1], process.hrtime()];
+ node.pulseCount = node.pulseCount + 1;
+ }
+ };
+
+ // Called when an input message arrives. If the topic contains 'load' (case
+ // insensitive) and the payload is a valid number, set the count to that
+ // number, otherwise set it to zero
+ var inputCallback = function (msg) {
+ if (String(msg.topic).search(/load/i) < 0 || isFinite(msg.payload) == false) {
+ node.pulseCount = 0;
+ } else {
+ node.pulseCount = Number(msg.payload);
+ }
+ };
+
+ // Called by the message timer. Send two messages: the scaled pulse count on
+ // the first output and the scaled instantaneous pulse rate on the second.
+ // The instantaneous pulse rate is the reciprocal of the larger of either the
+ // time interval between the last two pulses, or the time interval since the last pulse.
+ var timerCallback = function () {
+ var now = process.hrtime();
+ var lastTime = node.pulseTime[1][0] - node.pulseTime[0][0] + (node.pulseTime[1][1] - node.pulseTime[0][1]) / 1e9;
+ var thisTime = now[0] - node.pulseTime[1][0] + (now[1] - node.pulseTime[1][1]) / 1e9;
+ var msg = [{ topic:node.topic }, { topic:node.topic }];
+ msg[0].payload = node.countUnit * node.pulseCount;
+ // At startup, pulseTime contains NaN's: force the rate output to 0
+ msg[1].payload = node.countRate / Math.max(thisTime, lastTime) || 0;
+ node.send(msg);
+ };
+
+ // If we have a valid pin, set it as an input and read the (digital) state
+ if (["P8_7", "P8_8", "P8_9", "P8_10", "P8_11", "P8_12", "P8_13", "P8_14", "P8_15",
+ "P8_16", "P8_17", "P8_18", "P8_19", "P8_26", "P9_11", "P9_12", "P9_13", "P9_14",
+ "P9_15", "P9_16", "P9_17", "P9_18", "P9_21", "P9_22", "P9_23", "P9_24", "P9_26",
+ "P9_27", "P9_30", "P9_41", "P9_42"].indexOf(node.pin) >= 0) {
+ // Don't set up interrupts & intervals until after the close event handler has been installed
+ bonescript.detachInterrupt(node.pin);
+ process.nextTick(function () {
+ bonescript.pinMode(node.pin, bonescript.INPUT);
+ bonescript.digitalRead(node.pin, function (x) {
+ // Initialise the currentState based on the value read
+ node.currentState = Number(x.value);
+ // Attempt to attach an interrupt handler to the pin. If we succeed,
+ // set the input event and interval handlers
+ var interruptType;
+ if (node.countType === "pulse") {
+ // interruptType = bonescript.FALLING; <- doesn't work in v0.2.4
+ interruptType = bonescript.RISING;
+ } else {
+ interruptType = bonescript.CHANGE;
+ }
+ if (bonescript.attachInterrupt(node.pin, true, interruptType, interruptCallback)) {
+ node.interruptAttached = true;
+ node.on("input", inputCallback);
+ node.intervalId = setInterval(timerCallback, node.updateInterval);
+ } else {
+ node.error("Failed to attach interrupt");
+ }
+ });
+ });
+ } else {
+ node.error("Unconfigured input pin");
+ }
+}
+
+// Node constructor for discrete-out
+function DiscreteOutputNode(n) {
+ RED.nodes.createNode(this, n);
+
+ // Store local copies of the node configuration (as defined in the .html)
+ this.topic = n.topic; // the topic is not currently used
+ this.pin = n.pin; // The Beaglebone Black pin identifying string
+ this.defaultState = Number(n.defaultState); // What state to set up as
+ this.inverting = n.inverting;
+ this.toggle = n.toggle;
+
+ // Working variables
+ this.currentState = this.defaultState;
+
+ var node = this;
+
+ // If the input message paylod is numeric, values > 0.5 are 'true', otherwise use
+ // the truthiness of the payload. Apply the inversion flag before setting the output
+ var inputCallback = function (msg) {
+ var newState;
+ if (node.toggle) {
+ newState = node.currentState === 0 ? 1 : 0;
+ } else {
+ if (isFinite(Number(msg.payload))) {
+ newState = Number(msg.payload) > 0.5 ? true : false;
+ } else if (msg.payload) {
+ newState = true;
+ } else {
+ newState = false;
+ }
+ if (node.inverting) {
+ newState = !newState;
+ }
+ }
+ bonescript.digitalWrite(node.pin, newState ? 1 : 0);
+ node.send({ topic:node.topic, payload:newState });
+ node.currentState = newState;
+ };
+
+ // If we have a valid pin, set it as an output and set the default state
+ if (["P8_7", "P8_8", "P8_9", "P8_10", "P8_11", "P8_12", "P8_13", "P8_14", "P8_15",
+ "P8_16", "P8_17", "P8_18", "P8_19", "P8_26", "P9_11", "P9_12", "P9_13", "P9_14",
+ "P9_15", "P9_16", "P9_17", "P9_18", "P9_21", "P9_22", "P9_23", "P9_24", "P9_26",
+ "P9_27", "P9_30", "P9_41", "P9_42", "USR0", "USR1", "USR2", "USR3"].indexOf(node.pin) >= 0) {
+ // Don't set up interrupts & intervals until after the close event handler has been installed
+ bonescript.detachInterrupt(node.pin);
+ process.nextTick(function () {
+ bonescript.pinMode(node.pin, bonescript.OUTPUT);
+ node.on("input", inputCallback);
+ setTimeout(function () { bonescript.digitalWrite(node.pin, node.defaultState); }, 50);
+ });
+ } else {
+ node.error("Unconfigured output pin");
+ }
+}
+
+// Node constructor for pulse-out
+function PulseOutputNode(n) {
+ RED.nodes.createNode(this, n);
+
+ // Store local copies of the node configuration (as defined in the .html)
+ this.topic = n.topic; // the topic is not currently used
+ this.pin = n.pin; // The Beaglebone Black pin identifying string
+ this.pulseState = Number(n.pulseState); // What state the pulse will be..
+ this.defaultState = this.pulseState === 1 ? 0 : 1;
+ this.retriggerable = n.retriggerable;
+ this.pulseTime = n.pulseTime * 1000; // Pulse width in milliseconds
+
+ // Working variables
+ this.pulseTimer = null; // Non-null while a pulse is being generated
+
+ var node = this;
+
+ // Generate a pulse in response to an input message. If the topic includes the text
+ // 'time' (case insensitive) and the payload is numeric, use this value as the
+ // pulse time. Otherwise use the value from the properties dialog.
+ // If the resulting pulse time is < 1ms, do nothing.
+ // If the pulse mode is not retriggerable, then if no pulseTimer is active, generate
+ // a pulse. If the pulse mode is retriggerable, and a pulseTimer is active, cancel it.
+ // If no timer is active, set the pulse output. In both cases schedule a new pulse
+ // timer.
+ var inputCallback = function (msg) {
+ var time = node.pulseTime;
+ if (String(msg.topic).search(/time/i) >= 0 && isFinite(msg.payload)) {
+ time = msg.payload * 1000;
+ }
+ if (time >= 1) {
+ if (node.retriggerable === false) {
+ if (node.pulseTimer === null) {
+ node.pulseTimer = setTimeout(endPulseCallback, time);
+ bonescript.digitalWrite(node.pin, node.pulseState);
+ node.send({ topic:node.topic, payload:node.pulseState });
+ }
+ } else {
+ if (node.pulseTimer !== null) {
+ clearTimeout(node.pulseTimer);
+ } else {
+ bonescript.digitalWrite(node.pin, node.pulseState);
+ node.send({ topic:node.topic, payload:node.pulseState });
+ }
+ node.pulseTimer = setTimeout(endPulseCallback, time);
+ }
+ }
+ };
+
+ // At the end of the pulse, restore the default state and set the timer to null
+ var endPulseCallback = function () {
+ node.pulseTimer = null;
+ bonescript.digitalWrite(node.pin, node.defaultState);
+ node.send({ topic:node.topic, payload:node.defaultState });
+ };
+
+ // If we have a valid pin, set it as an output and set the default state
+ if (["P8_7", "P8_8", "P8_9", "P8_10", "P8_11", "P8_12", "P8_13", "P8_14", "P8_15",
+ "P8_16", "P8_17", "P8_18", "P8_19", "P8_26", "P9_11", "P9_12", "P9_13", "P9_14",
+ "P9_15", "P9_16", "P9_17", "P9_18", "P9_21", "P9_22", "P9_23", "P9_24", "P9_26",
+ "P9_27", "P9_30", "P9_41", "P9_42", "USR0", "USR1", "USR2", "USR3"].indexOf(node.pin) >= 0) {
+ // Don't set up interrupts & intervals until after the close event handler has been installed
+ bonescript.detachInterrupt(node.pin);
+ process.nextTick(function () {
+ bonescript.pinMode(node.pin, bonescript.OUTPUT);
+ node.on("input", inputCallback);
+ // Set the pin to the default stte once the dust settles
+ setTimeout(endPulseCallback, 50);
+ });
+ } else {
+ node.error("Unconfigured output pin");
+ }
+}
+
+// Register the nodes by name. This must be called before overriding any of the Node functions.
+RED.nodes.registerType("analog-in", AnalogInputNode);
+RED.nodes.registerType("discrete-in", DiscreteInputNode);
+RED.nodes.registerType("pulse-in", PulseInputNode);
+RED.nodes.registerType("discrete-out", DiscreteOutputNode);
+RED.nodes.registerType("pulse-out", PulseOutputNode);
+
+// On close, detach the interrupt (if we attached one) and clear any active timers
+DiscreteInputNode.prototype.close = function () {
+ if (this.interruptAttached) {
+ bonescript.detachInterrupt(this.pin);
+ }
+ if (this.intervalId !== null) {
+ clearInterval(this.intervalId);
+ }
+ if (this.debounceTimer !== null) {
+ clearTimeout(this.debounceTimer);
+ }
+};
+
+// On close, detach the interrupt (if we attached one) and clear the interval (if we set one)
+PulseInputNode.prototype.close = function () {
+ if (this.interruptAttached) {
+ bonescript.detachInterrupt(this.pin);
+ }
+ if (this.intervalId !== null) {
+ clearInterval(this.intervalId);
+ }
+};
+
+// On close, clear an active pulse timer
+PulseOutputNode.prototype.close = function () {
+ if (this.pulseTimer !== null) {
+ clearTimeout(this.pulseTimer);
+ }
+};
diff --git a/hardware/BBB/README.md b/hardware/BBB/README.md
new file mode 100644
index 00000000..a141bbbc
--- /dev/null
+++ b/hardware/BBB/README.md
@@ -0,0 +1,106 @@
+# BBB-hardware
+
+Node-RED nodes for interfacing with hardware inputs and outputs on the BeagleBone Black.
+Uses the bonescript library provided with the BBB, available through NPM and on Github
+at
+
+## Installation and Requirements
+
+The BBB should be running with the as-supplied device tree compiled into the kernel. This
+code is not currently aware of dynamically loaded device tree overlays, and in particular
+the list of pins presented for each node is based on the default. You might be lucky though...
+
+You need to be root to use bonescript, an error message will be logged if you are not.
+
+This code has been tested with the latest node-RED from github as of 2014-02-27T20:00
+using bonescript 0.2.4 running under node v0.8.22 on Angström
+Linux 3.8.13. The BBB was reflashed with the 2013.09.04 image, updated using opkg upgrade.
+
+Note that you do not need to add bonescript to node-RED's global context (in settings.js) to
+use these nodes, but no harm should arise if you do.
+
+Currently there are no checks against assigning the same pin to multiple nodes. Don't do it!
+
+## Available Nodes
+
+More nodes will be added as time is available, but the list at present is:
+
+### analogue-in
+
+Uses the on-chip ADC to measure a voltage between 0V and +1.8V on any one of the 7
+dedicated input pins. A message input is used to trigger an ADC conversion, or
+alternatively a single message can trigger a rapid 'burst' of 10 conversions
+which are averaged together to reduce the noise (which can be a problem, especially
+with high source impedances).
+
+Linear scaling can be applied to the measurement result. Linearity correction can
+be applied using a piecewise-linear approach, useful for linearising transducers such
+as thermistors or LDRs.
+
+Useful for conversion rates up to about 50 samples/sec.
+
+### discrete-in
+
+Reads a GPIO pin, and generates a message when it changes state: either in both
+directions or just rising or just falling edges only. A debounce algorithm may be
+applied, useful if the input is connected to a pushbutton.
+
+One pin state is denoted as 'active', and the node accumulates the total time in this
+state. A message with this total is output periodically, controlled by an internal
+timer. The total can be cleared or set to an arbitrary value by an input message
+who's topic copntains 'load'.
+
+Useful for energy monitoring, e.g. logging boiler on time.
+
+### pulse-in
+
+Counts input pulses at a GPIO pin. The count can be advanced by either the rising edge
+of the pulse, or by both edges. Two outputs are provided:
+
+1. The current total number of pulses
+2. The current pulse rate (pulses/second)
+
+Separate scaling factors are applied to each output. Output messages are generated
+at regular intervals, controlled by an internal timer. The count can be cleared
+or set to an arbitrary value by an input message who's topic contains 'load'.
+
+Useful for energy monitoring, e.g. electricty meter pulse outputs.
+
+The 'instantaneous' pulse rate is derived from either the time between the last two
+pulses, or the time since the last pulse, whichever is larger. This gives a rapid
+dynamic response to changes in electricty use, for example.
+
+Recommended maximum pulse rate 20 pulses/second - but it did work OK at 500 Hz!
+
+### discrete-out
+
+Sets a GPIO pin to the state specified by the input message payload: 0/1 or true/false.
+The pin can either follow the input state or be an inverted copy. Alternatively, in toggle
+mode each input message (whatever its topic & payload) will toggle the pin between
+the two states.
+
+You can specify the state to be set at startup, prior to the arrival of the first message.
+
+The node generates an output message with payload 1 or 0 each time it changes state.
+The user LEDs are available to the discrete-out node
+
+### pulse-out
+
+Pulses a GPIO pin when a message arrives. The pulse time is set in the property editor,
+but can be overridden by a time value in an input message (topic containing 'time').
+
+The can be set to generate high pulses (normally low) or vice-versa. It may also be
+set as retriggerable or not.
+
+The node generates an output message with payload 1 or 0 each time it changes state.
+The user LEDs are available to the pulse-out node
+
+## Author
+
+BBB-hardware was written by
+
+* Max Hadley [@MRHadley](http://twitter.com/MRHadley)
+
+## Copyright and license
+
+Copyright 2014 M R Hadley, made available under [the Apache 2.0 license](LICENSE).