// requirements const logger = require('./logger'); const config = require('../config.json'); const util = require('util'); // third party requirements const blinkstick = require('blinkstick'); const hexcolor = require('hex-color-regex'); // constants // const LEDS = [0, 1, 2, 3, 4, 5, 6, 7]; const LEDS_ALL = 'index_all'; const RANDOM = 'random'; const ANIMATION_STATE_INPROGRESS = 1; const ANIMATION_STATE_FINISH = 0; // variables let led; const LEDS = new Map(); function findBlinkstick() { return new Promise((resolve, reject) => { led = blinkstick.findFirst(); if (led !== undefined && led !== null) { return resolve(led); } return reject('could not find any blinkstick'); }); } // light it up async function illuminate(blinkstickConfig) { led = await findBlinkstick(); let indices = getIndices(blinkstickConfig); for (let index = 0; index < indices.length; index++) { try { await setLedState(indices[index], ANIMATION_STATE_INPROGRESS); await singleAnimation(JSON.parse(JSON.stringify(blinkstickConfig)), indices[index]); } catch (err) { logger.error(err); } } } // turn the blinkstick or specified led off async function powerOff(index) { led = await findBlinkstick(); // check for NaN if (index !== index) { index = LEDS_ALL; } let config = {color: '#000000', mode: 'poweroff', options: {index: index}}; let indices = getIndices(config); for (let index = 0; index < indices.length; index++) { try { await singleAnimation(JSON.parse(JSON.stringify(config)), indices[index]); } catch (err) { logger.error(err); } } if (index !== LEDS_ALL) { logger.info('led \'' + index + '\' powered off'); return; } led.stop(); led.turnOff(); logger.info('blinkstick powered off'); } // animations function singleAnimation(config, index) { return new Promise((resolve, reject) => { config.options.index = index; logger.debug('changing color of led \'' + config.options.index + '\' to \'' + config.color + '\' (mode: ' + config.mode + ')...'); switch (config.mode) { case 'morph': led.morph(config.color, config.options, callback); break; case 'pulse': led.pulse(config.color, config.options, callback); break; default: led.setColor(config.color, config.options, callback); } function callback(err) { clearLedState(config.options.index); if (err) { reject('changing color of led \'' + config.options.index + '\' to \'' + config.color + '\' encountered an error > ' + err); } logger.debug('changed color of led \'' + config.options.index + '\' to \'' + config.color + '\' (mode: ' + config.mode + ')'); resolve(); } }); } // start pulsing function startPulsing(blinkstickConfig) { return new Promise((resolve, reject) => { let animation = getAnimation(blinkstickConfig); if (animation?.state === ANIMATION_STATE_FINISH || (blinkstickConfig.options.pulse.max > 0 && animation.done === blinkstickConfig.options.pulse.max)) { clearAnimationProperties(blinkstickConfig); return resolve('finished pulsing \'' + blinkstickConfig.color + '\''); } let currentAnimation = getAnimationByIndex(blinkstickConfig?.options?.index); if (currentAnimation !== undefined && currentAnimation.id !== blinkstickConfig.id) { stopAnimation(currentAnimation) .then(logger.info) .then(() => { startPulsing(blinkstickConfig, resolve, reject) }) .catch(logger.error); return; } animation = setAnimationProperties(blinkstickConfig); led.pulse(blinkstickConfig.color, blinkstickConfig.options, (err) => { if (err) { clearAnimationProperties(blinkstickConfig); return reject('error pulsing ' + msg + ' > ' + err); } animation.done++; logger.debug('pulsed ' + msg + ' ' + animation.done + '/' + blinkstickConfig.options.pulse.max + ' times'); startPulsing(blinkstickConfig) .catch(reject); }); }); } // led / index helper functions function getIndices(blinkstickConfig) { if (blinkstickConfig.options.index === LEDS_ALL) { return [0, 1, 2, 3, 4, 5, 6, 7]; } return [blinkstickConfig.options.index]; } async function setLedState(index, state) { await stopLedAnimation(index); LEDS.set(index, state); } function clearLedState(index) { LEDS.delete(index); } function isLedAnimated(index) { return (LEDS.get(index) || LEDS.get(LEDS_ALL)) !== undefined; } function stopLedAnimation(index) { return new Promise((resolve, reject) => { waitForAnimationEnd(index, () => { resolve(); }); }); } function waitForAnimationEnd(index, callback) { if (!isLedAnimated(index)) { return callback(); } setTimeout(() => { waitForAnimationEnd(index, callback); }, 100); } // color / parser functions function parseColor(value) { if (!value || value === RANDOM) { return RANDOM; } let parsedColor = parseRGBColor(value); if (!parsedColor) { parsedColor = parseHexColor(value); } return parsedColor || function () { logger.warn('could not parse color value \'' + value + '\', defaulting to \'' + config.api.post.color.default + '\''); return config.api.post.color.default; }(); } function parseRGBColor(value) { if (value.indexOf(',') === -1 && isRGBValue(value)) { return convertRGBToHex(parseInt(value) || 0, 0, 0); } else { const splittedValues = value.split(','); let color = {}; for (let index = 0; index < splittedValues.length; index++) { const value = splittedValues[index]; if (index === 0) { color.red = parseInt(value) || 0; } else if (index === 1) { color.green = parseInt(value) || 0; } else if (index === 2) { color.blue = parseInt(value) || 0; } } if (isRGBValue(color.red) && isRGBValue(color.green) && isRGBValue(color.blue)) { return convertRGBToHex(color.red, color.green, color.blue); } } } function isRGBValue(value) { return value !== undefined && !isNaN(value) && value >= 0 && value <= 255; } function convertRGBToHex(red, green, blue) { return '#' + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1); } function parseHexColor(value) { if (value[0] !== '#') { value = '#' + value; } if (value.length === 4) { value = (value[0] + value[1] + value[1] + value[2] + value[2] + value[3] + value[3]); } if (hexcolor({strict: true}).test(value)) { return value; } } // exports module.exports = { parseColor, illuminate, powerOff, LEDS_ALL };