From 0cc33db58fadcfb2c276d62a26a853dcd68dd748 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 4 Mar 2016 17:12:02 +0000 Subject: [PATCH] Add out node --- hardware/sensehat/README.md | 77 +++++++++-- hardware/sensehat/colours.js | 220 ++++++++++++++++++++++++++++++++ hardware/sensehat/sensehat.html | 73 ++++++++++- hardware/sensehat/sensehat.js | 118 +++++++++++++++++ hardware/sensehat/sensehat.py | 132 ++++++++++++++----- 5 files changed, 573 insertions(+), 47 deletions(-) create mode 100644 hardware/sensehat/colours.js 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