UNPKG

node-red-raspicam

Version:

A node that will take a single, or a timelapse series of images and save to file.

296 lines (270 loc) 12.7 kB
"use strict" const spawn = require('child_process').spawn const fs = require('fs') const Writable = require('stream').Writable const url = require('url') const raspistill_def = require('./raspistill_def.js') module.exports = function (RED) { function Raspicam(config) { RED.nodes.createNode(this, config) raspistill_def.params.forEach((param) => { if (param.active) { this[param.name] = config[param.name] this[param.name + "type"] = config[param.name + "type"] } }) this.filename = config.filename this.filenametype = config.filenametype || "str" this.path = config.path || "" this.pathtype = config.pathtype || "str" var raspicam_process = null class MsgStream extends Writable { constructor(options) { super(options); } _write(chunk, encoding, callback) { callback() } } var buildParams = (msg) => { let parameters = [] raspistill_def.params.forEach((currentValue, index, array) => { if (this.hasOwnProperty(currentValue.name) && currentValue.active && this[currentValue.name] !== "") { if (currentValue.name == "timelapse" && this[currentValue.name + "type"] == "bool") { // If in single mode, do not output a timelapse parameter } else { if (currentValue.type == "boolean") { if (this[currentValue.name] == true) { parameters.push(currentValue.alias) } } else { parameters.push(currentValue.alias) if (this[currentValue.name + "type"] == "msg") { parameters.push(msg[this[currentValue.name]]) } else { if (currentValue.type == 'number') { parameters.push(parseInt(this[currentValue.name])) } else { if (currentValue.name == 'output') { if (this.output == "-") { parameters.push("-") } else { var pathStr = (this.pathtype == "str") ? this.path : msg[this.path] var filename = (this.filenametype == "str") ? this.filename : msg[this.filename] parameters.push(pathStr + filename) } } else { parameters.push(this[currentValue.name]) } } } } } } }) parameters.push("--nopreview") RED.log.info("Raspicam: parameters for spawned process: " + JSON.stringify(parameters)) return parameters } var buildParamsPreview = (msg) => { let parameters = [] raspistill_def.params.forEach((currentValue, index, array) => { if (currentValue.active && msg[currentValue.name]) { if (currentValue.name == "timelapse" || currentValue.name == "output" || currentValue.name == "timeout") { // If in single mode, do not output a timelapse parameter } else { if (currentValue.type == "boolean") { if (msg[currentValue.name] == true) { parameters.push(currentValue.alias) } } else { parameters.push(currentValue.alias) if (currentValue.type == 'number') { parameters.push(parseInt(msg[currentValue.name])) } else { if (currentValue.name == 'output') { parameters.push("-") } } } } } }) parameters.push("--nopreview") parameters.push("--output") parameters.push("-") parameters.push("--timeout") parameters.push(100) RED.log.debug("raspicam: preview parameters for child process:" + JSON.stringify(parameters)) return parameters } this.on('close', (callback) => { raspicam_process.kill("SIGTERM", 1880) callback() }) this.on('input', (msg) => { RED.log.info(`Raspicam: Input Message: ` + JSON.stringify(msg)) if (this.output == "-") { msg.payload = new MsgStream() } else { var pathStr = (this.pathtype == "str") ? this.path : msg[this.path] if (!fs.existsSync(pathStr)) { fs.mkdirSync(pathStr) this.framestart = 0 } if (this.framestart == "") { let files = fs.readdirSync(pathStr) let framestart = 0 files.forEach((element, index) => { var frame = parseInt(element.replace(/[^0-9\.]/g, '')) if (frame > framestart) { framestart = frame } }) if (framestart > 0) { this.framestart = framestart + 1 } } } RED.log.info(`Raspicam: Starting capture`) this.status({ fill: "green", shape: "dot", text: "Active" }); raspicam_process = spawn(raspistill_def.command.name, buildParams(msg)) var currentFile; var dataStr = "" raspicam_process.stdout.on('data', (data) => { //msg.payload.write(data) RED.log.debug(`Raspicam sending ` + data.length + " bytes") }) raspicam_process.stdout.on('end', (data) => { msg.payload = currentFile RED.log.info(`Raspicam sending end `) this.send([msg, null]) }) raspicam_process.stderr.on('data', (data) => { if (this.output == "-") { // output image direct to msg.payload if (msg.payload) { } //msg.payload.write(data) } else { dataStr = dataStr + data.toString() while (dataStr.indexOf('\n') > -1) { if (dataStr.startsWith("Opening output file ")) { currentFile = dataStr.substr(20, dataStr.indexOf('\n') - 20) RED.log.debug(`stderr raspicam setting current: ${currentFile}`) } if (dataStr.startsWith("Finished capture ")) { var outmsg = msg outmsg.payload = currentFile if (this.timelapse !== "") { this.send([outmsg, null]) } RED.log.debug(`stderr raspicam finished capture of ${currentFile}`) } dataStr = dataStr.substr(dataStr.indexOf("\n") + 1) } } }) raspicam_process.on('close', (code, signal) => { switch (code) { case null: RED.log.warn(`Raspicam: process exited with signal ${signal}`) raspicam_process = null return case 0: RED.log.debug(`Raspicam: process exited with code: ${code} signal: ${signal}`) break case 64: RED.log.error(`Raspicam: code ${code}` + " Bad command line parameter") break case 70: RED.log.error(`Raspicam: code ${code}` + " Software or camera error") break case 130: RED.log.error(`Raspicam: code ${code}` + " Application terminated by Ctrl-C") break default: RED.log.error(`Raspicam: Process exited with code: ${code} signal: ${signal}`) } if (!msg.raspicam) { msg.raspicam = {} } if (this.timelapse > 0) { if (!msg.raspicam.timelapse) { msg.raspicam.timelapse = {} } if (this.output == "-") { msg.raspicam.timelapse.outputmode = "stream" } else { msg.raspicam.timelapse.outputmode = "file" msg.raspicam.timelapse.directory = this.path msg.raspicam.timelapse.file_pattern = this.filename } } else { if (!msg.raspicam.image) { msg.raspicam.image = {} } if (this.output == "-") { msg.raspicam.image.outputmode = "stream" } else { msg.raspicam.image.outputmode = "file" msg.raspicam.image.filename = this.output } msg.payload = currentFile } this.send([null, msg]) this.status({ fill: "yellow", shape: "dot", text: "Idle" }) raspicam_process = null }) raspicam_process.on('disconnect', () => { console.log("Raspicam disconnected") }) raspicam_process.on('error', (err) => { console.log("Raspicam error:" + JSON.stringify(err)) }) raspicam_process.on('exit', (code, signal) => { console.log("Raspicam exit code:" + code + " signal: " + signal) }) }) RED.httpAdmin.get("/raspicam/image-preview", (req, res) => { let reqUrl = url.parse(req.url, true) var message = {} for (var prop in reqUrl.query) { if (prop.startsWith("node-input-")) { message[prop.substr(11)] = reqUrl.query[prop] } } var raspicam = spawn(raspistill_def.command.name, buildParamsPreview(message)) raspicam.stdout.on('data', (data) => { if (!res.headersSent) { res.writeHead(200, { 'Content-Type': 'image/jpeg', 'Transfer-Encoding': 'chunked' }) } res.write(data) }) raspicam.stderr.on('data', (data) => { res.status(500) res.end(data) }) raspicam.on('close', (code) => { if (!res.headerSent) { res.status(500) res.end(code) } }) }) RED.httpAdmin.get("/raspicam/raspistill_def.js", (req, res) => { var readStream = fs.createReadStream(__dirname + "/raspistill_def.js") if (readStream) { res.status(200) res.setHeader('Content-Type', 'application/javascript') readStream.pipe(res); } else { res.send(404) } }) this.status({ fill: "yellow", shape: "dot", text: "Idle" }) } RED.nodes.registerType("raspicam", Raspicam) }