diff --git a/hardware/sensehat/README.md b/hardware/sensehat/README.md
index 230b83d2..62ecfd7f 100644
--- a/hardware/sensehat/README.md
+++ b/hardware/sensehat/README.md
@@ -28,7 +28,8 @@ Run the following command in your Node-RED user directory (typically `~/.node-re
### Input Node
-This node sends readings from the various sensors on the Sense HAT, grouped into three sets; motion events, environment events and joystick events.
+This node sends readings from the various sensors on the Sense HAT, grouped into
+three sets; motion events, environment events and joystick events.
#### Motion events
@@ -38,9 +39,9 @@ per second. The `topic` is set to `motion` and the `payload` is an object with t
following values:
- `acceleration.x/y/z` : the acceleration intensity in Gs
- - `gyroscope.x/y/z` : the rotational intensity in radians/s
- - `orientation.roll/pitch/yaw` : the angle of the axis in degrees
- - `compass` : the direction of North in degrees
+ -`gyroscope.x/y/z` : the rotational intensity in radians/s
+ -`orientation.roll/pitch/yaw` : the angle of the axis in degrees
+ -`compass` : the direction of North in degrees
#### Environment events
@@ -49,17 +50,69 @@ sensors. They are sent at a rate of approximately 1 per second. The `topic`
is set to `environment` and the `payload` is an object
with the following values:
- - `temperature` : degrees Celsius
- - `humidity` : percentage of relative humidity
- - `pressure` : Millibars
+ -`temperature` : degrees Celsius
+ -`humidity` : percentage of relative humidity
+ -`pressure` : Millibars
#### Joystick events
Joystick events are sent when the Sense HAT joystick is interacted with. The
`topic` is set to `joystick` and the `payload` is an object with the following values:
- - `key` : one of `UP`, `DOWN`, `LEFT`, `RIGHT`, `ENTER`
- - `state` : the state of the key:
- - `0` : the key has been released
- - `1` : the key has been pressed
- - `2` : the key is being held down
+ -`key` : one of `UP`, `DOWN`, `LEFT`, `RIGHT`, `ENTER`
+ -`state` : the state of the key:
+ -`0` : the key has been released
+ -`1` : the key has been pressed
+ -`2` : the key is being held down
+
+
+### Output Node
+
+This node sends commands to the 8x8 LED display on the Sense HAT.
+
+Commands are sent to the node in `msg.payload`. Multiple commands can
+be sent in a single message by separating them with newline (\n) characters.
+
+#### Set the colour of individual pixels
+
+Format: `<x>,<y>,<colour>`
+
+`x` and `y` must either be a value in the range 0-7, or `*` to indicate the entire row or column.
+
+`colour` must be one of:
+
+ - the well-known HTML colour names
+ - eg `red` or `aquamarine`,
+ - the CheerLights colour names,
+ - a HEX colour value - eg `#aa9900`
+ - an RGB triple - `190,255,0`
+ - or simply `off`
+
+To set the entire screen to red: `*,*,red`
+
+To set the four corners of the display to red, green (#00ff00), yellow and blue (0,0,255):
+
+`0,0,red,0,7,#00ff00,7,7,yellow,7,0,0,0,255`
+
+#### Rotate the screen
+
+Format: `R<angle>`
+
+`angle` must be 0, 90, 180 or 270.
+
+#### Flip the screen
+
+Format: `R<axis>`
+
+`axis` must be either `H` or `V` to flip on the horizontal or vertical axis respectively.
+
+#### Scroll a message
+
+If `msg.payload` is not recognised as any of the above commands, it is treated
+as a text message to be scrolled across the screen.
+
+The following message properties can be used to customise the appearance:
+
+ - `msg.colour` - the colour of the text, default: `white`
+ - `msg.background` - the colour of the background, default: `off`
+ - `msg.speed` - the scroll speed. A value in the range 1 (slower) to 5 (faster), default: `3`
diff --git a/hardware/sensehat/colours.js b/hardware/sensehat/colours.js
new file mode 100644
index 00000000..fa0fa1aa
--- /dev/null
+++ b/hardware/sensehat/colours.js
@@ -0,0 +1,220 @@
+/**
+ * Copyright 2016 IBM Corp.
+ *
+ * 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.
+ **/
+
+var colours = {
+ 'aqua':'#00FFFF',
+ 'aliceblue':'#F0F8FF',
+ 'antiquewhite':'#FAEBD7',
+ 'black':'#000000',
+ 'off':'#000000',
+ 'blue':'#0000FF',
+ 'cyan':'#00FFFF',
+ 'darkblue':'#00008B',
+ 'darkcyan':'#008B8B',
+ 'darkgreen':'#006400',
+ 'darkturquoise':'#00CED1',
+ 'deepskyblue':'#00BFFF',
+ 'green':'#008000',
+ 'lime':'#00FF00',
+ 'mediumblue':'#0000CD',
+ 'mediumspringgreen':'#00FA9A',
+ 'navy':'#000080',
+ 'springgreen':'#00FF7F',
+ 'teal':'#008080',
+ 'midnightblue':'#191970',
+ 'dodgerblue':'#1E90FF',
+ 'lightseagreen':'#20B2AA',
+ 'forestgreen':'#228B22',
+ 'seagreen':'#2E8B57',
+ 'darkslategray':'#2F4F4F',
+ 'darkslategrey':'#2F4F4F',
+ 'limegreen':'#32CD32',
+ 'mediumseagreen':'#3CB371',
+ 'turquoise':'#40E0D0',
+ 'royalblue':'#4169E1',
+ 'steelblue':'#4682B4',
+ 'darkslateblue':'#483D8B',
+ 'mediumturquoise':'#48D1CC',
+ 'indigo':'#4B0082',
+ 'darkolivegreen':'#556B2F',
+ 'cadetblue':'#5F9EA0',
+ 'cornflowerblue':'#6495ED',
+ 'mediumaquamarine':'#66CDAA',
+ 'dimgray':'#696969',
+ 'dimgrey':'#696969',
+ 'slateblue':'#6A5ACD',
+ 'olivedrab':'#6B8E23',
+ 'slategray':'#708090',
+ 'slategrey':'#708090',
+ 'lightslategray':'#778899',
+ 'lightslategrey':'#778899',
+ 'mediumslateblue':'#7B68EE',
+ 'lawngreen':'#7CFC00',
+ 'aquamarine':'#7FFFD4',
+ 'chartreuse':'#7FFF00',
+ 'gray':'#808080',
+ 'grey':'#808080',
+ 'maroon':'#800000',
+ 'olive':'#808000',
+ 'purple':'#800080',
+ 'lightskyblue':'#87CEFA',
+ 'skyblue':'#87CEEB',
+ 'blueviolet':'#8A2BE2',
+ 'darkmagenta':'#8B008B',
+ 'darkred':'#8B0000',
+ 'saddlebrown':'#8B4513',
+ 'darkseagreen':'#8FBC8F',
+ 'lightgreen':'#90EE90',
+ 'mediumpurple':'#9370DB',
+ 'darkviolet':'#9400D3',
+ 'palegreen':'#98FB98',
+ 'darkorchid':'#9932CC',
+ 'yellowgreen':'#9ACD32',
+ 'sienna':'#A0522D',
+ 'brown':'#A52A2A',
+ 'darkgray':'#A9A9A9',
+ 'darkgrey':'#A9A9A9',
+ 'greenyellow':'#ADFF2F',
+ 'lightblue':'#ADD8E6',
+ 'paleturquoise':'#AFEEEE',
+ 'lightsteelblue':'#B0C4DE',
+ 'powderblue':'#B0E0E6',
+ 'firebrick':'#B22222',
+ 'darkgoldenrod':'#B8860B',
+ 'mediumorchid':'#BA55D3',
+ 'rosybrown':'#BC8F8F',
+ 'darkkhaki':'#BDB76B',
+ 'silver':'#C0C0C0',
+ 'mediumvioletred':'#C71585',
+ 'indianred':'#CD5C5C',
+ 'peru':'#CD853F',
+ 'chocolate':'#D2691E',
+ 'tan':'#D2B48C',
+ 'lightgray':'#D3D3D3',
+ 'lightgrey':'#D3D3D3',
+ 'thistle':'#D8BFD8',
+ 'goldenrod':'#DAA520',
+ 'orchid':'#DA70D6',
+ 'palevioletred':'#DB7093',
+ 'crimson':'#DC143C',
+ 'gainsboro':'#DCDCDC',
+ 'plum':'#DDA0DD',
+ 'burlywood':'#DEB887',
+ 'lightcyan':'#E0FFFF',
+ 'lavender':'#E6E6FA',
+ 'darksalmon':'#E9967A',
+ 'palegoldenrod':'#EEE8AA',
+ 'violet':'#EE82EE',
+ 'azure':'#F0FFFF',
+ 'honeydew':'#F0FFF0',
+ 'khaki':'#F0E68C',
+ 'lightcoral':'#F08080',
+ 'sandybrown':'#F4A460',
+ 'beige':'#F5F5DC',
+ 'mintcream':'#F5FFFA',
+ 'wheat':'#F5DEB3',
+ 'whitesmoke':'#F5F5F5',
+ 'ghostwhite':'#F8F8FF',
+ 'lightgoldenrodyellow':'#FAFAD2',
+ 'linen':'#FAF0E6',
+ 'salmon':'#FA8072',
+ 'oldlace':'#FDF5E6',
+ 'warmwhite':'#FDF5E6',
+ 'bisque':'#FFE4C4',
+ 'blanchedalmond':'#FFEBCD',
+ 'coral':'#FF7F50',
+ 'cornsilk':'#FFF8DC',
+ 'darkorange':'#FF8C00',
+ 'deeppink':'#FF1493',
+ 'floralwhite':'#FFFAF0',
+ 'fuchsia':'#FF00FF',
+ 'gold':'#FFD700',
+ 'hotpink':'#FF69B4',
+ 'ivory':'#FFFFF0',
+ 'lavenderblush':'#FFF0F5',
+ 'lemonchiffon':'#FFFACD',
+ 'lightpink':'#FFB6C1',
+ 'lightsalmon':'#FFA07A',
+ 'lightyellow':'#FFFFE0',
+ 'magenta':'#FF00FF',
+ 'mistyrose':'#FFE4E1',
+ 'moccasin':'#FFE4B5',
+ 'navajowhite':'#FFDEAD',
+ 'orange':'#FFA500',
+ 'orangered':'#FF4500',
+ 'papayawhip':'#FFEFD5',
+ 'peachpuff':'#FFDAB9',
+ 'pink':'#FFC0CB',
+ 'red':'#FF0000',
+ 'seashell':'#FFF5EE',
+ 'snow':'#FFFAFA',
+ 'tomato':'#FF6347',
+ 'white':'#FFFFFF',
+ 'yellow':'#FFFF00',
+ 'amber':'#FFD200'
+};
+
+var hexColour = /^#([0-9A-F][0-9A-F][0-9A-F]){1,2}$/i;
+
+module.exports.getRGB = function(col,rgb) {
+ if (!col) {
+ return null;
+ }
+ if (/^\d{1,3},\d{1,3},\d{1,3}$/.test(col)) {
+ return col;
+ }
+ col = col.toString().toLowerCase();
+ if (col in colours) {
+ col = colours[col];
+ }
+ if (hexColour.test(col)) {
+ if (col.length === 4) {
+ col = "#"+col[1]+col[1]+col[2]+col[2]+col[3]+col[3];
+ }
+ if (rgb === "grb") {
+ g = parseInt(col.slice(1,3),16);
+ r = parseInt(col.slice(3,5),16);
+ b = parseInt(col.slice(5),16);
+ }
+ else {
+ r = parseInt(col.slice(1,3),16);
+ g = parseInt(col.slice(3,5),16);
+ b = parseInt(col.slice(5),16);
+ }
+ return r+","+g+","+b;
+ }
+ else {
+ return null;
+ }
+}
+
+module.exports.getHex = function(col) {
+ col = col.toString().toLowerCase();
+ if (col in colours) {
+ return colours[col];
+ }
+ else { return null; }
+}
+
+module.exports.HexRGB = function(hex) {
+ try {
+ r = parseInt(hex.slice(1,3),16);
+ g = parseInt(hex.slice(3,5),16);
+ b = parseInt(hex.slice(5),16);
+ return r+","+g+","+b;
+ }
+ catch(e) { return null; }
+}
diff --git a/hardware/sensehat/sensehat.html b/hardware/sensehat/sensehat.html
index 05d879f7..2370ea9b 100644
--- a/hardware/sensehat/sensehat.html
+++ b/hardware/sensehat/sensehat.html
@@ -36,6 +36,13 @@
+
+
+
diff --git a/hardware/sensehat/sensehat.js b/hardware/sensehat/sensehat.js
index 5ca20ca8..d86327be 100644
--- a/hardware/sensehat/sensehat.js
+++ b/hardware/sensehat/sensehat.js
@@ -18,6 +18,7 @@ module.exports = function(RED) {
"use strict";
var fs = require('fs');
var spawn = require('child_process').spawn;
+ var colours = require('./colours');
var hatCommand = __dirname+'/sensehat';
@@ -198,10 +199,16 @@ module.exports = function(RED) {
} else {
done();
}
+ },
+ send: function(msg) {
+ if (hat) {
+ hat.stdin.write(msg+'\n');
+ }
}
}
})();
+
function SenseHatInNode(n) {
RED.nodes.createNode(this,n);
this.motion = n.motion;
@@ -215,4 +222,115 @@ module.exports = function(RED) {
});
}
RED.nodes.registerType("rpi-sensehat in",SenseHatInNode);
+
+ function SenseHatOutNode(n) {
+ RED.nodes.createNode(this,n);
+ var node = this;
+ HAT.open(this);
+
+ node.on("close", function(done) {
+ HAT.close(this,done);
+ });
+
+ node.on("input",function(msg) {
+ var command;
+ var parts;
+ var col;
+ if (typeof msg.payload === 'string') {
+ var lines = msg.payload.split("\n");
+ lines.forEach(function(line) {
+ command = null;
+ col = colours.getRGB(line);
+ if ( /^(([0-7]|\*),([0-7]|\*),(\d{1,3},\d{1,3},\d{1,3}|#[a-f0-9]{3,6}|[a-z]+))(,([0-7]|\*),([0-7]|\*),(\d{1,3},\d{1,3},\d{1,3}|#[a-f0-9]{3,6}|[a-z]+))*$/i.test(line)) {
+ parts = line.split(",");
+ var expanded = [];
+ var i=0;
+ var j=0;
+ while (i 0) {
+ var pixels = {};
+ var rules = [];
+ for (i=expanded.length-1;i>=0;i--) {
+ var rule = expanded[i];
+ if (!pixels[rule[0]+","+rule[1]]) {
+ rules.unshift(rule.join(","));
+ pixels[rule[0]+","+rule[1]] = true;
+ }
+ }
+ if (rules.length > 0) {
+ command = "P"+rules.join(",");
+ }
+ }
+ }
+
+
+ if (!command) {
+ if (/^R(0|90|180|270)$/i.test(line)) {
+ command = line.toUpperCase();
+ } else if (/^F(H|V)$/i.test(line)) {
+ command = line.toUpperCase();
+ } else {
+ var textCol = colours.getRGB(msg.color);
+ var backCol = colours.getRGB(msg.background);
+ var speed = null;
+ if (!isNaN(msg.speed)) {
+ speed = msg.speed;
+ }
+ command = "T";
+ if (textCol) {
+ command += textCol;
+ if (backCol) {
+ command += ","+backCol;
+ }
+ }
+ if (speed) {
+ var s = parseInt(speed);
+ if (s >= 1 && s <= 5) {
+ s = 0.1 + (3-s)*0.03;
+ }
+ command = command + ((command.length === 1)?"":",") + s;
+ }
+ command += ":" + line;
+ }
+ }
+ if (command) {
+ //console.log(command);
+ HAT.send(command);
+ }
+ });
+ }
+ });
+ }
+ RED.nodes.registerType("rpi-sensehat out",SenseHatOutNode);
+
}
diff --git a/hardware/sensehat/sensehat.py b/hardware/sensehat/sensehat.py
index 9cc6396f..e8a87195 100644
--- a/hardware/sensehat/sensehat.py
+++ b/hardware/sensehat/sensehat.py
@@ -16,7 +16,7 @@
# C[R,G,B] - clear to colour (or off if no RGB provided)
# R[rot] - rotate by rot (0,90,180,270)
# P[x,y,R,G,B]+ - set individual pixel(s) to a colour
-# T[R,G,B:]Message - scroll a message (nb: if message contains ':' it must be prefixed with ':')
+# T[R,G,B[,R,G,B][,S]:]Message - scroll a message (nb: if message contains ':' it must be prefixed with ':')
# F[H|V] - flip horizontal|vertical
# X[0|1] - high frequency reporting (accel/gyro/orientation/compass) off|on
# Y[0|1] - low frequency reporting (temperature/humidity/pressure) off|on
@@ -32,8 +32,11 @@ import sys
import glob
import time
import errno
+import ctypes
import select
import struct
+import inspect
+import threading
from sense_hat import SenseHat
@@ -69,40 +72,48 @@ lf_interval = 1
hf_enabled = False
lf_enabled = False
+scroll = None
+
+class ScrollThread(threading.Thread):
+ def __init__(self,fcol,bcol,speed,message):
+ threading.Thread.__init__(self)
+ self.fcol = fcol
+ self.bcol = bcol
+ self.message = message
+ self.speed = speed
+
+ def run(self):
+ global SH
+ old_rotation = SH.rotation
+
+ try:
+ SH.show_message(self.message,text_colour=self.fcol,back_colour=self.bcol,scroll_speed=self.speed)
+ except:
+ SH.set_rotation(old_rotation,False)
+ SH.clear(self.bcol);
+ pass
+
+ def interrupt(self):
+ if not self.isAlive():
+ raise threading.ThreadError()
+ for thread_id, thread_object in threading._active.items():
+ if thread_object == self:
+ r = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id,ctypes.py_object(StandardError))
+ if r == 1:
+ pass
+ else:
+ if r > 1:
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
+ raise SystemError()
+ return
+
+
+
def process_command(data):
- global hf_enabled, lf_enabled
- if data[0] == "R":
- SH.set_rotation(float(data[1:]))
- elif data[0] == "C":
- data = data[1:].strip()
- if len(data) > 0:
- s = data.split(",")
- col = (int(s[0]),int(s[1]),int(s[2]))
- else:
- col = (0,0,0)
- SH.clear(col)
- elif data[0] == "P":
- data = data[1:].strip()
- s = data.split(',')
- for p in range(0,len(s),5):
- SH.set_pixel(int(s[p]),int(s[p+1]),int(s[p+2]),int(s[p+3]),int(s[p+4]))
- elif data[0] == "T":
- data = data[1:].strip()
- col = (255,255,255)
- s = data.split(':',1)
- if len(s) == 2:
- data = s[1]
- if len(s[0]) > 0:
- c = s[0].split(",")
- col = (int(c[0]),int(c[1]),int(c[2]))
- SH.show_message(data,text_colour=col)
- elif data[0] == "F":
- if data[1] == "H":
- SH.flip_h()
- elif data[1] == "V":
- SH.flip_v()
- elif data[0] == "X":
+ global hf_enabled, lf_enabled,scroll
+
+ if data[0] == "X":
if data[1] == '0':
hf_enabled = False
else:
@@ -112,6 +123,61 @@ def process_command(data):
lf_enabled = False
else:
lf_enabled = True
+ else:
+ if threading.activeCount() == 2:
+ scroll.interrupt()
+ while scroll.isAlive():
+ time.sleep(0.01)
+ try:
+ scroll.interrupt()
+ except:
+ pass
+ if data[0] == "R":
+ SH.set_rotation(float(data[1:]))
+ elif data[0] == "C":
+ data = data[1:].strip()
+ if len(data) > 0:
+ s = data.split(",")
+ col = (int(s[0]),int(s[1]),int(s[2]))
+ else:
+ col = (0,0,0)
+ SH.clear(col)
+ elif data[0] == "P":
+ data = data[1:].strip()
+ s = data.split(',')
+ for p in range(0,len(s),5):
+ SH.set_pixel(int(s[p]),int(s[p+1]),int(s[p+2]),int(s[p+3]),int(s[p+4]))
+ elif data[0] == "T":
+ data = data[1:].strip()
+ tcol = (255,255,255)
+ bcol = (0,0,0)
+ speed = 0.1
+ s = data.split(':',1)
+ if len(s) == 2:
+ data = s[1]
+ if len(s[0]) > 0:
+ c = s[0].split(",")
+ if len(c) == 1:
+ speed = float(c[0])
+ elif len(c) == 3:
+ tcol = (int(c[0]),int(c[1]),int(c[2]))
+ if len(c) == 4:
+ tcol = (int(c[0]),int(c[1]),int(c[2]))
+ speed = float(c[3])
+ elif len(c) == 6:
+ tcol = (int(c[0]),int(c[1]),int(c[2]))
+ bcol = (int(c[3]),int(c[4]),int(c[5]))
+ elif len(c) == 7:
+ tcol = (int(c[0]),int(c[1]),int(c[2]))
+ bcol = (int(c[3]),int(c[4]),int(c[5]))
+ speed = float(c[6])
+ scroll = ScrollThread(tcol,bcol,speed,data);
+ scroll.start()
+ elif data[0] == "F":
+ if data[1] == "H":
+ SH.flip_h()
+ elif data[1] == "V":
+ SH.flip_v()
def idle_work():
global last_hf_time, last_lf_time