const logger = require('./logger.js'); const util = require('./util.js'); const constants = require('./constants.js'); const blinkstick = require('blinkstick'); const LEDAnimations = new Map(); let blinksticks = new Map(); // get connected blinkstick by id (or return the first one found) async function getBlinkstick(id, filter) { if (!global.config.blinkstick?.cache || blinksticks.size === 0) { mapBlinkSticks(filter); } if (id === undefined) { return blinksticks.values().next().value; } if (id === constants.ALL) { 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.size === 0) { 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'); } } } // reset a blinkstick async function resetBlinkstick(id) { if (blinksticks === undefined || blinksticks.length === 0) { return; } let tmp; 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); } mapBlinkSticks(); } // simple animation (set the color / morph to color) async function simple(config) { await stopLEDsAccordingly(config); config.timestamp = new Date().getTime(); let indexes = getIndices(config); for (let index = 0; index < indexes.length; index++) { const tmpConfig = JSON.parse(JSON.stringify(config)); await singleAnimation(tmpConfig, indexes[index]); if (index === 0) { await setColorIfRandom(config); } } return { status: 'ok', color: config.color, indexes: indexes, 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 indexes = getIndices(config); for (let index = 0; index < indexes.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, indexes[index]); 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(); 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); if (config.options.index === constants.ALL) { LEDAnimations.set(constants.ALL, { stop: new Date().getTime() }); } for (let index = 0; index < indexes.length; index++) { await stopLEDAnimation(indexes[index]); await singleAnimation(JSON.parse(JSON.stringify(config)), indexes[index]); logger.info('led \'' + indexes[index] + '\' powered off'); } if (config.options.index === constants.ALL) { const blinkstick = await getBlinkstick(); blinkstick.turnOff(); LEDAnimations.clear(); logger.info('blinkstick powered off'); } return { status: 'ok', indexes: indexes, time: (new Date().getTime() - config.timestamp) + 'ms' }; } // animations async function singleAnimation(config, index) { config.options.index = index; const blinkstick = await getBlinkstick(config.blinkstick); 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 constants.MODE_MORPH: blinkstick.morph(config.color, config.options, callback); break; case constants.MODE_BLINK: blinkstick.blink(config.color, config.options, callback); break; case constants.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 !== constants.MODE_BLINK && config.mode !== constants.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 === constants.ALL) { return [0, 1, 2, 3, 4, 5, 6, 7]; } return [blinkstickConfig.options.index]; } async function getColors(blinkstick, index) { let blinksticksToCheck = []; 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 + '\''); const blinkstick = await getBlinkstick(config.blinkstick); return await new Promise((resolve, reject) => { blinkstick.getColorString(index, (err, color) => { if (err) { reject(err); } logger.debug('led with index \'' + index + '\' is set to color \'' + color + '\''); resolve(color); }); }); } async function setColorIfRandom(config) { if (config.options.index !== constants.ALL || config.color !== constants.RANDOM) { return; } config.color = await getColor(config); } async function stopLEDsAccordingly(config) { if (LEDAnimations.size === 0 && config.mode !== constants.MODE_BLINK && config.mode !== constants.MODE_PULSE) { return; } if (config.options.index === constants.ALL) { logger.debug('stopping all leds...'); return await powerOff({ id: Math.random(), mode: constants.MODE_POWEROFF, color: '#000000', options: { index: constants.ALL } }); } return stopLEDAnimation(config.options.index); } function shouldLEDFinish(config) { if (LEDAnimations.has(constants.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 === constants.MODE_BLINK || config.mode === constants.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 === constants.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(constants.ALL) && LEDAnimations.get(constants.ALL).stop !== undefined) { return true; } if (LEDAnimations.has(index) && LEDAnimations.get(index).stop !== undefined) { return true; } return false; } async function stopLEDAnimation(index) { if (index === constants.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 !== constants.MODE_BLINK && config.mode !== constants.MODE_PULSE) { return false; } return config.repetitions.max === 0; } // exports module.exports = { getBlinkstick, mapBlinkSticks, simple, complex, powerOff, isInfiniteAnimation, getColors }