diff --git a/README.md b/README.md index fc7accf..a0ea95a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ control your blinkstick via http requests ## requirements -- nodejs +- nodejs 8.x - yarn / npm - node-gyp (probably; to build the node-hid module) diff --git a/blinky.js b/blinky.js index d9b6d74..81ad62f 100644 --- a/blinky.js +++ b/blinky.js @@ -1,23 +1,41 @@ // requirements -const blinkstick = require('./libs/blinkstick'); -const server = require('./libs/server'); -const logger = require('./libs/logger'); +const server = require('./libs/server.js'); +const util = require('./libs/util.js'); +const logger = require('./libs/logger.js'); const packageJSON = require('./package'); +const INTERRUPTS = ['SIGINT', 'SIGTERM']; + +global.config = process.argv[2] || __dirname + '/config.json'; + +// handle interrupts +handleInterrupts(); + // start the application main(); // main - let's get this party started function main() { - server.start() - .then(logger.info) - .then(server.handleRequests) - .catch(exit); + util.fileExists(global.config) + .then((result) => { + global.config = require(result.path); + global.config.path = result.path; + }) + .then(logger.initialize) + .then(server.start) + .then(logger.info) + .then(server.handleRequests) + .catch(exit); } // ... and it all comes crashing down -function exit(err) { - let code = 0; +function exit(err, code) { + if (code == undefined) { + code = 0; + if (err != undefined) { + code = 1; + } + } if (err) { logger.error(err); logger.error(packageJSON.name + " ended due to an error"); @@ -26,3 +44,11 @@ function exit(err) { } process.exit(code); } + +function handleInterrupts() { + for (var index = 0; index < INTERRUPTS.length; index++) { + process.once(INTERRUPTS[index], (code) => { + exit(null, code); + }); + } +} diff --git a/config.json b/config.json index fda54cc..7710b7f 100644 --- a/config.json +++ b/config.json @@ -1,11 +1,11 @@ { "server": { "listen": "0.0.0.0", - "port": 3000, - "timestamp": "DD.MM.YYYY HH:mm:ss:SS" + "port": 3000 }, "log": { - "level": "info" + "level": "debug", + "timestamp": "DD.MM.YYYY HH:mm:ss:SS" }, "api": { "get": { diff --git a/libs/blinkstick.js b/libs/blinkstick.js index 5508017..dc6d052 100644 --- a/libs/blinkstick.js +++ b/libs/blinkstick.js @@ -7,8 +7,7 @@ const blinkstick = require('blinkstick'); const hexcolor = require('hex-color-regex'); // constants -const RANDOM = "random" -const ANIMATION_STATE_INFINITE = 2; +const RANDOM = 'random' const ANIMATION_STATE_INPROGRESS = 1; const ANIMATION_STATE_FINISH = 0; @@ -17,19 +16,27 @@ let led; let animation = {}; function findBlinkstick() { - return new Promise(function (resolve, reject) { + return new Promise((resolve, reject) => { led = blinkstick.findFirst(); if (led !== undefined && led !== null) { return resolve(led); } - return reject("could not find any blinkstick"); + return reject('could not find any blinkstick'); }); } // turn the blinkstick off function powerOff() { - led.turnOff(); - logger.info("blinkstick powered off"); + return new Promise((resolve, reject) => { + findBlinkstick() + .then((led) => { + led.stop(); + led.turnOff(); + logger.info('blinkstick powered off'); + resolve(); + }) + .catch(reject); + }) } // parse a color object from given request arguments @@ -42,17 +49,17 @@ function parseColor(value) { parsedColor = parseHexColor(value); } return parsedColor || function () { - logger.warn("could not parse color value '" + value + "', defaulting to '" + config.api.post.color.default + "'"); + logger.warn('could not parse color value \'' + value + '\', defaulting to \'' + config.api.post.color.default + '\''); return config.api.post.color.default; }(); } // parse rgb color values function parseRGBColor(value) { - if (value.indexOf(",") == -1 && isRGBValue(value)) { + if (value.indexOf(',') == -1 && isRGBValue(value)) { return convertRGBToHex(parseInt(value) || 0, 0, 0); } else { - const splittedValues = value.split(","); + const splittedValues = value.split(','); let color = {}; for (let index = 0; index < splittedValues.length; index++) { const value = splittedValues[index]; @@ -77,7 +84,7 @@ function isRGBValue(value) { // convert rgb to hex color values function convertRGBToHex(red, green, blue) { - return "#" + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1); + return '#' + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1); } // parse hex color values @@ -99,10 +106,10 @@ function illuminate(blinkstickConfig) { findBlinkstick() .then(function () { switch (blinkstickConfig.mode) { - case "morph": + case 'morph': morph(blinkstickConfig, resolve, reject); break; - case "pulse": + case 'pulse': startPulsing(blinkstickConfig, resolve, reject); break; default: @@ -119,13 +126,13 @@ function setColor(blinkstickConfig, resolve, reject) { stopAnimation() .then(function () { setAnimationProperties(blinkstickConfig); - logger.debug("setting color to '" + blinkstickConfig.color + "'..."); + logger.debug('setting color to \'' + blinkstickConfig.color + '\'...'); led.setColor(blinkstickConfig.color, blinkstickConfig.options, function (err) { clearAnimationProperties(); if (err) { - return reject("error setting color '" + blinkstickConfig.color + "' (" + err + ")"); + return reject('error setting color \'' + blinkstickConfig.color + '\' (' + err + ')'); } - return resolve("set color to '" + blinkstickConfig.color + "'"); + return resolve('set color to \'' + blinkstickConfig.color + '\''); }); }); } @@ -135,22 +142,23 @@ function morph(blinkstickConfig, resolve, reject) { stopAnimation() .then(function () { setAnimationProperties(blinkstickConfig); - logger.debug("morphing color to '" + blinkstickConfig.color + "'..."); + logger.debug('morphing color to \'' + blinkstickConfig.color + '\'...'); + logger.debug('OPTS: ' + JSON.stringify(blinkstickConfig.options)); led.morph(blinkstickConfig.color, blinkstickConfig.options, function (err) { clearAnimationProperties(); if (err) { - return reject("error morphing color to '" + blinkstickConfig.color + "' (" + err + ")"); + return reject('error morphing color to \'' + blinkstickConfig.color + '\' (' + err + ')'); } - return resolve("morphed color to '" + blinkstickConfig.color + "'"); + return resolve('morphed color to \'' + blinkstickConfig.color + '\''); }); }); } // start pulsing function startPulsing(blinkstickConfig, resolve, reject) { - if (animation.state == ANIMATION_STATE_FINISH || (blinkstickConfig.options.pulse.max > 0 && (blinkstickConfig.options.pulse.done && blinkstickConfig.options.pulse.done < blinkstickConfig.options.pulse.max))) { + if (animation.state == ANIMATION_STATE_FINISH || (blinkstickConfig.options.pulse.max > 0 && (blinkstickConfig.options.pulse.done && blinkstickConfig.options.pulse.done == blinkstickConfig.options.pulse.max))) { clearAnimationProperties(); - return resolve("finished pulsing color '" + blinkstickConfig.color + "'"); + return resolve('finished pulsing color \'' + blinkstickConfig.color + '\''); } if (animation.id && animation.id != blinkstickConfig.id) { stopAnimation() @@ -165,10 +173,10 @@ function startPulsing(blinkstickConfig, resolve, reject) { led.pulse(blinkstickConfig.color, blinkstickConfig.options, function (err) { if (err) { clearAnimationProperties(); - return reject("error pulsing color '" + blinkstickConfig.color + "' (" + err + ")"); + return reject('error pulsing color \'' + blinkstickConfig.color + '\' (' + err + ')'); } blinkstickConfig.options.pulse.done++; - logger.debug("pulsed color '" + blinkstickConfig.color + "' " + blinkstickConfig.options.pulse.done + "/" + blinkstickConfig.options.pulse.max + " times"); + logger.debug('pulsed color \'' + blinkstickConfig.color + '\' ' + blinkstickConfig.options.pulse.done + '/' + blinkstickConfig.options.pulse.max + ' times'); startPulsing(blinkstickConfig, resolve, reject); }); } @@ -180,11 +188,7 @@ function setAnimationProperties(blinkstickConfig) { } led.animationsEnabled = true; animation.id = blinkstickConfig.id; - if (blinkstickConfig.options.pulse && blinkstickConfig.options.pulse.max === 0) { - animation.state = ANIMATION_STATE_INFINITE; - } else { - animation.state = ANIMATION_STATE_INPROGRESS; - } + animation.state = ANIMATION_STATE_INPROGRESS; } // clear properties for the current animation @@ -219,11 +223,6 @@ function isAnimationInProgress() { return animation != undefined && animation.state != undefined; } -// is currently an infinite animation in progress -function isInfiniteAnimationInProgress() { - return isAnimationInProgress() && animation.state == ANIMATION_STATE_INFINITE; -} - // exports module.exports = { parseColor, diff --git a/libs/logger.js b/libs/logger.js index 9e17daf..118d410 100644 --- a/libs/logger.js +++ b/libs/logger.js @@ -1,21 +1,35 @@ -// requirements -const config = require("../config.json"); -// third party requirements -const moment = require("moment"); +const moment = require('moment'); // constants -const LOG_PREFIX_DEBUG = "debug"; -const LOG_PREFIX_INFO = "info"; -const LOG_PREFIX_WARNING = "warning"; -const LOG_PREFIX_ERROR = "error"; +const LOG_PREFIX_DEBUG = 'debug'; +const LOG_PREFIX_INFO = 'info'; +const LOG_PREFIX_WARNING = 'warning'; +const LOG_PREFIX_ERROR = 'error'; const LOGLEVEL_DEBUG = 0; const LOGLEVEL_INFO = 1; const LOGLEVEL_WARNING = 2; const LOGLEVEL_ERROR = 3; -// set loglevel on "require" -const loglevel = function() { - switch (config.log.level) { +var loglevel = getLogLevel(); +var timestamp = getTimestamp(); + +function initialize() { + return new Promise((resolve, reject) => { + if (global.config == undefined) { + reject('could not initialize logger, config is undefined'); + } + loglevel = getLogLevel(); + timestamp = getTimestamp(); + resolve(); + }); +} + +// get the loglevel +function getLogLevel() { + if (global.config == undefined || global.config.log == undefined || global.config.log.level == undefined) { + return LOGLEVEL_INFO; + } + switch (global.config.log.level) { case LOG_PREFIX_DEBUG: case LOGLEVEL_DEBUG: return LOGLEVEL_DEBUG; @@ -31,20 +45,31 @@ const loglevel = function() { default: return LOGLEVEL_INFO; } -}(); +} + +// get the timestamp format +function getTimestamp() { + if (global.config != undefined && global.config.log != undefined && global.config.log.format != undefined) { + return global.config.log.timestamp; + } + return "DD.MM.YYYY HH:mm:ss:SS"; +} // log a http request -function logRequest(request) { - let message = "[" + request.method + "] url: \"" + request.url + "\""; +function http(request) { + if (request == undefined) { + return; + } + let message = '[' + request.method + '] url: \'' + request.url + '\''; let counter = 1; for (let param in request.body) { - message += ", parameter " + counter + ": \"" + param + "=" + request.body[param] + "\""; + message += ', parameter ' + counter + ': \'' + param + '=' + request.body[param] + '\''; counter++; } debug(message.trim()); } -// prefix log with "info" +// prefix log with 'info' function info(message) { if (loglevel > LOGLEVEL_INFO) { return; @@ -52,28 +77,34 @@ function info(message) { trace(message); } -// prefix log with "info" +// prefix log with 'info' function warn(message) { if (loglevel > LOGLEVEL_WARNING) { return; } - trace(message, "warning"); + trace(message, 'warning'); } -// prefix log with "debug" +// prefix log with 'debug' function debug(message) { if (loglevel > LOGLEVEL_DEBUG) { return; } - trace(message, "debug"); + trace(message, 'debug'); } -// prefix log with "error" +// prefix log with 'error' function error(message) { if (loglevel > LOGLEVEL_ERROR) { return; } - trace(message, "error"); + if (message.errors != undefined) { + for (var index = 0; index < message.errors.length; index++) { + trace(message.errors[index], 'error'); + } + return; + } + trace(message, 'error'); } // default logging function @@ -82,31 +113,32 @@ function trace(message, prefix) { return; } if (prefix === undefined || prefix === null || prefix.length === 0) { - prefix = "info"; + prefix = 'info'; } let print; switch (prefix) { - case "error": + case 'error': print = console.error; break; - case "debug": + case 'debug': print = console.debug; break; - case "warning": + case 'warning': print = console.warn; break; default: print = console.log; } - message = moment().format(config.server.timestamp) + " | " + prefix + " > " + message; + message = moment().format(timestamp) + ' | ' + prefix + ' > ' + message; print(message); } // exports module.exports = { + initialize, info, warn, debug, error, - logRequest -}; \ No newline at end of file + http +} \ No newline at end of file diff --git a/libs/server.js b/libs/server.js index e22c5b3..5ec0085 100644 --- a/libs/server.js +++ b/libs/server.js @@ -77,19 +77,23 @@ function start() { function handleRequests() { // GET methods - app.get('*', function (request, response) { - logger.logRequest(request); + app.get('*', (request, response) => { + logger.http(request); response.send(getHTML()); response.end(); }); + app.post('/off', (request, response) => { + logger.http(request); + response.end(); + blinkstick.powerOff(); + }); // POST methods - app.post('*', function (request, response) { - logger.logRequest(request); - response.end(); - blinkstick.illuminate(parseRequest(request.body)) - .then(logger.info) - .catch(logger.error); - return; + app.post('*', (request, response) => { + logger.http(request); + response.end(); + blinkstick.illuminate(parseRequest(request.body)) + .then(logger.info) + .catch(logger.error); }); } @@ -97,20 +101,26 @@ function handleRequests() { function parseRequest(data) { let blinkstickConfig = { "id": Math.random(), - "mode": data["mode"] || config.api.post.mode.default, - "color": blinkstick.parseColor(data["color"]), + "mode": data.mode || config.api.post.mode.default, + "color": blinkstick.parseColor(data.color), "options": { - "duration": data["duration"] || config.api.post.duration.default, + "steps": data.steps, + "duration": data.duration || config.api.post.duration.default, "pulse": { - "max": data["pulses"] || 0, + "max": data.pulses || 0, "done": 0 } } }; - blinkstickConfig.options.steps = blinkstickConfig.options.duration / 10; + if (data.index != undefined) { + blinkstickConfig.options.index = data.index; + } if (blinkstickConfig.options.duration < 100) { blinkstickConfig.options.duration = 100; } + if (blinkstickConfig.options.steps == undefined || blinkstickConfig.options.steps == 0) { + blinkstickConfig.options.steps = blinkstickConfig.options.duration / 10; + } return blinkstickConfig; } diff --git a/libs/util.js b/libs/util.js new file mode 100644 index 0000000..5dc77d4 --- /dev/null +++ b/libs/util.js @@ -0,0 +1,39 @@ +const realpath = require('fs').realpath; +const stat = require('fs').stat; + +function fileExists(file) { + return new Promise((resolve, reject) => { + if (file == undefined) { + reject('can not check the existence of an undefined file'); + } + resolvePath(file) + .then((path) => { + stat(path, (err, stats) => { + if (err) { + reject(err); + } + resolve({ path, stats }); + }) + }) + .catch(reject); + }); +} + +function resolvePath(file) { + return new Promise((resolve, reject) => { + if (file == undefined) { + reject('can not resolve a path to an undefined file'); + } + realpath(file, (err, resolvedPath) => { + if (err) { + reject('resolving path \'' + file + '\' encountered an error >>> ' + err); + } + resolve(resolvedPath); + }) + }); +} + +module.exports = { + fileExists, + resolvePath +} \ No newline at end of file