const logger = require('./logger'); const blinkstick = require('blinkstick'); const ALL = 'all'; const MODE_SET = 'set'; const MODE_MORPH = 'morph'; const MODE_BLINK = 'blink'; const MODE_PULSE = 'pulse'; const MODE_POWEROFF = 'poweroff'; const LEDS_ANIMATED = []; const LEDS_STOP = []; let blinksticks; // find connected blinkstick(s) async function findBlinkstick(index, ignoreFilter) { if (!global.config.blinkstick?.cache || blinksticks === undefined) { blinksticks = blinkstick.findAll(); if (global.config.blinkstick?.serials?.length > 0) { blinksticks = blinksticks.filter((blinkstick) => { return global.config.blinkstick.serials.includes(blinkstick.serial); }); if (blinksticks.length === 0) { throw new Error('could not find any blinkstick matching the defined serial(s)'); } } } if (blinksticks.length === 0) { throw new Error('could not find any blinkstick, make sure at least one blinkstick is connected'); } if (index === undefined) { index = 0; } else if (index !== ALL) { index = parseInt(index); } if (isNaN(index)) { index = 0; } if (index > blinksticks.length - 1) { throw new Error('there is no blinkstick for index \'' + index + '\''); } if (index === ALL) { return blinksticks; } return blinksticks[index]; } // simple animation (set the color / morph to color) async function simple(config) { await stopLedsAccordingly(config); config.timestamp = new Date().getTime(); let indices = getIndices(config); for (let index = 0; index < indices.length; index++) { setLedAnimated(indices[index]); await singleAnimation(JSON.parse(JSON.stringify(config)), indices[index]); clearLedState(indices[index]); } return { status: 'ok', color: config.color, indices: indices, time: (new Date().getTime() - config.timestamp) + 'ms' }; } // complex animation (pulse / blink) async function complex(config) { if (config.timestamp === undefined) { await stopLedsAccordingly(config); config.timestamp = new Date().getTime(); } let indices = getIndices(config); for (let index = 0; index < indices.length; index++) { if (shouldLedFinish(config)) { clearLedState(indices[index]); return { status: 'ok', time: (new Date().getTime() - config.timestamp) + 'ms' }; } setLedAnimated(indices[index]); await singleAnimation(JSON.parse(JSON.stringify(config)), indices[index]); if (config.options.index === ALL) { clearLedState(indices[index]); } config.repetitions.done++; } return complex(config); } // power the blinkstick (or just a specific led) off async function powerOff(config) { config.timestamp = new Date().getTime(); let indices = getIndices(config); await forceStop(config.options.index); for (let index = 0; index < indices.length; index++) { await singleAnimation(JSON.parse(JSON.stringify(config)), indices[index]); logger.info('led \'' + indices[index] + '\' powered off'); } if (config.options.index === ALL) { const blinkstick = await findBlinkstick(); blinkstick.stop(); blinkstick.turnOff(); logger.info('blinkstick powered off'); } return { status: 'ok', indices: indices, time: (new Date().getTime() - config.timestamp) + 'ms' }; } // animations async function singleAnimation(config, index) { config.options.index = index; const blinkstick = await findBlinkstick(); return await new Promise((resolve, reject) => { logger.debug('changing color of led \'' + config.options.index + '\' to \'' + config.color + '\' (mode: ' + config.mode + ')...'); switch (config.mode) { case MODE_MORPH: blinkstick.morph(config.color, config.options, callback); break; case MODE_BLINK: blinkstick.blink(config.color, config.options, callback); break; case MODE_PULSE: blinkstick.pulse(config.color, config.options, callback); break; default: blinkstick.setColor(config.color, config.options, callback); break; } function callback(err) { if (err) { reject(new Error('changing color of led \'' + config.options.index + '\' to \'' + config.color + '\' encountered an error > ' + err)); } logger.info('changed color of led \'' + config.options.index + '\' to \'' + config.color + '\' (mode: ' + config.mode + ')'); resolve(); } }); } async function stopLedsAccordingly(config) { if (config.options.index === ALL) { logger.debug('stopping all leds...'); return await powerOff({ id: Math.random(), mode: MODE_POWEROFF, color: '#000000', options: { index: ALL } }); } return stopLedAnimation(config.options.index); } async function forceStop(index) { if (index === ALL) { LEDS_STOP.push(ALL); return await new Promise((resolve, reject) => { waitForAllAnimationsEnd(() => { resolve(); }); }); } setLedStopping(index); return await new Promise((resolve, reject) => { waitForAnimationEnd(index, () => { resolve(); }); }); } function shouldLedFinish(config) { if (isLedStopping(config.options.index)) { logger.debug('led \'' + config.options.index + '\' is set to \'stop\' and should finish now'); return true; } if (config.mode === MODE_BLINK || config.mode === MODE_PULSE) { return config.repetitions.max !== 0 && config.repetitions.done >= config.repetitions.max; } return false; } // led / index helper functions function getIndices(blinkstickConfig) { if (blinkstickConfig.options.index === ALL) { return [0, 1, 2, 3, 4, 5, 6, 7]; } return [blinkstickConfig.options.index]; } function setLedAnimated(index) { if (isLedAnimated(index)) { return; } LEDS_ANIMATED.push(index); logger.debug('led \'' + index + '\ set to \'animated\''); } function setLedStopping(index) { if (!isLedAnimated(index) || isLedStopping(index)) { return; } LEDS_STOP.push(index); logger.debug('led \'' + index + '\ set to \'stop\''); } function clearLedState(index) { if (index === ALL) { LEDS_ANIMATED.length = 0; logger.debug('cleared animation state of all leds'); return; } LEDS_ANIMATED.splice(LEDS_ANIMATED.indexOf(index), 1); logger.debug('cleared animation state of led \'' + index + '\''); } function isLedAnimated(index) { return LEDS_ANIMATED.includes(index); } function isLedStopping(index) { if (LEDS_STOP.includes(ALL)) { return true; } return LEDS_STOP.includes(index); } function stopLedAnimation(index) { setLedStopping(index); return new Promise((resolve, reject) => { waitForAnimationEnd(index, () => { resolve(); }); }); } function waitForAnimationEnd(index, callback) { logger.debug('waiting for animated led \'' + index + '\' to end...'); if (!isLedAnimated(index)) { logger.debug('animation of led \'' + index + '\' should have ended now'); LEDS_STOP.splice(LEDS_STOP.indexOf(index), 1); return callback(); } setTimeout(() => { waitForAnimationEnd(index, callback); }, 100); } function waitForAllAnimationsEnd(callback) { logger.debug('waiting for all animations to end...'); if (LEDS_ANIMATED.length === 0) { logger.debug('all animations should have ended now'); LEDS_STOP.length = 0; return callback(); } setTimeout(() => { waitForAllAnimationsEnd(callback); }, 100); } function isInfiniteAnimation(config) { if (config.mode !== MODE_BLINK && config.mode !== MODE_PULSE) { return false; } return config.repetitions.max === 0; } // exports module.exports = { findBlinkstick, simple, complex, powerOff, isInfiniteAnimation, ALL, MODE_SET, MODE_MORPH, MODE_BLINK, MODE_PULSE, MODE_POWEROFF };