Major rework to server.

This commit is contained in:
Thomas Cole 2022-04-29 11:21:51 -04:00
parent fd01385fe8
commit 451af8155d
20 changed files with 552 additions and 79 deletions

View File

@ -1,2 +1,3 @@
#Port you want the server to run on. It is best to use something uncomon
PORT=8899
CONFIG_PATH=.yasd
CONFIG_FILE=deck_config.json
WEBSERVER_PORT=8899

36
apps/server/Main.js Normal file
View File

@ -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!")

View File

@ -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;

View File

@ -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;

View File

@ -1,8 +0,0 @@
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.status(200).send("Unset");
});
module.exports = router;

View File

@ -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}`);
});

View File

@ -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}

78
apps/server/lib/Deck.js Normal file
View File

@ -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 }

View File

@ -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)
}
}

View File

@ -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",
}

View File

@ -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;

60
apps/server/lib/util.js Normal file
View File

@ -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}

View File

@ -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"

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;