const logger = require('./logger.js'); const util = require('./util.js'); const blinkstick = require('blinkstick'); const RANDOM = require('./parser').COLOR_RANDOM; const ALL = 'all'; const MODE_SET = 'set'; const MODE_MORPH = 'morph'; const MODE_BLINK = 'blink'; const MODE_PULSE = 'pulse'; const MODE_POWEROFF = 'poweroff'; const LEDAnimations = new Map(); 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++) { const tmpConfig = JSON.parse(JSON.stringify(config)); await singleAnimation(tmpConfig, indices[index]); if (index === 0) { config = setColorIfRandom(config); } } 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(config.options.index); return { status: 'ok', time: (new Date().getTime() - config.timestamp) + 'ms' }; } const tmpConfig = JSON.parse(JSON.stringify(config)); await singleAnimation(tmpConfig, indices[index]); if (index === 0) { config = setColorIfRandom(config); } config.repetitions.done++; } return await complex(config); } // power the blinkstick (or just a specific led) off async function powerOff(config) { config.timestamp = new Date().getTime(); let indices = getIndices(config); if (config.options.index === ALL) { LEDAnimations.set(ALL, { stop: new Date().getTime() }); } for (let index = 0; index < indices.length; index++) { await stopLEDAnimation(indices[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.turnOff(); LEDAnimations.clear(); 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 + ' | options: ' + JSON.stringify(config.options) + ')...'); setLEDAnimated(config.options.index); blinkstick.animationsEnabled = true; 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 (config.mode !== MODE_BLINK && config.mode !== MODE_PULSE) { clearLedState(config.options.index); } 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(); } }); } // 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]; } async function getColor(index) { if (index === undefined) { index = 0; } logger.debug('getting color for led with index \'' + index + '\''); const blinkstick = await findBlinkstick(); return await new Promise((resolve, reject) => { blinkstick.getColorString(index, (color) => { logger.debug('led with index \'' + index + '\' is set to color \'' + color + '\''); resolve(color); }); }); } async function setColorIfRandom(config) { if (config.options.index === ALL && config.color === RANDOM) { config.color = await getColor(0); } return config; } async function stopLEDsAccordingly(config) { if (LEDAnimations.size === 0) { return; } 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); } function shouldLEDFinish(config) { if (LEDAnimations.has(ALL) || (isLEDAnimated(config.options.index) && 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; } function setLEDAnimated(index) { if (isLEDAnimated(index)) { return; } LEDAnimations.set(index, { start: new Date().getTime() }); logger.debug('led \'' + index + '\ set to \'animated\''); } function setLEDStopping(index) { if (!isLEDAnimated(index) || isLEDStopping(index)) { return; } if (LEDAnimations.has(index)) { LEDAnimations.get(index).stop = new Date().getTime(); } logger.debug('led \'' + index + '\ set to \'stop\''); } function clearLedState(index) { if (index === ALL) { LEDAnimations.clear(); logger.debug('cleared animation state of all leds'); return; } LEDAnimations.delete(index); logger.debug('cleared animation state of led \'' + index + '\''); } function isLEDAnimated(index) { return LEDAnimations.has(index); } function isLEDStopping(index) { if (LEDAnimations.has(ALL) && LEDAnimations.get(ALL).stop !== undefined) { return true; } if (LEDAnimations.has(index) && LEDAnimations.get(index).stop !== undefined) { return true; } return false; } async function stopLEDAnimation(index) { if (index === ALL) { for (const [key, value] of LEDAnimations) { setLEDStopping(key); } await waitForAllAnimationsEnd(); return; } setLEDStopping(index); await waitForAnimationEnd(index); } async function waitForAnimationEnd(index, timestamp) { logger.debug('waiting for animated led \'' + index + '\' to end...'); if (!isLEDAnimated(index)) { logger.debug('animation of led \'' + index + '\' should have ended now'); return; } if (timestamp === undefined) { timestamp = new Date().getTime(); } await util.sleep(100); return await waitForAnimationEnd(index, timestamp); } async function waitForAllAnimationsEnd(callback, timestamp) { if (LEDAnimations.size === 0) { return; } logger.debug('waiting for all animations to end...'); for (const [key, value] of LEDAnimations) { await waitForAnimationEnd(key); } } 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 };