From 451af8155dcaaba3d874d492f4f3c4d0834997b9 Mon Sep 17 00:00:00 2001 From: Thomas Cole Date: Fri, 29 Apr 2022 11:21:51 -0400 Subject: [PATCH] Major rework to server. --- apps/server/.env.sample | 5 +- apps/server/Main.js | 36 +++++ apps/server/api/debug/routes.js | 20 --- apps/server/api/routes.js | 15 -- apps/server/api/unset/routes.js | 8 -- apps/server/index.js | 29 ---- apps/server/lib/ConfigManager.js | 132 ++++++++++++++++++ apps/server/lib/Deck.js | 78 +++++++++++ apps/server/lib/DeckButton.js | 59 ++++++++ apps/server/lib/Events.js | 8 ++ apps/server/lib/PluginLoader.js | 25 ++++ apps/server/lib/util.js | 60 ++++++++ apps/server/package.json | 9 +- apps/server/plugins/builtin.js | 34 +++++ apps/server/plugins/keyboard.js | 36 +++++ apps/server/webserver/WebServer.js | 30 ++++ apps/server/webserver/api/buttons.js | 14 ++ apps/server/webserver/api/info.js | 14 ++ .../set/routes.js => webserver/api/pages.js} | 3 +- apps/server/webserver/api/routes.js | 16 +++ 20 files changed, 552 insertions(+), 79 deletions(-) create mode 100644 apps/server/Main.js delete mode 100644 apps/server/api/debug/routes.js delete mode 100644 apps/server/api/routes.js delete mode 100644 apps/server/api/unset/routes.js delete mode 100644 apps/server/index.js create mode 100644 apps/server/lib/ConfigManager.js create mode 100644 apps/server/lib/Deck.js create mode 100644 apps/server/lib/DeckButton.js create mode 100644 apps/server/lib/Events.js create mode 100644 apps/server/lib/PluginLoader.js create mode 100644 apps/server/lib/util.js create mode 100644 apps/server/plugins/builtin.js create mode 100644 apps/server/plugins/keyboard.js create mode 100644 apps/server/webserver/WebServer.js create mode 100644 apps/server/webserver/api/buttons.js create mode 100644 apps/server/webserver/api/info.js rename apps/server/{api/set/routes.js => webserver/api/pages.js} (65%) create mode 100644 apps/server/webserver/api/routes.js diff --git a/apps/server/.env.sample b/apps/server/.env.sample index a942373..2959798 100644 --- a/apps/server/.env.sample +++ b/apps/server/.env.sample @@ -1,2 +1,3 @@ -#Port you want the server to run on. It is best to use something uncomon -PORT=8899 \ No newline at end of file +CONFIG_PATH=.yasd +CONFIG_FILE=deck_config.json +WEBSERVER_PORT=8899 \ No newline at end of file diff --git a/apps/server/Main.js b/apps/server/Main.js new file mode 100644 index 0000000..7752e29 --- /dev/null +++ b/apps/server/Main.js @@ -0,0 +1,36 @@ +require('dotenv').config(); +const sd = require('./lib/Deck'); +const web = require('./webserver/WebServer'); +const cm = require('./lib/ConfigManager'); +const pl = require('./lib/PluginLoader'); + +const EventEmitter = require('events'); + +/** + * Extend the default EventEmitter to have the ability to return an object. + * Normal normal events work as expected + * https://stackoverflow.com/questions/42802931/node-js-how-can-i-return-a-value-from-an-event-listener + */ +class EventObjectEmitter extends EventEmitter { + emitObject(event, obj = {}){ + this.emit(event, obj); + return obj; + } +} +global.eventBus = new EventObjectEmitter(); + +console.log("Staring") + +sd.init(); +web.init(); + +global.pluginloader = new pl(); +pluginloader.loadFromFolder(); + +//config last to be loaded +//It fires off the config_changed event that signals the app is ready +cm.init(); + + + +console.log("Ready!") \ No newline at end of file diff --git a/apps/server/api/debug/routes.js b/apps/server/api/debug/routes.js deleted file mode 100644 index fd3b11a..0000000 --- a/apps/server/api/debug/routes.js +++ /dev/null @@ -1,20 +0,0 @@ -const express = require('express'); -const router = express.Router(); - -router.get('/', (req, res) => { - res.status(200).send("Debug"); -}); - -router.get('/showconfig', (req, res)=>{ - res.status(200).send('CONFIG DATA'); -}); - -router.post('/clearscreen', (req, res)=>{ - res.sendStatus(200); -}); - -router.post('/resetconfig', (req, res)=> { - res.status(200).send('CONFIG RESET'); -}); - -module.exports = router; \ No newline at end of file diff --git a/apps/server/api/routes.js b/apps/server/api/routes.js deleted file mode 100644 index 27c4910..0000000 --- a/apps/server/api/routes.js +++ /dev/null @@ -1,15 +0,0 @@ -const express = require('express') -const router = express.Router(); -const debugRoutes = require('./debug/routes'); -const setRoutes = require('./set/routes'); -const unsetRoutes = require('./unset/routes'); - -router.get('/', (req, res) => { - res.sendStatus(200); -}); - -router.use('/debug', debugRoutes); -router.use('/set', setRoutes); -router.use('/unset', unsetRoutes); - -module.exports = router; \ No newline at end of file diff --git a/apps/server/api/unset/routes.js b/apps/server/api/unset/routes.js deleted file mode 100644 index b908ab2..0000000 --- a/apps/server/api/unset/routes.js +++ /dev/null @@ -1,8 +0,0 @@ -const express = require('express'); -const router = express.Router(); - -router.get('/', (req, res) => { - res.status(200).send("Unset"); -}); - -module.exports = router; \ No newline at end of file diff --git a/apps/server/index.js b/apps/server/index.js deleted file mode 100644 index fe3582a..0000000 --- a/apps/server/index.js +++ /dev/null @@ -1,29 +0,0 @@ -require('dotenv').config(); -const express = require('express'); -const cors = require('cors'); -const app = express(); -const port = process.env.PORT; -const apiRoutes = require('./api/routes'); -const { openStreamDeck } = require('@elgato-stream-deck/node'); - -const myStreamDeck = openStreamDeck(); - -myStreamDeck.on('down', (keyIndex) => { - console.log('key %d down', keyIndex) -}) - -myStreamDeck.on('up', (keyIndex) => { - console.log('key %d up', keyIndex) -}) - -app.use(cors()) - -app.get('/', (req, res) => { - res.status(200).send({some: 'json'}); -}); - -app.use('/api', apiRoutes); - -app.listen(port, () => { - console.log(`Stream deck server listening on port ${port}`); -}); \ No newline at end of file diff --git a/apps/server/lib/ConfigManager.js b/apps/server/lib/ConfigManager.js new file mode 100644 index 0000000..b79d342 --- /dev/null +++ b/apps/server/lib/ConfigManager.js @@ -0,0 +1,132 @@ +const fs = require('fs'); +const homedir = require('os').homedir(); +const configPath = [homedir, process.env.CONFIG_PATH].join('/'); +const fullConfigPath = [configPath, process.env.CONFIG_FILE].join('/'); +const deckButton = require('./DeckButton'); +const EVENTS = require('./Events'); + +let configState; + +/** + * Initializes the configManager. + * This will check for the config file and folder on disk and create a blank configuration if one does not exist. + */ +function init(){ + console.log(" - Starting configManager.js") + + //check for existance of config folder in user home dir + try { + if(fs.existsSync(fullConfigPath)){ + console.log(" - Config file exists"); + } else { + console.log(" - No config file found. Creating one now"); + let blankConfigObject = {"backgroundImage": "",pages:[{pagename: "Blank Page",buttons: []}]} + for (let i = 0; i < 32; i++) { + blankConfigObject.pages[0].buttons.push(new deckButton('', "Test Button: " + i, "#000000", "builtin:nullAction:")); + } + + //Make config folder. + fs.mkdirSync(configPath, {recursive: true}, (err) => { + if(err) throw err; + }); + + //Make config file. + fs.writeFileSync(fullConfigPath, JSON.stringify(blankConfigObject,null,2), (err) => { + if(err) throw err; + }); + } + } catch (error) { + console.error(" - " + error) + } + + //Announce the loaded config on the bus + eventBus.emit(EVENTS.CONFIG_READY, this.getConfig()); + + eventBus.on(EVENTS.UPDATE_BUTTON, data => { + console.log("Update button with: " + JSON.stringify(data)) + updateButton(data.pageNumber, data.buttonIndex, data.iconPath, data.label, data.color, data.pressAction, data.releaseAction, data.toggleable, data.toggleIcon) + }); +} + +/** + * Will read the config from the defined configuration path. + * @returns + * An object of pages. + * Each page is an object with a name and an array of deckButton objects. + * Each array is 32 in length to match the StreamDeckXL. + * All smaller StreamDecks fit within this array + */ +function getConfig(){ + const data = JSON.parse(fs.readFileSync(fullConfigPath)); + configState = data; + + for (let i = 0; i < data.pages.length; i++) { + const page = data.pages[i]; + for (let j = 0; j < page.buttons.length; j++) { + const deckBtn = deckButton.from(page.buttons[j]); + configState.pages[i].buttons[j] = deckBtn; + } + } + + return configState; +} + +/** + * Write the current config state to the disk + */ +function writeConfig(){ + fs.writeFileSync(fullConfigPath, JSON.stringify(configState, null, 2), err => { + if(err){ + console.error(err) + } + }) +} + +//Local function to modify config +/** + * Call to notify the event bus of a new config + */ +function configChanged(){ + writeConfig(); + eventBus.emit('config_changed', configState); +} + +function addPage(){ + +} + +function updateButton(pageNumber, buttonIndex, iconPath, label, color, pressAction, releaseAction, toggleable, toggleIcon){ + let button = configState.pages[pageNumber].buttons[buttonIndex]; + + if(iconPath){ + button.iconPath = iconPath; + } + + if(label){ + button.label = label; + } + + if(color){ + button.color = color; + } + + if(pressAction){ + button.pressAction = pressAction; + } + + if(releaseAction){ + button.releaseAction = releaseAction; + } + + if(toggleable){ + button.toggleable = toggleable; + } + + if(toggleIcon){ + button.toggleIcon = toggleIcon; + } + + configChanged() +} + +module.exports = {init, getConfig, writeConfig} \ No newline at end of file diff --git a/apps/server/lib/Deck.js b/apps/server/lib/Deck.js new file mode 100644 index 0000000..5b25c1f --- /dev/null +++ b/apps/server/lib/Deck.js @@ -0,0 +1,78 @@ +const { openStreamDeck } = require('@elgato-stream-deck/node'); +const util = require('./util'); +const EVENTS = require('./Events'); + +let myStreamDeck; +let deckConfig; +let iconBuffers=[]; +let activePage = 0; + + +/** + * + */ +function init(){ + console.log(" - Starting deck.js") + + eventBus.on(EVENTS.CONFIG_READY, config => { + deckConfig = config; + generateIcons().then(()=>{ + drawPage(activePage) + }) + }); + + eventBus.on(EVENTS.CONFIG_CHANGED, newConfig => { + console.log("Config changed event received in DECK"); + deckConfig = newConfig; + generateIcons().then(()=>{ + drawPage(activePage) + }) + }); + + eventBus.on(EVENTS.GET_ACTIVE_DECK, (e)=>{ + e.deck = myStreamDeck; + }) + + myStreamDeck = openStreamDeck(); + myStreamDeck.clearPanel(); + + registerCallbacks(); +} + +function registerCallbacks(){ + myStreamDeck.on('down', (keyIndex) => { + deckConfig.pages[0].buttons[keyIndex].press() + }); + + myStreamDeck.on('up', (keyIndex) => { + deckConfig.pages[0].buttons[keyIndex].release() + }); +} + +function drawPage(pageNum){ + activePage = pageNum; + for (let i = 0; i < myStreamDeck.NUM_KEYS; i++) { + const color = util.hexToRGB(deckConfig.pages[pageNum].buttons[i].color) + myStreamDeck.fillKeyColor(i, color.r, color.g, color.b) + const buffer = iconBuffers[pageNum][i]; + if(buffer) myStreamDeck.fillKeyBuffer(i, iconBuffers[pageNum][i]) + } + +} + +async function generateIcons(){ + for (let i = 0; i < deckConfig.pages.length; i++) { + const page = deckConfig.pages[i]; + iconBuffers.push([]); + for (let j = 0; j < page.buttons.length; j++) { + const deckBtn = deckConfig.pages[i].buttons[j]; + if(deckBtn.iconPath != ''){ + let buffer = await util.buttonImageToBuffer(deckBtn.iconPath, myStreamDeck.ICON_SIZE) + iconBuffers[i][j] = buffer; + } + } + } + +} + +module.exports = { init } diff --git a/apps/server/lib/DeckButton.js b/apps/server/lib/DeckButton.js new file mode 100644 index 0000000..d705117 --- /dev/null +++ b/apps/server/lib/DeckButton.js @@ -0,0 +1,59 @@ +'use strict' + +module.exports = class deckButton { + constructor(iconPath, label, color, pressAction, releaseAction=null, toggleable=false, toggleIcon=null){ + this.iconPath = iconPath; + this.label = label; + this.color = color; + this.pressAction = pressAction; + this.releaseAction = releaseAction; + this.toggleable = toggleable; + this.toggleIcon = toggleIcon; + } + + press(){ + if(!this.pressAction){ + return; + } + + let action = this.pressAction.split(':'); + try { + const o = pluginloader.plugins[action[0]]; + const f = o[action[1]] + const a = f(action[2]) + } catch (error) { + console.log("Press Referenced plugin not found.") + } + } + + release(){ + if(!this.releaseAction){ + return; + } + + let action = this.releaseAction.split(':'); + try { + const o = pluginloader.plugins[action[0]]; + const f = o[action[1]] + const a = f(action[2]) + } catch (error) { + console.log("Release Referenced plugin not found.") + } + } + + toJSON() { + return { + "iconPath": this.iconPath, + "label": this.label, + "color": this.color, + "pressAction": this.pressAction, + "releaseAction": this.releaseAction, + "toggleable": this.toggleable, + "toggleIcon": this.toggleIcon + }; + } + + static from(json) { + return Object.assign(new deckButton(), json) + } +} \ No newline at end of file diff --git a/apps/server/lib/Events.js b/apps/server/lib/Events.js new file mode 100644 index 0000000..753880a --- /dev/null +++ b/apps/server/lib/Events.js @@ -0,0 +1,8 @@ +module.exports = { + UPDATE_BUTTON: "update_button", + UPDATE_PAGE: "update_page", + CONFIG_READY: "config_ready", + CONFIG_CHANGED: "config_changed", + GET_CONFIG: "get_config", + GET_ACTIVE_DECK: "get_deck", +} \ No newline at end of file diff --git a/apps/server/lib/PluginLoader.js b/apps/server/lib/PluginLoader.js new file mode 100644 index 0000000..188ab19 --- /dev/null +++ b/apps/server/lib/PluginLoader.js @@ -0,0 +1,25 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Loads modules from the plugins folder. + * Will expose the plugin on the event bus. + * @example + */ +class PluginLoader { + constructor(){ + this.plugins = {}; + } + + async loadFromFolder() { + const dirList = fs.readdirSync(path.resolve(__dirname, "../plugins")); + for (let file in dirList) { + console.log(" - Found plugin: " + dirList[file]) + const pluginName = dirList[file].replace('.js', ''); + const module = require("../plugins/" + dirList[file]); + this.plugins = {...this.plugins, [pluginName]: module}; + } + } +} + +module.exports = PluginLoader; \ No newline at end of file diff --git a/apps/server/lib/util.js b/apps/server/lib/util.js new file mode 100644 index 0000000..5844d55 --- /dev/null +++ b/apps/server/lib/util.js @@ -0,0 +1,60 @@ +const path = require('path'); +const sharp = require('sharp'); + +/** + * Generates a promise to fill the button image buffer + * @param {*} imgPath + * @param {*} iconSize + * @returns + */ +async function buttonImageToBuffer(imgPath, iconSize){ + const buffer = await sharp(path.resolve(imgPath)) + .flatten() + .resize(iconSize, iconSize) + .raw() + .toBuffer() + .catch( err => { + console.log(err) + }) + + return Promise.resolve(buffer); +} + +/** + * Generates the buffer to fill the SD Panel + * @param {*} imgPath + * @param {*} iconSize + * @param {*} cols + * @param {*} rows + * @param {*} callback + */ +async function panelImageToBuffer(imgPath, iconSize, cols, rows, callback){ + await sharp(path.resolve(imgPath)) + .flatten() + .resize(iconSize*cols, iconSize*rows) + .raw() + .toBuffer() + .then( data => { + callback(data) + }) + .catch( err => { + console.log(err) + callback(null) + }) +} + +/** + * https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb + * @param {hex color code} hex + * @returns Object with r g b values. + */ +function hexToRGB(hex){ + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} + +module.exports = {buttonImageToBuffer, panelImageToBuffer, hexToRGB} \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index dbb05e2..04845f9 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -3,16 +3,17 @@ "version": "0.0.1", "private": true, "scripts": { - "dev": "nodemon index.js", - "start": "node index.js" + "dev": "nodemon Main.js", + "start": "node Main.js" }, "dependencies": { "@elgato-stream-deck/node": "5.3.1", "express": "^4.17.3", "cors": "^2.8.5", "open": "^8.4.0", - "robotjs": "^0.6.0", - "dotenv": "16.0.0" + "dotenv": "16.0.0", + "@nut-tree/nut-js": "2.0.1", + "sharp": "0.30.4" }, "devDependencies": { "nodemon": "^2.0.15" diff --git a/apps/server/plugins/builtin.js b/apps/server/plugins/builtin.js new file mode 100644 index 0000000..f6e9316 --- /dev/null +++ b/apps/server/plugins/builtin.js @@ -0,0 +1,34 @@ +/** + * Placeholder action + */ +function nullAction(){ +} + +/** + * Changes the active page. + * @param {The desired page} page + */ +function changePage(page){ + +} + +/** + * Change the brightness of the streamdeck + * @param {The relative change in brightness.} relativeChange + * @example + * changeBrightness(5) - Plus 5% brightness + * changeBrightness(-5) - Minus 5% brightness + */ +function changeBrightness(relativeChange){ + +} + +/** + * Sets desired brightness on the streamdeck. (0 - 100) + * @param {Percent brightness} percent + */ +function setBrightness(percent){ + +} + +module.exports={changePage, changeBrightness, setBrightness, nullAction} \ No newline at end of file diff --git a/apps/server/plugins/keyboard.js b/apps/server/plugins/keyboard.js new file mode 100644 index 0000000..8f36f12 --- /dev/null +++ b/apps/server/plugins/keyboard.js @@ -0,0 +1,36 @@ +const {keyboard, Key } = require('@nut-tree/nut-js'); + +/** + * A few predefined shortcuts for common actions like copy, paste, etc... + */ +const SHORTCUTS = { + copy: [Key.LeftControl, Key.C], + paste: [Key.LeftControl, Key.V], + cut: [Key.LeftControl, Key.X], + altf4: [Key.LeftAlt, Key.F4] +} + +/** + * Will type a string with a provided delay. + * @param {A string or sequence of keys to type. Wrapper for @nut-tree/nut-js keyboard class} string + * @param {Delay between keypresses in milliseconds.} delay + */ +function typeString(string, delay = 0){ + keyboard.config.autoDelayMs = delay + keyboard.type(string); +} + +/** + * Will type a sequence of keys. + * You can also call on some predefined shortcuts. + * @param {An array of keys using the nutjs key import or key codes} keys + * @example + * typeCommnad(Key.LeftControl, Key.C) + * typeCommand(SHORTCUTS.copy) + */ +function typeCommand(keys){ + keyboard.pressKey(...keys); + keyboard.releaseKey(...keys) +} + +module.exports = {SHORTCUTS, typeCommand, typeString} \ No newline at end of file diff --git a/apps/server/webserver/WebServer.js b/apps/server/webserver/WebServer.js new file mode 100644 index 0000000..bf25b32 --- /dev/null +++ b/apps/server/webserver/WebServer.js @@ -0,0 +1,30 @@ +const EVENTS = require('../lib/Events'); +const express = require('express'); +const cors = require('cors'); +const app = express(); +const port = 8899; +const api = require('./api/routes'); + +function init(){ + eventBus.on(EVENTS.CONFIG_READY, config => { + }); + + eventBus.on(EVENTS.CONFIG_CHANGED, newConfig => { + console.log("Config changed event received in WEB"); + }); + + app.use(cors()); + app.use(express.json()) + app.use('/api', api) + + app.get('/', (req, res) => { + res.sendStatus(200); + }); + + app.listen(port, () => { + console.log(`Stream deck server listening on port ${port}`); + }); +} + + +module.exports = {init} \ No newline at end of file diff --git a/apps/server/webserver/api/buttons.js b/apps/server/webserver/api/buttons.js new file mode 100644 index 0000000..e395650 --- /dev/null +++ b/apps/server/webserver/api/buttons.js @@ -0,0 +1,14 @@ +const express = require('express'); +const router = express.Router(); +const EVENTS = require('../../lib/Events'); + +router.get('/', (req, res) => { + res.sendStatus(200); +}); + +router.post('/updateButton', (req, res) => { + eventBus.emit(EVENTS.UPDATE_BUTTON, req.body) + res.sendStatus(200); +}) + +module.exports = router; \ No newline at end of file diff --git a/apps/server/webserver/api/info.js b/apps/server/webserver/api/info.js new file mode 100644 index 0000000..b5af208 --- /dev/null +++ b/apps/server/webserver/api/info.js @@ -0,0 +1,14 @@ +const express = require('express'); +const router = express.Router(); +const EVENTS = require('../../lib/Events'); + +router.get('/', (req, res) => { + res.sendStatus(200); +}); + +router.get('/device', (req, res) => { + const deckInfo = eventBus.emitObject(EVENTS.GET_ACTIVE_DECK, {deck: {}}); + res.status(200).send(JSON.stringify(deckInfo.deck.device.deviceProperties)) +}); + +module.exports = router; \ No newline at end of file diff --git a/apps/server/api/set/routes.js b/apps/server/webserver/api/pages.js similarity index 65% rename from apps/server/api/set/routes.js rename to apps/server/webserver/api/pages.js index 4926b17..78e552b 100644 --- a/apps/server/api/set/routes.js +++ b/apps/server/webserver/api/pages.js @@ -1,8 +1,9 @@ const express = require('express'); const router = express.Router(); +const EVENTS = require('../../lib/Events'); router.get('/', (req, res) => { - res.status(200).send("Set"); + res.sendStatus(200); }); module.exports = router; \ No newline at end of file diff --git a/apps/server/webserver/api/routes.js b/apps/server/webserver/api/routes.js new file mode 100644 index 0000000..3e69c4d --- /dev/null +++ b/apps/server/webserver/api/routes.js @@ -0,0 +1,16 @@ +const express = require('express'); +const router = express.Router(); + +const info = require('./info'); +const buttons = require('./buttons'); +const pages = require('./pages'); + +router.get('/', (req, res) => { + res.sendStatus(200); +}); + +router.use('/info', info); +router.use('/buttons', buttons); +router.use('/pages', pages) + +module.exports = router; \ No newline at end of file