Compare commits

..

10 commits

12 changed files with 2239 additions and 53 deletions

7
.gitignore vendored
View file

@ -1,6 +1,3 @@
.vscode/ config.json
.idea/
node_modules/ node_modules/
package-lock.json npm-debug.log
yarn.lock
npm-debug.log

28
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,28 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"runtimeVersion": "8",
"request": "launch",
"name": "blinky",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/blinky.js"
},
{
"type": "node",
"runtimeVersion": "8",
"request": "launch",
"name": "blinky cli",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/blinky.js",
"args": [
"--get-blinksticks"
]
}
]
}

View file

@ -1,4 +1,5 @@
MIT License Copyright (c) <year> <copyright holders> # MIT License
**Copyright (c) 2022 Daniel Sommer \<daniel.sommer@velvettear.de\>**
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -11,9 +12,9 @@ The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the paragraph) shall be included in all copies or substantial portions of the
Software. Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR **THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 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 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. OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**

View file

@ -40,8 +40,6 @@ run:
`echo "KERNEL==\"hidraw*\", SUBSYSTEM==\"hidraw\", ATTRS{idVendor}==\"20a0\", ATTRS{idProduct}==\"41e5\", MODE=\"0666\"" | sudo tee /etc/udev/rules.d/85-blinkstick-hid.rules` `echo "KERNEL==\"hidraw*\", SUBSYSTEM==\"hidraw\", ATTRS{idVendor}==\"20a0\", ATTRS{idProduct}==\"41e5\", MODE=\"0666\"" | sudo tee /etc/udev/rules.d/85-blinkstick-hid.rules`
and reboot your system and reboot your system
## systemd ## systemd
**for security reasons it is highly recommended to not run blinky with root permissions!** **for security reasons it is highly recommended to not run blinky with root permissions!**

View file

@ -26,7 +26,7 @@ async function main() {
if (await cli.handleArguments()) { if (await cli.handleArguments()) {
util.exit(); util.exit();
} }
await controller.findBlinkstick(); controller.mapBlinkSticks();
logger.info(await server.start()); logger.info(await server.start());
server.handleRequests(); server.handleRequests();
} }

View file

@ -5,6 +5,16 @@
}, },
"blinkstick": { "blinkstick": {
"cache": true, "cache": true,
"map": [
{
"id": "square",
"serial": "BS006537-3.0"
},
{
"id": "strip",
"serial": "BS042165-3.0"
}
],
"serials": [ "serials": [
"BS006537-3.0" "BS006537-3.0"
] ]
@ -15,7 +25,10 @@
}, },
"api": { "api": {
"get": { "get": {
"description": "show this page" "description": "show this page",
"endpoints": [
"/color"
]
}, },
"post": { "post": {
"endpoints": [ "endpoints": [
@ -25,6 +38,11 @@
"/pulse", "/pulse",
"/poweroff" "/poweroff"
], ],
"blinkstick": {
"available": "string values",
"default": "undefined",
"description": "specifies the blinkstick by the id given in the config.json file or by serial"
},
"color": { "color": {
"available": "random, hex color codes (#ffffff), rgb color codes (255, 255, 255)", "available": "random, hex color codes (#ffffff), rgb color codes (255, 255, 255)",
"default": "random", "default": "random",

View file

@ -11,7 +11,7 @@ async function handleArguments() {
continue; continue;
case constants.ARG_LIST: case constants.ARG_LIST:
case constants.ARG_LIST_SHORT: case constants.ARG_LIST_SHORT:
logger.info('blinksticks: ' + JSON.stringify(await controller.findBlinkstick(constants.ALL, true))); logger.info('blinksticks: ' + JSON.stringify(await controller.getBlinkstick(constants.ALL, false)));
handled++; handled++;
break; break;
} }

View file

@ -5,36 +5,81 @@ const blinkstick = require('blinkstick');
const LEDAnimations = new Map(); const LEDAnimations = new Map();
let blinksticks; let blinksticks = new Map();
// find connected blinkstick(s) // get connected blinkstick by id (or return the first one found)
async function findBlinkstick(index, ignoreFilter) { async function getBlinkstick(id, filter) {
if (!global.config.blinkstick?.cache || blinksticks === undefined) { if (!global.config.blinkstick?.cache || blinksticks.size === 0) {
blinksticks = blinkstick.findAll(); mapBlinkSticks(filter);
if (!ignoreFilter && global.config.blinkstick?.serials?.length > 0) { }
blinksticks = blinksticks.filter((blinkstick) => { if (id === undefined) {
return global.config.blinkstick.serials.includes(blinkstick.serial); return blinksticks.values().next().value;
}); }
if (blinksticks.length === 0) { if (id === constants.ALL) {
throw new Error('could not find any blinkstick matching the defined serial(s)'); return Array.from(blinksticks.values());
}
if (!blinksticks.has(id)) {
throw new Error('could not find any blinkstick matching the given id \'' + id + '\'');
}
return blinksticks.get(id);
}
// map found blinksticks
function mapBlinkSticks(filter) {
const foundBlinksticks = blinkstick.findAll();
if (filter === undefined) {
filter = global.config?.blinkstick?.map?.length > 0;
}
blinksticks = new Map();
filter = filter && global.config.blinkstick?.map?.length > 0;
for (let blinkstickIndex = 0; blinkstickIndex < foundBlinksticks.length; blinkstickIndex++) {
const serial = foundBlinksticks[blinkstickIndex].serial;
if (!filter) {
blinksticks.set(serial, foundBlinksticks[blinkstickIndex]);
continue;
}
for (filterIndex = 0; filterIndex < global.config.blinkstick.map.length; filterIndex++) {
let tmp = global.config.blinkstick.map[filterIndex];
if (tmp.serial !== serial) {
continue;
} }
blinksticks.set(tmp.id || serial, foundBlinksticks[blinkstickIndex]);
break;
} }
} }
if (blinksticks.length === 0) { if (blinksticks.size === 0) {
throw new Error('could not find any blinkstick, make sure at least one blinkstick is connected'); if (filter) {
throw new Error('could not find any blinkstick matching the given serial(s)');
} else {
throw new Error('could not find any blinkstick, make sure at least one blinkstick is connected');
}
} }
if (index === undefined) { }
index = 0;
} else if (index !== constants.ALL) { // reset a blinkstick
index = parseInt(index) || 0; async function resetBlinkstick(id) {
if (blinksticks === undefined || blinksticks.length === 0) {
return;
} }
if (index > blinksticks.length - 1) { let tmp;
throw new Error('there is no blinkstick for index \'' + index + '\''); if (id === constants.ALL) {
tmp = await getBlinkstick(id);
for (let index = 0; index < tmp.length; index++) {
if (tmp[index] === undefined) {
continue;
}
tmp[index].close();
}
blinksticks.clear();
} else {
tmp = await getBlinkstick(id);
if (tmp === undefined) {
return;
}
tmp.close();
blinksticks.delete(id);
} }
if (index === constants.ALL) { mapBlinkSticks();
return blinksticks;
}
return blinksticks[index];
} }
// simple animation (set the color / morph to color) // simple animation (set the color / morph to color)
@ -79,6 +124,16 @@ async function complex(config) {
// power the blinkstick (or just a specific led) off // power the blinkstick (or just a specific led) off
async function powerOff(config) { async function powerOff(config) {
config.timestamp = new Date().getTime(); config.timestamp = new Date().getTime();
if (config.blinkstick === constants.ALL) {
const promises = [];
const blinkstickNames = Array.from(blinksticks.keys());
for (let index = 0; index < blinkstickNames.length; index++) {
const tmp = JSON.parse(JSON.stringify(config));
tmp.blinkstick = blinkstickNames[index];
promises.push(powerOff(tmp));
}
return await Promise.allSettled(promises);
}
let indexes = getIndices(config); let indexes = getIndices(config);
if (config.options.index === constants.ALL) { if (config.options.index === constants.ALL) {
LEDAnimations.set(constants.ALL, { stop: new Date().getTime() }); LEDAnimations.set(constants.ALL, { stop: new Date().getTime() });
@ -89,7 +144,7 @@ async function powerOff(config) {
logger.info('led \'' + indexes[index] + '\' powered off'); logger.info('led \'' + indexes[index] + '\' powered off');
} }
if (config.options.index === constants.ALL) { if (config.options.index === constants.ALL) {
const blinkstick = await findBlinkstick(); const blinkstick = await getBlinkstick();
blinkstick.turnOff(); blinkstick.turnOff();
LEDAnimations.clear(); LEDAnimations.clear();
logger.info('blinkstick powered off'); logger.info('blinkstick powered off');
@ -100,7 +155,7 @@ async function powerOff(config) {
// animations // animations
async function singleAnimation(config, index) { async function singleAnimation(config, index) {
config.options.index = index; config.options.index = index;
const blinkstick = await findBlinkstick(); const blinkstick = await getBlinkstick(config.blinkstick);
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
logger.debug('changing color of led \'' + config.options.index + '\' to \'' + config.color + '\' (mode: ' + config.mode + ' | options: ' + JSON.stringify(config.options) + ')...'); logger.debug('changing color of led \'' + config.options.index + '\' to \'' + config.color + '\' (mode: ' + config.mode + ' | options: ' + JSON.stringify(config.options) + ')...');
setLEDAnimated(config.options.index); setLEDAnimated(config.options.index);
@ -141,12 +196,47 @@ function getIndices(blinkstickConfig) {
return [blinkstickConfig.options.index]; return [blinkstickConfig.options.index];
} }
async function getColor(index) { async function getColors(blinkstick, index) {
if (index === undefined) { let blinksticksToCheck = [];
index = 0; if (blinkstick === undefined) {
blinksticksToCheck = Array.from(blinksticks.keys());
} else {
blinksticksToCheck.push(blinkstick);
}
let indices = [0, 1, 2, 3, 4, 5, 6, 7];
if (index !== undefined && index !== constants.AL && !isNaN(index)) {
index = [index];
}
let results = [];
for (let blinkstickIndex = 0; blinkstickIndex < blinksticksToCheck.length; blinkstickIndex++) {
const tmpBlinkstick = blinksticksToCheck[blinkstickIndex];
let result = {
blinkstick: tmpBlinkstick,
leds: []
};
for (let ledIndex = 0; ledIndex < indices.length; ledIndex++) {
result.leds.push({
index: ledIndex,
color: await getColor({
blinkstick: tmpBlinkstick,
options: {
index: ledIndex
}
})
});
}
results.push(result);
}
return results;
}
async function getColor(config) {
let index = 0;
if (!isNaN(config.options.index)) {
index = parseInt(config.options.index);
} }
logger.debug('getting color for led with index \'' + index + '\''); logger.debug('getting color for led with index \'' + index + '\'');
const blinkstick = await findBlinkstick(); const blinkstick = await getBlinkstick(config.blinkstick);
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
blinkstick.getColorString(index, (err, color) => { blinkstick.getColorString(index, (err, color) => {
if (err) { if (err) {
@ -162,7 +252,7 @@ async function setColorIfRandom(config) {
if (config.options.index !== constants.ALL || config.color !== constants.RANDOM) { if (config.options.index !== constants.ALL || config.color !== constants.RANDOM) {
return; return;
} }
config.color = await getColor(0); config.color = await getColor(config);
} }
async function stopLEDsAccordingly(config) { async function stopLEDsAccordingly(config) {
@ -278,9 +368,11 @@ function isInfiniteAnimation(config) {
// exports // exports
module.exports = { module.exports = {
findBlinkstick, getBlinkstick,
mapBlinkSticks,
simple, simple,
complex, complex,
powerOff, powerOff,
isInfiniteAnimation isInfiniteAnimation,
getColors
} }

View file

@ -20,9 +20,15 @@ function get() {
'<div>' + '<div>' +
'<h2>get:</h2>' + '<h2>get:</h2>' +
'<p>' + config.api.get.description + '</p>' + '<p>' + config.api.get.description + '</p>' +
'</div>'; '<h3>endpoints: </>';
for (let index = 0; index < config.api.get.endpoints.length; index++) {
if (index > 0) {
html += ', ';
}
html += config.api.get.endpoints[index];
}
html += html +=
'</div>' +
'<div>' + '<div>' +
'<h2>post:</h2>' + '<h2>post:</h2>' +
'<h3>endpoints: </>'; '<h3>endpoints: </>';

View file

@ -8,6 +8,7 @@ function parseRequest(requestBody, mode) {
} }
let config = { let config = {
'id': Math.random(), 'id': Math.random(),
'blinkstick': parseBlinkstick(requestBody.blinkstick),
'mode': mode, 'mode': mode,
'options': { 'options': {
'index': parseIndex(requestBody.index), 'index': parseIndex(requestBody.index),
@ -39,6 +40,14 @@ function parseRequest(requestBody, mode) {
return config; return config;
} }
// parse the blinkstick
function parseBlinkstick(blinkstick) {
if (blinkstick === undefined) {
return undefined;
}
return blinkstick;
}
// parse the index // parse the index
function parseIndex(index) { function parseIndex(index) {
if (index === undefined) { if (index === undefined) {
@ -155,5 +164,7 @@ function parseHexColor(value) {
// exports // exports
module.exports = { module.exports = {
parseRequest parseRequest,
parseBlinkstick,
parseIndex
}; };

View file

@ -12,7 +12,7 @@ const bodyparser = require('body-parser');
const app = express(); const app = express();
app.use(favicon(path.join(path.dirname(__dirname), 'public', 'favicon.ico'))); app.use(favicon(path.join(path.dirname(__dirname), 'public', 'favicon.ico')));
app.use(bodyparser.json()); app.use(bodyparser.json());
app.use(bodyparser.urlencoded({extended: true})); app.use(bodyparser.urlencoded({ extended: true }));
const html = require('./index.js').get(); const html = require('./index.js').get();
@ -30,6 +30,11 @@ async function start() {
} }
function handleRequests() { function handleRequests() {
// GET '/color'
app.get('/color', (request, response) => {
logger.http(request);
handleGETColor(request.body.blinkstick, request.body.index, response);
});
// GET html page // GET html page
app.get('*', (request, response) => { app.get('*', (request, response) => {
logger.http(request); logger.http(request);
@ -63,19 +68,36 @@ function handleRequests() {
}); });
} }
async function handleGETColor(blinkstick, index, response) {
try {
let result = await controller.getColors(blinkstick, index);
response.end(JSON.stringify(result));
} catch (err) {
if (response === undefined) {
return;
}
logger.error(err);
response.status(500);
response.end(JSON.stringify({ status: 'error', error: err.message }));
}
}
async function handleSimpleAnimation(config, response) { async function handleSimpleAnimation(config, response) {
try { try {
response.end(JSON.stringify(await controller.simple(config))); response.end(JSON.stringify(await controller.simple(config)));
} catch (err) { } catch (err) {
if (response === undefined) {
return;
}
logger.error(err); logger.error(err);
response.status(500); response.status(500);
response.end(JSON.stringify({status: 'error', error: err.message})); response.end(JSON.stringify({ status: 'error', error: err.message }));
} }
} }
async function handleComplexAnimation(config, response) { async function handleComplexAnimation(config, response) {
if (controller.isInfiniteAnimation(config)) { if (controller.isInfiniteAnimation(config)) {
response.end(JSON.stringify({status: 'ok', time: 'infinite'})); response.end(JSON.stringify({ status: 'ok', time: 'infinite' }));
response = undefined; response = undefined;
} }
try { try {
@ -90,16 +112,19 @@ async function handleComplexAnimation(config, response) {
} }
logger.error(err); logger.error(err);
response.status(500); response.status(500);
response.end(JSON.stringify({status: 'error', error: err.message})); response.end(JSON.stringify({ status: 'error', error: err.message }));
} }
} }
async function handlePowerOff(config, response) { async function handlePowerOff(config, response) {
try { try {
if (config.blinkstick === undefined) {
config.blinkstick = constants.ALL;
}
response.end(JSON.stringify(await controller.powerOff(config))); response.end(JSON.stringify(await controller.powerOff(config)));
} catch (err) { } catch (err) {
response.status(500); response.status(500);
response.end(JSON.stringify({status: 'error', error: err.message})); response.end(JSON.stringify({ status: 'error', error: err.message }));
} }
} }

2010
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff