initial commit
This commit is contained in:
commit
7be704eeb9
12 changed files with 719 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
npm-debug.log
|
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Program",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}/blinky.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
MIT License Copyright (c) <year> <copyright holders>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice (including the next
|
||||||
|
paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||||
|
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||||
|
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
7
README.md
Normal file
7
README.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# blinky
|
||||||
|
|
||||||
|
control your blinkstick via http GET and POST requests
|
||||||
|
|
||||||
|
## requirements
|
||||||
|
|
||||||
|
- node v8.17.0
|
28
blinky.js
Normal file
28
blinky.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// requirements
|
||||||
|
const blinkstick = require('./libs/blinkstick');
|
||||||
|
const server = require('./libs/server');
|
||||||
|
const logger = require('./libs/logger');
|
||||||
|
const packageJSON = require('./package');
|
||||||
|
|
||||||
|
// start the application
|
||||||
|
main();
|
||||||
|
|
||||||
|
// main - let's get this party started
|
||||||
|
function main() {
|
||||||
|
server.start()
|
||||||
|
.then(logger.info)
|
||||||
|
.then(server.handleRequests)
|
||||||
|
.catch(exit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... and it all comes crashing down
|
||||||
|
function exit(err) {
|
||||||
|
let code = 0;
|
||||||
|
if (err) {
|
||||||
|
logger.error(err);
|
||||||
|
logger.error(packageJSON.name + " ended due to an error");
|
||||||
|
} else {
|
||||||
|
logger.info(packageJSON.name + " shutting down gracefully")
|
||||||
|
}
|
||||||
|
process.exit(code);
|
||||||
|
}
|
43
config.json
Normal file
43
config.json
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"listen": "0.0.0.0",
|
||||||
|
"port": 3000,
|
||||||
|
"timestamp": "DD.MM.YYYY HH:mm:ss:SS"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"level": "info"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"get": {
|
||||||
|
"description": "show this page"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"mode": {
|
||||||
|
"available": "set, morph, pulse",
|
||||||
|
"default": "set",
|
||||||
|
"description": "specifies the color change mode"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"available": "random, hex color codes (#ffffff), rgb color codes (255, 255, 255)",
|
||||||
|
"default": "random",
|
||||||
|
"description": "specifies the color to change to"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"available": "number values",
|
||||||
|
"default": 1000,
|
||||||
|
"description": "specifies the duration of the color change animation in milliseconds"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"available": "number values",
|
||||||
|
"default": "[duration] / 10",
|
||||||
|
"description": "specifies the amount of steps for the color change"
|
||||||
|
},
|
||||||
|
"pulses": {
|
||||||
|
"restrictions": "pulse",
|
||||||
|
"available": "number values",
|
||||||
|
"default": "0 (infinite)",
|
||||||
|
"description": "specifies the number of pulses"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
259
libs/blinkstick.js
Normal file
259
libs/blinkstick.js
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
// requirements
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
|
// third party requirements
|
||||||
|
const blinkstick = require('blinkstick');
|
||||||
|
const async = require('async');
|
||||||
|
const hexcolor = require('hex-color-regex');
|
||||||
|
|
||||||
|
// constants
|
||||||
|
const COLORTYPE_RGB = "rgb";
|
||||||
|
const COLORTYPE_HEX = "hex";
|
||||||
|
const ANIMATION_STATE_INFINITE = 2;
|
||||||
|
const ANIMATION_STATE_INPROGRESS = 1;
|
||||||
|
const ANIMATION_STATE_FINISH = 0;
|
||||||
|
|
||||||
|
// variables
|
||||||
|
let led;
|
||||||
|
let animation = {};
|
||||||
|
|
||||||
|
function findBlinkstick() {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
led = blinkstick.findFirst();
|
||||||
|
if (led !== undefined && led !== null) {
|
||||||
|
return resolve(led);
|
||||||
|
}
|
||||||
|
return reject("could not find any blinkstick");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn the blinkstick off
|
||||||
|
function powerOff() {
|
||||||
|
led.turnOff();
|
||||||
|
logger.info("blinkstick powered off");
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse a color object from given request arguments
|
||||||
|
function parseColor(blinkstickConfig) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (blinkstickConfig.color == "random") {
|
||||||
|
resolve(color);
|
||||||
|
}
|
||||||
|
if (blinkstickConfig.color == undefined || blinkstickConfig.color.length === 0) {
|
||||||
|
reject("no hex color code given, request will be ignored");
|
||||||
|
}
|
||||||
|
let parsedColor = parseRGBColor(blinkstickConfig.color);
|
||||||
|
if (!parsedColor) {
|
||||||
|
parsedColor = parseHexColor(blinkstickConfig.color);
|
||||||
|
}
|
||||||
|
if (!parsedColor) {
|
||||||
|
return reject("could not parse given color code '" + color + "'");
|
||||||
|
}
|
||||||
|
blinkstickConfig.color = parsedColor;
|
||||||
|
resolve(blinkstickConfig);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse rgb color values
|
||||||
|
function parseRGBColor(colorValues) {
|
||||||
|
let color = {};
|
||||||
|
if (colorValues.indexOf(",") == -1) {
|
||||||
|
if (isRGBValue(colorValues)) {
|
||||||
|
color.type = COLORTYPE_RGB;
|
||||||
|
color.red = parseInt(colorValues);
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const splittedValues = colorValues.split(",");
|
||||||
|
for (let index = 0; index < splittedValues.length; index++) {
|
||||||
|
const value = splittedValues[index];
|
||||||
|
if (!isRGBValue(value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (index == 0) {
|
||||||
|
color.red = parseInt(value);
|
||||||
|
} else if (index === 1) {
|
||||||
|
color.green = parseInt(value);
|
||||||
|
} else if (index === 2) {
|
||||||
|
color.blue = parseInt(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (color && (color.red || color.green || color.blue)) {
|
||||||
|
color.type = COLORTYPE_RGB;
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isRGBValue(colorValues.red) || isRGBValue(colorValues.green) || isRGBValue(colorValues.blue)) {
|
||||||
|
color.type = COLORTYPE_RGB;
|
||||||
|
return colorValues;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check a single rgb value
|
||||||
|
function isRGBValue(value) {
|
||||||
|
return value != undefined && value.length > 0 && !isNaN(value) && value >= 0 && value <= 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse hex color values
|
||||||
|
function parseHexColor(colorValues) {
|
||||||
|
if (colorValues[0] !== '#') {
|
||||||
|
colorValues = '#' + colorValues;
|
||||||
|
}
|
||||||
|
if (colorValues.length === 4) {
|
||||||
|
colorValues.type = COLORTYPE_HEX;
|
||||||
|
return (colorValues[0] + colorValues[1] + colorValues[1] + colorValues[2] + colorValues[2] + colorValues[3] + colorValues[3]);
|
||||||
|
}
|
||||||
|
if (colorValues.length === 7 && hexcolor({ strict: true }).test(colorValues)) {
|
||||||
|
colorValues.type = COLORTYPE_HEX;
|
||||||
|
return colorValues;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass the options to the blinkstick accordingly
|
||||||
|
function illuminate(blinkstickConfig) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
waitForAnimation
|
||||||
|
let color;
|
||||||
|
findBlinkstick().then(function () {
|
||||||
|
if (blinkstickConfig.color.type === COLORTYPE_HEX) {
|
||||||
|
color = blinkstickConfig.color.red;
|
||||||
|
} else {
|
||||||
|
color = blinkstickConfig.color.red + ", " + blinkstickConfig.color.green + ", " + blinkstickConfig.color.blue;
|
||||||
|
}
|
||||||
|
switch (blinkstickConfig.mode) {
|
||||||
|
case "morph":
|
||||||
|
morph(blinkstickConfig, color, resolve, reject);
|
||||||
|
break;
|
||||||
|
case "pulse":
|
||||||
|
startPulsing(blinkstickConfig, color, resolve, reject);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setColor(blinkstickConfig, color, resolve, reject);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// set a static color
|
||||||
|
function setColor(blinkstickConfig, color, resolve, reject) {
|
||||||
|
stopAnimation()
|
||||||
|
.then(function () {
|
||||||
|
setAnimationProperties(blinkstickConfig);
|
||||||
|
led.setColor(blinkstickConfig.color.red, blinkstickConfig.color.green, blinkstickConfig.color.blue, function (err) {
|
||||||
|
clearAnimationProperties();
|
||||||
|
logger.debug("setting color to '" + color + "'...");
|
||||||
|
if (err) {
|
||||||
|
return reject("error setting color '" + color + "' (" + err + ")");
|
||||||
|
}
|
||||||
|
return resolve("set color to '" + color + "'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// morph to a color
|
||||||
|
function morph(blinkstickConfig, color, resolve, reject) {
|
||||||
|
stopAnimation()
|
||||||
|
.then(function () {
|
||||||
|
setAnimationProperties(blinkstickConfig);
|
||||||
|
led.morph(blinkstickConfig.color.red, blinkstickConfig.color.green, blinkstickConfig.color.blue, blinkstickConfig.options, function (err) {
|
||||||
|
clearAnimationProperties();
|
||||||
|
logger.debug("morphing color to '" + color + "'...");
|
||||||
|
if (err) {
|
||||||
|
return reject("error morphing color to '" + color + "' (" + err + ")");
|
||||||
|
}
|
||||||
|
return resolve("morphed color to '" + color + "'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// start pulsing
|
||||||
|
function startPulsing(blinkstickConfig, color, 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))) {
|
||||||
|
clearAnimationProperties();
|
||||||
|
return resolve("finished pulsing color '" + color + "'");
|
||||||
|
}
|
||||||
|
if (animation.id && animation.id != blinkstickConfig.id) {
|
||||||
|
stopAnimation()
|
||||||
|
.then(logger.info)
|
||||||
|
.then(function () {
|
||||||
|
startPulsing(blinkstickConfig, color, resolve, reject)
|
||||||
|
})
|
||||||
|
.catch(logger.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAnimationProperties(blinkstickConfig);
|
||||||
|
led.pulse(blinkstickConfig.color.red, blinkstickConfig.color.green, blinkstickConfig.color.blue, blinkstickConfig.options, function (err) {
|
||||||
|
if (err) {
|
||||||
|
clearAnimationProperties();
|
||||||
|
return reject("error pulsing color '" + color + "' (" + err + ")");
|
||||||
|
}
|
||||||
|
blinkstickConfig.options.pulse.done++;
|
||||||
|
logger.debug("pulsed color '" + color + "' " + blinkstickConfig.options.pulse.done + "/" + blinkstickConfig.options.pulse.max + " times");
|
||||||
|
startPulsing(blinkstickConfig, color, resolve, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// set properties for the current animation
|
||||||
|
function setAnimationProperties(blinkstickConfig) {
|
||||||
|
if (animation.id == blinkstickConfig.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear properties for the current animation
|
||||||
|
function clearAnimationProperties() {
|
||||||
|
led.stop();
|
||||||
|
animation = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop the current animation
|
||||||
|
function stopAnimation() {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (!isAnimationInProgress()) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
animation.state = ANIMATION_STATE_FINISH;
|
||||||
|
waitForAnimation(Date.now(), resolve, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for current animation
|
||||||
|
function waitForAnimation(timestamp, resolve, reject) {
|
||||||
|
if (!isAnimationInProgress()) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
setTimeout(function () {
|
||||||
|
waitForAnimation(timestamp, resolve, reject);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// is currently an animation in progress
|
||||||
|
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,
|
||||||
|
illuminate,
|
||||||
|
isAnimationInProgress,
|
||||||
|
isInfiniteAnimationInProgress,
|
||||||
|
stopAnimation,
|
||||||
|
powerOff
|
||||||
|
};
|
112
libs/logger.js
Normal file
112
libs/logger.js
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
// requirements
|
||||||
|
const config = require("../config.json");
|
||||||
|
// third party requirements
|
||||||
|
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 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) {
|
||||||
|
case LOG_PREFIX_DEBUG:
|
||||||
|
case LOGLEVEL_DEBUG:
|
||||||
|
return LOGLEVEL_DEBUG;
|
||||||
|
case LOG_PREFIX_INFO:
|
||||||
|
case LOGLEVEL_INFO:
|
||||||
|
return LOGLEVEL_INFO;
|
||||||
|
case LOG_PREFIX_WARNING:
|
||||||
|
case LOGLEVEL_WARNING:
|
||||||
|
return LOGLEVEL_WARNING;
|
||||||
|
case LOG_PREFIX_ERROR:
|
||||||
|
case LOGLEVEL_ERROR:
|
||||||
|
return LOGLEVEL_ERROR;
|
||||||
|
default:
|
||||||
|
return LOGLEVEL_INFO;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
// log a http request
|
||||||
|
function logRequest(request) {
|
||||||
|
let message = "[" + request.method + "] url: \"" + request.url + "\"";
|
||||||
|
let counter = 1;
|
||||||
|
for (let param in request.body) {
|
||||||
|
message += ", parameter " + counter + ": \"" + param + "=" + request.body[param] + "\"";
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
debug(message.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix log with "info"
|
||||||
|
function info(message) {
|
||||||
|
if (loglevel > LOGLEVEL_INFO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trace(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix log with "info"
|
||||||
|
function warn(message) {
|
||||||
|
if (loglevel > LOGLEVEL_WARNING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trace(message, "warning");
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix log with "debug"
|
||||||
|
function debug(message) {
|
||||||
|
if (loglevel > LOGLEVEL_DEBUG) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trace(message, "debug");
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix log with "error"
|
||||||
|
function error(message) {
|
||||||
|
if (loglevel > LOGLEVEL_ERROR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trace(message, "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// default logging function
|
||||||
|
function trace(message, prefix) {
|
||||||
|
if (message === undefined || message === null || message.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (prefix === undefined || prefix === null || prefix.length === 0) {
|
||||||
|
prefix = "info";
|
||||||
|
}
|
||||||
|
let print;
|
||||||
|
switch (prefix) {
|
||||||
|
case "error":
|
||||||
|
print = console.error;
|
||||||
|
break;
|
||||||
|
case "debug":
|
||||||
|
print = console.debug;
|
||||||
|
break;
|
||||||
|
case "warning":
|
||||||
|
print = console.warn;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
print = console.log;
|
||||||
|
}
|
||||||
|
message = moment().format(config.server.timestamp) + " | " + prefix + " > " + message;
|
||||||
|
print(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// exports
|
||||||
|
module.exports = {
|
||||||
|
info,
|
||||||
|
warn,
|
||||||
|
debug,
|
||||||
|
error,
|
||||||
|
logRequest
|
||||||
|
};
|
125
libs/server.js
Normal file
125
libs/server.js
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
// requirements
|
||||||
|
const path = require('path');
|
||||||
|
const config = require('../config');
|
||||||
|
const packageJSON = require('../package.json');
|
||||||
|
const logger = require('./logger');
|
||||||
|
const blinkstick = require('./blinkstick');
|
||||||
|
// third party requirements
|
||||||
|
const express = require('express');
|
||||||
|
const favicon = require('serve-favicon')
|
||||||
|
const parser = require('body-parser');
|
||||||
|
|
||||||
|
// setup express, blinkstick and other stuff
|
||||||
|
const app = express();
|
||||||
|
app.use(favicon(path.join(path.dirname(__dirname), "public", "favicon.ico")));
|
||||||
|
app.use(parser.json());
|
||||||
|
app.use(parser.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// get the html content for get requests
|
||||||
|
// TODO: replace with template engine (vue.js)
|
||||||
|
function getHTML() {
|
||||||
|
let welcomeMessage =
|
||||||
|
"<html>" +
|
||||||
|
"<head>" +
|
||||||
|
"<title>" + packageJSON.name + " " + packageJSON.version + "</title>" +
|
||||||
|
"<style>" +
|
||||||
|
"body { background: #2a2a2a; color: #f1f1f1; margin: 1.25% }" +
|
||||||
|
"th, td { border: 1px solid #919191; padding: 6px; text-align: left }" +
|
||||||
|
"</style>" +
|
||||||
|
"</head>" +
|
||||||
|
"<body>" +
|
||||||
|
"<div>" +
|
||||||
|
"<h1>" + packageJSON.name + " " + packageJSON.version + "</h1>" +
|
||||||
|
"</div>" +
|
||||||
|
"<hr>";
|
||||||
|
welcomeMessage +=
|
||||||
|
"<div>" +
|
||||||
|
"<h2>get:</h2>" +
|
||||||
|
"<p>" + config.api.get.description + "</p>" +
|
||||||
|
"</div>";
|
||||||
|
welcomeMessage +=
|
||||||
|
"<div>" +
|
||||||
|
"<h2>post:</h2>" +
|
||||||
|
"<table style=\"width:100%\">" +
|
||||||
|
"<th>argument</th>" +
|
||||||
|
"<th>available values</th>" +
|
||||||
|
"<th>default</th>" +
|
||||||
|
"<th>description</th>";
|
||||||
|
Object.keys(config.api.post).forEach(function (argument) {
|
||||||
|
welcomeMessage +=
|
||||||
|
"<tr>" +
|
||||||
|
"<td>" + argument + "</td>" +
|
||||||
|
"<td>" + config.api.post[argument].available + "</td>" +
|
||||||
|
"<td>" + config.api.post[argument].default + "</td>" +
|
||||||
|
"<td>" + config.api.post[argument].description + "</td>" +
|
||||||
|
"</tr>";
|
||||||
|
});
|
||||||
|
welcomeMessage +=
|
||||||
|
"</table>" +
|
||||||
|
"</div>" +
|
||||||
|
"</body>" +
|
||||||
|
"</html>"
|
||||||
|
return welcomeMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the express http server and handle gets/posts
|
||||||
|
function start() {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
app.listen(config.server.port, config.server.listen)
|
||||||
|
.on("listening", function () {
|
||||||
|
return resolve("server listening on " + config.server.listen + ":" + config.server.port + "...")
|
||||||
|
})
|
||||||
|
.on("error", function (err) {
|
||||||
|
return reject("error starting server (" + err + ")");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequests() {
|
||||||
|
// GET methods
|
||||||
|
app.get('*', function (request, response) {
|
||||||
|
logger.logRequest(request);
|
||||||
|
response.send(getHTML());
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
// POST methods
|
||||||
|
app.post('*', function (request, response) {
|
||||||
|
logger.logRequest(request);
|
||||||
|
if (!blinkstick.isAnimationInProgress() || blinkstick.isInfiniteAnimationInProgress()) {
|
||||||
|
response.end();
|
||||||
|
blinkstick.parseColor(parseRequest(request.body))
|
||||||
|
.then(blinkstick.illuminate)
|
||||||
|
.then(logger.info)
|
||||||
|
.catch(logger.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.sendStatus(503);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the request and return an object with sane defaults
|
||||||
|
function parseRequest(data) {
|
||||||
|
const blinkstickConfig = {
|
||||||
|
"id": Math.random(),
|
||||||
|
"mode": data["mode"] || config.api.post.mode.default,
|
||||||
|
"color": data["color"] || config.api.post.color.default,
|
||||||
|
"options": {
|
||||||
|
"duration": data["duration"] || config.api.post.duration.default,
|
||||||
|
"pulse": {
|
||||||
|
"max": data["pulses"] || 0,
|
||||||
|
"done": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
blinkstickConfig.options.steps = blinkstickConfig.options.duration / 10;
|
||||||
|
if (blinkstickConfig.options.duration < 100) {
|
||||||
|
blinkstickConfig.options.duration = 100;
|
||||||
|
}
|
||||||
|
return blinkstickConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// exports
|
||||||
|
module.exports = {
|
||||||
|
start,
|
||||||
|
handleRequests
|
||||||
|
};
|
23
package.json
Normal file
23
package.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "blinky",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "control a blinkstick via http requests",
|
||||||
|
"main": "blinky.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"blinkstick",
|
||||||
|
"http"
|
||||||
|
],
|
||||||
|
"author": "Daniel Sommer <daniel.sommer@velvettear.de>",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"blinkstick": "^1.2.0",
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"hex-color-regex": "^1.1.0",
|
||||||
|
"moment": "^2.29.1",
|
||||||
|
"serve-favicon": "^2.5.0"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
86
scripts/blinky-feed.sh
Executable file
86
scripts/blinky-feed.sh
Executable file
|
@ -0,0 +1,86 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# author: Daniel Sommer <daniel.sommer@velvettear.de>
|
||||||
|
# license: MIT
|
||||||
|
|
||||||
|
# blinky
|
||||||
|
blinky_url="127.0.0.1:3000"
|
||||||
|
blinky_mode="morph"
|
||||||
|
blinky_duration="2500"
|
||||||
|
|
||||||
|
# temperature
|
||||||
|
max_temp="85000"
|
||||||
|
crit_temp="90000"
|
||||||
|
threshold_upper="66"
|
||||||
|
threshold_lower="33"
|
||||||
|
thermal_zones=(
|
||||||
|
"*"
|
||||||
|
)
|
||||||
|
|
||||||
|
# check for root permissions
|
||||||
|
[[ "$EUID" != 0 ]] && printf "error: permission denied!\n" && exit 1
|
||||||
|
|
||||||
|
# argument to lowercase
|
||||||
|
arg="${1,,}"
|
||||||
|
|
||||||
|
# functions
|
||||||
|
|
||||||
|
# convert a temperature value to a rgb color value
|
||||||
|
function temperatureToRGB() {
|
||||||
|
[[ ! "$1" ]] && printf "error: did not receive a temperature value to convert to a rgb value\n" && exit 1
|
||||||
|
printf "converting temperature to rgb value... "
|
||||||
|
percentage="$(bc <<< "$1 / ( $max_temp / 100 )")"
|
||||||
|
[[ "$percentage" -gt "100" ]] && percentage="100"
|
||||||
|
color_main="$(bc <<< "$percentage * 2.55")"
|
||||||
|
color_supplement="$(bc <<< "255 - $color_main")"
|
||||||
|
color_unused="0"
|
||||||
|
if [[ "$percentage" -ge "$threshold_upper" ]]; then
|
||||||
|
result="$color_main, $color_supplement, $color_unused"
|
||||||
|
elif [[ "$percentage" -ge "33" ]]; then
|
||||||
|
result="$color_supplement, $color_main, $color_unused"
|
||||||
|
else
|
||||||
|
result="$color_unused, $color_supplement, $color_main"
|
||||||
|
fi
|
||||||
|
printf "result: '$result'\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# send http post request to blinky
|
||||||
|
function sendPOST() {
|
||||||
|
[[ ! "$1" ]] && printf "error: did not receive any arguments for post request\n" && exit 1
|
||||||
|
cmd="curl -X POST"
|
||||||
|
for arg in "$@"; do
|
||||||
|
cmd="$cmd -d \"$arg\""
|
||||||
|
done
|
||||||
|
cmd="$cmd \"$blinky_url\""
|
||||||
|
printf "sending post request '$cmd'...\n"
|
||||||
|
eval "$cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
# get (average) temperature from defined thermal zones
|
||||||
|
function getTemperature() {
|
||||||
|
counter="0"
|
||||||
|
printf "getting temperature value from thermal zone(s) "
|
||||||
|
for zone in "${thermal_zones[@]}"; do
|
||||||
|
printf "'"$zone"'... "
|
||||||
|
for value in $(cat "/sys/devices/virtual/thermal/thermal_zone"$zone"/temp"); do
|
||||||
|
[[ ! "$temp" ]] && temp="$value" || temp=$(( $temp + $value ))
|
||||||
|
(( counter++ ))
|
||||||
|
done
|
||||||
|
done
|
||||||
|
result="$(( $temp / $counter ))"
|
||||||
|
printf "result: '$result'\n"
|
||||||
|
[[ "$result" -ge "$crit_temp" ]] && printf "warning: critical temperature reached\n" && blinky_mode="pulse" && blinky_duration="500" && return 0
|
||||||
|
[[ "$result" -ge "$max_temp" ]] && printf "warning: maximum temperature reached\n" && blinky_mode="pulse" && blinky_duration="1500" && return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# main part
|
||||||
|
case "$arg" in
|
||||||
|
-t|--temp|--temperature)
|
||||||
|
getTemperature
|
||||||
|
temperatureToRGB "$result"
|
||||||
|
sendPOST "color=$result" "mode=$blinky_mode" "duration=$blinky_duration"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf "error: no argument given or argument unknown!\n" && exit 1
|
||||||
|
;;
|
||||||
|
esac
|
Loading…
Reference in a new issue