UNPKG

node-red-contrib-easybotics-led-matrix

Version:

control led matrix with node-red on a raspberryPI, node-red binding of: https://www.npmjs.com/package/easybotics-rpi-rgb-led-matrix

734 lines (592 loc) 18.9 kB
var Matrix = require('easybotics-rpi-rgb-led-matrix') var getPixels = require('get-pixels') var dp = require('./displayPrimitives.js') var parse = require('./parsers.js') //var led = new LedMatrix(64, 64, 1, 2, 'adafruit-hat-pwm') module.exports = function(RED) { var led var nodeRegister var context = 0 /* * a config node that holds global state for the led matrix * nodes that want to use the hardware will hook into an instance of this * but right now it uses global var 'led' meaning its limited to one hardware output per node-red instance */ function LedMatrix(n) { RED.nodes.createNode(this, n) const node = this var lastDraw = 0 var drawSpeed = 0 //get the field settings, these inputs are defined in the html node.width = parse.validateOrDefault(n.width, 64) node.height = parse.validateOrDefault(n.height, 64) node.chained = parse.validateOrDefault(n.chained, 2) node.parallel = parse.validateOrDefault(n.parallel, 1) node.brightness = parse.validateOrDefault(n.brightness, 100) node.mapping = (n.mapping || 'adafruit-hat-pwm') node.rgbSequence = parse.validateOrDefault(n.rgbSequence, 'RGB', parse.validateRGBSequence) node.refreshDelay = parse.validateOrDefault(n.refreshDelay, 500) node.autoRefresh = (n.autoRefresh) node.cmdArgs = n.cmdArgs.split(' ') console.log(node.cmdArgs) context++ //nodes that wish to draw things on the matrix register themselves in the 'nodeRegister' set //then when this node.draw callback is called we sort the set by Z level and call their draw callbacks node.draw = function() { led.clear() var nArray = [] for(let n of nodeRegister) { nArray.push(n) } nArray.sort(function(a, b) { const aa = a.zLevel != undefined ? a.zLevel : -99 const bb = b.zLevel != undefined ? b.zLevel : -99 return aa > bb }) for(let n of nArray) { n.draw() } led.update() } //nodes can request the display be refreshed, redrawing every registered node //however their is a ratelimiting in effect based on the refreshDelay property node.refresh = function () { if (!node.autoRefresh) {return} const currentMilli = Date.now() const passed = currentMilli - lastDraw var actualDelay = node.refreshDelay if(node.refreshDelay < (drawSpeed)) { actualDelay = parseInt(node.refreshDelay) + parseInt(drawSpeed) } if (passed > actualDelay) { node.draw() lastDraw = currentMilli if(actualDelay > node.refreshDelay) { node.log('using delay ' + actualDelay) } } } //if led is undefined we create a new one //some funky stuff due to the global state we're managing if(!led) { node.warn('initing led') led = new Matrix(parseInt(node.height), parseInt(node.width), parseInt(node.parallel), parseInt(node.chained), parseInt(node.brightness), node.mapping, node.rgbSequence, node.cmdArgs) led.clear() led.update() if(!nodeRegister) nodeRegister= new Set() } else { led.brightness( node.brightness) } //otherwise we clear the one we have, without these checks it can spawn new evertime we deploy led.clear() led.update() nodeRegister.clear() } /* * this node pushes the frame buffer to the hardware * faster than updating after every pixel change */ function RefreshMatrix (config) { RED.nodes.createNode(this, config) const node = this node.matrix = RED.nodes.getNode(config.matrix) node.on('input', function() { led.clear() node.matrix.draw(); led.update() }) } /* * Takes an image URI (not URL) and caches it in memory as an array of pixels * Can also cache an animated gif as a higher dimensional array * Increments frame when poked, and draws its cache to the display when requested */ function ImageToPixels (config) { RED.nodes.createNode(this, config) const node = this node.matrix = RED.nodes.getNode(config.matrix) //get config data node.offset = new dp.Point(parse.validateOrDefault(config.xOffset, 0), parse.validateOrDefault(config.yOffset, 0)) node.zLevel = parse.validateOrDefault(config.zLevel, 0) node.file = config.file //info about the frame we've built last; expensive so we want to avoid repeating this if we can! node.currentFrame = 0 node.frames = 0 node.cache = undefined //callback used by the LED matrix object //first we register ourselves to be drawn, and then wait for LED matrix to use this callback //we can also manually request for the LED matrix to come and draw us node.draw = function () { if(node.cache) { for(const tuple of node.cache[node.currentFrame]) { tuple.point.draw(led, tuple.color, node.offset) } } } node.clear = function () { nodeRegister.delete(node) node.matrix.refresh() } //function to actually send the output to the next node function readySend () { nodeRegister.add(node) node.matrix.refresh() } //function that takes a file and tries to convert the file into a stream of pixels //takes a callback which is handed the output and the number of frames //imagine if it returned a promise though! //probably uncesarry because we dont actually have to syncronize this to drawing, drawing when done is fine function createPixelStream (file, callback) { const cc = context getPixels(file, function(err, pixels, c = cc) { var output = [] if(!pixels) { node.error('image did not convert correctly\n please check the url or file location') return } const width = pixels.shape.length == 4 ? Math.min(128, pixels.shape[1]) : Math.min(128, pixels.shape[0]) const height = pixels.shape.length == 4 ? Math.min(128, pixels.shape[2]) : Math.min(128, pixels.shape[1]) //for getPixels, all gifs need to be treated the same way, even //single frame ones. this is why we need the gif variable, so //a single frame gif's pixels won't be accessed the same as a still image const isGif = pixels.shape.length == 4 const frames = isGif ? pixels.shape[0] : 1 //loop agnostic between images and gifs for(var frame = 0; frame < frames; frame++) { output[frame] = [] for(let x = 0; x < width; x++) { if(c != context) return for(let y = 0; y < height; y++) { //getting pixel is different for still images const r = isGif ? pixels.get(frame, x, y, 0) : pixels.get(x, y, 0) const g = isGif ? pixels.get(frame, x, y, 1) : pixels.get(x, y, 1) const b = isGif ? pixels.get(frame, x, y, 2) : pixels.get(x, y, 2) if(!(r || g || b)) continue //push to output array output[frame].push({point: new dp.Point(x, y), color: new dp.Color().fromRgb(r, g, b)}) } } } //call our send function from earlier //just sets the cache and the number of frames, remember that still images have '0' frames if(c == context) { //give our callback function the output and the number of frames callback(output, frames) } }) } //if we receive input node.on('input', function(msg) { if(msg.clear) { node.clear() return } var runFile = undefined //catch various attemps to modify the file and offset, either via direct injection //or via a msg.payload.data property if(typeof msg.payload === 'string') { runFile = msg.payload } else if(msg.payload.data) { runFile = msg.payload.data } else //if we don't do any type of payload and use the edit dialog instead { runFile = node.file } if(msg.payload.x !== undefined && msg.payload.y !== undefined){ node.offset = new dp.Point(msg.payload.x, msg.payload.y) } //make a cache for the image if it doesn't exist or it's for a different image if(node.cache == undefined || runFile != node.file) { node.file = runFile //set cache to an intermediate but valid state //thatway we only run when node.cache is in an UNDEFINED state, or we change the file //we only draw node.cache when it is in a valid drawable state //undefined -> intermediate -> drawable node.cache = false //give create pixel stream a callback which sets node.cache to a state that node.draw can use createPixelStream(node.file, function (output, frames) { node.cache = output node.frames = frames readySend() node.cache = output }) return } //update frame on animated images node.currentFrame++ if(node.currentFrame >= node.frames) node.currentFrame = 0 readySend(); }) } //clears our internal framebuffer, doesn't clear the hardware buffer though function ClearMatrix (config) { RED.nodes.createNode(this, config) const node = this node.on('input', function(msg) { if(msg.payload) { led.clear() } }) } /* * draws text to the buffer, if updated it tries to erease its previous drawing first */ function Text (config) { RED.nodes.createNode(this, config) var node = this node.matrix = RED.nodes.getNode(config.matrix) //fetch the matrix instantiation from the config node.prefix = config.prefix || '' node.source = config.source || 'msg.payload' node.font = config.font node.xOffset = parse.validateOrDefault(config.xOffset, 0) node.yOffset = parse.validateOrDefault(config.yOffset, 0) node.rgb = config.rgb node.zLevel = parse.validateOrDefault(config.zLevel, 2) var outputInfo node.draw = function () { if(outputInfo != undefined) { //when drawing we calculate the fonr directory, and call the drawText const color = new dp.Color().fromRgbString(outputInfo.rgb) const fontDir = __dirname + '/fonts/' + node.font led.drawText(parseInt(outputInfo.x), parseInt(outputInfo.y), outputInfo.data, fontDir, parseInt(color.r), parseInt(color.g), parseInt(color.b)) } } //dont draw this node anymore on the matrix node.clear = function () { nodeRegister.delete(node) node.matrix.refresh() } node.on('input', function(msg) { if(msg.clear) { node.clear() return } //scary and dangerous! //RCE waiting to happen const outputData = eval( node.source) //round floats const handleFloat = function (i) { if( !isNaN(i)) { return Math.round(i * 100) / 100 } return i } //handle being handed a struct instead of a string if(outputData != undefined) { outputInfo = { x : outputData.x ? outputData.x : node.xOffset, y : outputData.y ? outputData.y : node.yOffset, data: node.prefix + handleFloat((outputData.data ? outputData.data : outputData)), rgb: outputData.rgb || node.rgb, } nodeRegister.add(node) node.matrix.refresh() } }) } /* * node to create and modify .data objects we send to different display nodes */ function PixelDataTransform (config) { RED.nodes.createNode(this, config) const node = this node.matrix = RED.nodes.getNode(config.matrix) node.xOffset = (config.xOffset || 0) node.yOffset = (config.yOffset || 0) node.refresh = (config.refresh || 0) node.rgb = (config.rgb || '255,255,255') function outputFromString (msg) { const output = { data: msg.payload, x: parseInt(node.xOffset), y: parseInt(node.yOffset), refresh: parseInt(node.refresh), rgb: node.rgb, } msg.payload = output node.send( msg) } function outputFromObject (msg) { const output = { data: msg.payload.data, x : parseInt(node.xOffset), y : parseInt(node.yOffset), refresh: parseInt(node.refresh), rgb : node.rgb, } msg.payload = output node.send( msg) } node.on('input', function(msg) { if (typeof msg.payload == 'string') { return outputFromString(msg) } return outputFromObject(msg) }) } /* * node to print a circle to the matrix buffer */ function Circle (config) { RED.nodes.createNode(this, config) const node = this var outputInfo node.matrix = RED.nodes.getNode(config.matrix) node.xPos = parse.validateOrDefault(config.xPos, 0) node.yPos = parse.validateOrDefault(config.yPos, 0) node.radius = parse.validateOrDefault(config.radius, 0) node.rgb = (config.rgb || '255,255,255') node.zLevel = parse.validateOrDefault(config.zLevel, 1) node.draw = function() { if (outputInfo != undefined) { let o = outputInfo led.drawCircle( o.x, o.y, o.radius, o.color.r, o.color.g, o.color.b) } } node.clear = function () { nodeRegister.delete(node) node.matrix.refresh() } node.on('input', function (msg) { if(msg.clear) { node.clear() return } const data = msg.payload.data != undefined ? msg.payload.data : msg.payload outputInfo = { color : data.rgb != undefined ? new dp.Color().fromRgbString(data.rgb) : new dp.Color().fromRgbString(node.rgb), y : data.y != undefined ? parseInt(data.y) : parseInt(node.yPos), x : data.x != undefined ? parseInt(data.x) : parseInt(node.xPos), radius : data.radius != undefined ? parseInt(data.radius) : parseInt(node.radius), } nodeRegister.add(node) node.matrix.refresh() }) } /*draws a bounded polygon to the display, either filled or not filled * can be designed in the settings using a little drawing tool */ function Polygon (config) { RED.nodes.createNode(this, config) const node = this node.matrix = RED.nodes.getNode(config.matrix) //get the config data we'll use later node.zLevel = parse.validateOrDefault(config.zLevel, 1) node.savedPts = config.savedPts node.offset = new dp.Point(parse.validateOrDefault(config.xOffset, 0), parse.validateOrDefault(config.yOffset, 0)) node.rgb = config.rgb || '255,255,255' node.filled = config.filled || false node.oldPoints = undefined node.oldRgb = undefined node.oldFilled = undefined //the data we'll use to actually draw starts off empty node.polygon = undefined node.color = undefined //this functin returns a dp Polygon based on the config data //we only call this if the user doesn't want to draw their own custom polygon node.buildFromConfig = function(points, filled) { const realPoints = new Array() //fill realPoints with dp points to make a polygon later for(var i = 0; i < points.length; i++) { const x = points[i].x const y = points[i].y realPoints.push(new dp.Point(x, y)) } //create our DP polygon const polygon = new dp.Polygon(realPoints) if(filled) polygon.fill(node.matrix.refresh) return polygon } node.draw = function () { if(node.polygon && node.color) { node.polygon.draw(led, node.color, node.offset) } } node.clear = function () { nodeRegister.delete(node) node.matrix.refresh() } node.on('input', function (msg) { if(msg.clear) { node.clear() return } const data = msg.payload var runPts = undefined var runColor = undefined var runFilled = undefined //if we aren't handed new data we'll use the one from last time if(data.savedPts) runPts = data.savedPts if(data.filled) runFilled = data.filled if(data.rgb) runColor = data.rgb if(!runPts) runPts = node.savedPts if(!runFilled) runFilled = node.filled if(!runColor) runColor = node.rgb //color is cheap so we'll just set this every time node.color = new dp.Color().fromRgbString(runColor) //can we use the cached polygon? if(node.polygon && (node.oldPoints == runPts && node.oldFilled == runFilled)) return //don't redo this if we haven't had user data and the config hasn't changed //this if statement will need changing node.polygon = node.buildFromConfig(runPts, runFilled) node.oldPoints = runPts node.oldFilled = runFilled if(data.xOffset != undefined && data.yOffset != undefined) { node.offset = new dp.Point(data.xOffset, data.yOffset) } //dont forget to register our node to be drawn readySend() return }) function readySend () { nodeRegister.add(node) node.matrix.refresh() } } /*draws a scaled line graph in a boxed area*/ function Graph (config) { RED.nodes.createNode(this, config) const node = this node.matrix = RED.nodes.getNode(config.matrix) //get the config data we'll use later node.zLevel = -10 //these are the values we'll scale the graph between //for example, with the default values here //100 would be the very bottom of the graph //and 1500 would be the very top of the graph node.inMin = parse.validateOrDefault(config.minIn, 100) node.inMax = parse.validateOrDefault(config.maxIn, 1500) //width and height of the graph, origin is the top left corner node.width = parse.validateOrDefault(config.width, 100) node.height = parse.validateOrDefault(config.height, 50) //origin point node.x = parse.validateOrDefault(config.xOffset, 0) node.y = parse.validateOrDefault(config.yOffset, 0) //how far along the graph we're in node.tick = 0 node.rgb = parse.validateOrDefault(config.rgb, '255,255,255') node.rgbTick = parse.validateOrDefault(config.rgbTick, '255, 0, 0') node.color = new dp.Color().fromRgbString(node.rgb) node.bg = new dp.Color().fromRgbString(node.rgbTick) node.data = [] node.raw = 0 //normalizer node.scale = function (data, inMin, inMax, outMin, outMax) { return ( ((data - inMin) / (inMax - inMin)) * (outMax - outMin)) + outMin } node.on('input', function (msg) { if(msg.clear) { node.clear() return } const data = node.scale(msg.payload, node.inMin, node.inMax, node.height - 1, 0) node.raw = data node.data[node.tick % node.width] = new dp.Point((node.tick % node.width), data) node.tick++ readySend() }) node.draw = function () { if(node.data[0]) { const offset = new dp.Point(node.x, node.y) for(const point of node.data) point.draw(led, node.color, offset) dot = new dp.Line(new dp.Point((node.tick - 1) % node.width, node.raw - 1), new dp.Point((node.tick - 1) % node.width, node.raw + 1)) dot.draw(led, node.bg, new dp.Point(node.x, node.y)) } } function readySend() { nodeRegister.add(node) node.matrix.refresh() } } //register our functions with node-red RED.nodes.registerType('led-matrix', LedMatrix) //RED.nodes.registerType('clear-matrix', ClearMatrix) RED.nodes.registerType('graph-to-matrix', Graph) RED.nodes.registerType('refresh-matrix', RefreshMatrix) RED.nodes.registerType('image-to-matrix', ImageToPixels) RED.nodes.registerType('text-to-matrix', Text) RED.nodes.registerType('pixel-transform', PixelDataTransform) RED.nodes.registerType('circle', Circle) RED.nodes.registerType('polygon', Polygon) }