From 1e588718a855ae2871a8841f6c6e621f49795454 Mon Sep 17 00:00:00 2001 From: "jakob.stendahl" Date: Sat, 17 Dec 2022 21:31:41 +0100 Subject: Start moving to esm, work on updater --- src/NeoRuntimeManager/IPC.cjs | 199 ++++++++++++++++++ src/NeoRuntimeManager/IPC.js | 199 ------------------ src/NeoRuntimeManager/RuntimeProcess.cjs | 106 ++++++++++ src/NeoRuntimeManager/RuntimeProcess.js | 106 ---------- src/NeoRuntimeManager/index.cjs | 339 +++++++++++++++++++++++++++++++ src/NeoRuntimeManager/index.js | 339 ------------------------------- 6 files changed, 644 insertions(+), 644 deletions(-) create mode 100644 src/NeoRuntimeManager/IPC.cjs delete mode 100644 src/NeoRuntimeManager/IPC.js create mode 100644 src/NeoRuntimeManager/RuntimeProcess.cjs delete mode 100644 src/NeoRuntimeManager/RuntimeProcess.js create mode 100644 src/NeoRuntimeManager/index.cjs delete mode 100644 src/NeoRuntimeManager/index.js (limited to 'src/NeoRuntimeManager') diff --git a/src/NeoRuntimeManager/IPC.cjs b/src/NeoRuntimeManager/IPC.cjs new file mode 100644 index 0000000..0684418 --- /dev/null +++ b/src/NeoRuntimeManager/IPC.cjs @@ -0,0 +1,199 @@ +/** + * This module is used to communicate with a python NeoRuntime instance. + * + * @author jakobst1n. + * @since 3.10.2021 + */ + +const net = require("net"); +let logger = require("../Logger/index.cjs"); + +/** @type {int} How long wait between each reconnection attempt */ +const RECONNECT_INTERVAL = 1000; +/** @type {Object} ENUM-ish for command that can be sent to neoruntime */ +const COMMAND = Object.freeze({SET_GLOB : 0, + SET_VAR : 1, + SET_SEND_STRIP_BUF: 2}); +/** @type {Object} ENUM-ish for globvars */ +const GLOBVAR = Object.freeze({POWER_ON : 0, + BRIGHTNESS: 1}); +/** @type {Object} ENUM-ish for what type of data neoruntime sends */ +const DATATYPE = Object.freeze({STATES : 1, + STRIP_BUF: 2, + MATRIX: 3}); + +/** + * class that will keep a active connection to a socket if possible, and + * automatically reconnect. It will emit events when data is received, + * and it will send commands to the process. */ +class IPC { + + constructor(_socketFile, _eventEmitter) { + this.socketFile = _socketFile; + this.eventEmitter = _eventEmitter; + + this.client; + this.connected = false; + this.reconnectInterval = false; + + this.globvars = {}; + this.variables = {}; + + this.reconnect(); + } + + /** + * If we are not already attempting to reconnect, this will start a + * interval that tries to reconnect. */ + reconnect() { + if (this.reconnectInterval === false) { + this.reconnectInterval = setInterval(this.tryOpenSocketConnection.bind(this), RECONNECT_INTERVAL); + } + } + + /** + * This will attempt to connect to the socket, and then setup all listeners + * if it succedes. */ + tryOpenSocketConnection() { + // logger.info("Attempting to start IPC"); + + this.client = net.createConnection(this.socketFile) + .on('connect', () => { + clearInterval(this.reconnectInterval); + this.reconnectInterval = false; + logger.info("IPC Connected."); + }) + .on("ready", () => { + this.connected = true; + }) + .on('data', (data) => { + let json_data; + switch (data[0]) { + case DATATYPE.STATES: + try { + json_data = JSON.parse(data.toString("ascii", 1)); + } catch (e) { + logger.warning("Could not parse json data from neoruntime"); + return; + } + + if (json_data.hasOwnProperty("globvars")) { + forEachDiff(json_data["globvars"], this.globvars, (key, newVal) => { + this.eventEmitter.emit("change", key, newVal); + }); + this.globvars = json_data["globvars"]; + } + if (json_data.hasOwnProperty("variables")) { + forEachDiff(json_data["variables"], this.variables, (key, newVal) => { + this.eventEmitter.emit("change", `variable/${key}`, newVal); + }); + this.variables = json_data["variables"]; + } + break; + + case DATATYPE.MATRIX: + try { + json_data = JSON.parse(data.toString("ascii", 1)); + } catch (e) { + logger.warning("Could not parse json data from neoruntime"); + console.log(e); + } + this.eventEmitter.emit("matrix", json_data); + break; + + case DATATYPE.STRIP_BUF: + this.eventEmitter.emit("strip_buffer", Array.from(data.values()).slice(1)); + break; + + default: + logger.info(data); + } + + }) + .on("timeout", () => { + logger.info("IPC Timeout"); + }) + .on("close", (hadError) => { + logger.info("IPC Close, hadError: ", hadError); + this.connected = false; + this.reconnect(); + }) + .on("end", () => { + logger.info("IPC End"); + this.connected = false; + }) + .on('error', (data) => { + logger.info('IPC Server not active.'); + this.connected = false; + this.reconnect(); + }) + ; + } + + /** + * Will send a command to the socket if we have a active connection, + * if not it will just drop the command. there is no queue implemented + * for such events. */ + sendCommand(commandType, name, value) { + if (this.connected) { + let buf; + + switch (commandType) { + case (COMMAND.SET_GLOB): + buf = Buffer.allocUnsafe(3); + buf[1] = name; + buf[2] = value; + break; + + case (COMMAND.SET_VAR): + if (name.length > 32) { return {success: false, reason: "name too long", detail: "max size of name is 32 bytes"}; } + if (value.length > 93) { return {success: false, reason: "value too long", detail: "max size of value is 93 bytes"}; } + buf = Buffer.allocUnsafe(3 + name.length + value.length); + buf[1] = name.length; + buf[2] = value.length; + buf.write(name, 3, name.length, "ascii"); + buf.write(value, 3+name.length, value.length, "ascii"); + break; + + case (COMMAND.SET_SEND_STRIP_BUF): + buf = Buffer.allocUnsafe(2); + buf[1] = (name) ? 1 : 0; + break; + + default: + logger.warning(`IPC UNKNOWN COMMANDTYPE ${commandType}`) + return {success: false, reason: "ipc command unknown", detail: commandType}; + } + + buf[0] = commandType; + this.client.write(buf); + return {success: true} + } + return {success: false, reason: "socket not connected", detail: "This usually means the python script is not running"}; + } + +} + +const isObject = v => v && typeof v === 'object'; + +/** + * Will call callback on all the differences between the dicts + */ +function forEachDiff(dict1, dict2, callback) { + for (const key of new Set([...Object.keys(dict1), ...Object.keys(dict2)])) { + if (isObject(dict1[key]) && isObject(dict2[key])) { + if (dict1[key].value !== dict2[key].value) { + callback(key, dict1[key]); + } + } else if (dict1[key] !== dict2[key]) { + if (isObject(dict2[key]) && (dict1[key] == null)) { + dict2[key].value = null; + callback(key, dict2[key]) + } else { + callback(key, dict1[key]); + } + } + } +} + +module.exports = {IPC, COMMAND, GLOBVAR}; diff --git a/src/NeoRuntimeManager/IPC.js b/src/NeoRuntimeManager/IPC.js deleted file mode 100644 index 56c8b5d..0000000 --- a/src/NeoRuntimeManager/IPC.js +++ /dev/null @@ -1,199 +0,0 @@ -/** - * This module is used to communicate with a python NeoRuntime instance. - * - * @author jakobst1n. - * @since 3.10.2021 - */ - -const net = require("net"); -let logger = require(__appdir + "/src/Logger"); - -/** @type {int} How long wait between each reconnection attempt */ -const RECONNECT_INTERVAL = 1000; -/** @type {Object} ENUM-ish for command that can be sent to neoruntime */ -const COMMAND = Object.freeze({SET_GLOB : 0, - SET_VAR : 1, - SET_SEND_STRIP_BUF: 2}); -/** @type {Object} ENUM-ish for globvars */ -const GLOBVAR = Object.freeze({POWER_ON : 0, - BRIGHTNESS: 1}); -/** @type {Object} ENUM-ish for what type of data neoruntime sends */ -const DATATYPE = Object.freeze({STATES : 1, - STRIP_BUF: 2, - MATRIX: 3}); - -/** - * class that will keep a active connection to a socket if possible, and - * automatically reconnect. It will emit events when data is received, - * and it will send commands to the process. */ -class IPC { - - constructor(_socketFile, _eventEmitter) { - this.socketFile = _socketFile; - this.eventEmitter = _eventEmitter; - - this.client; - this.connected = false; - this.reconnectInterval = false; - - this.globvars = {}; - this.variables = {}; - - this.reconnect(); - } - - /** - * If we are not already attempting to reconnect, this will start a - * interval that tries to reconnect. */ - reconnect() { - if (this.reconnectInterval === false) { - this.reconnectInterval = setInterval(this.tryOpenSocketConnection.bind(this), RECONNECT_INTERVAL); - } - } - - /** - * This will attempt to connect to the socket, and then setup all listeners - * if it succedes. */ - tryOpenSocketConnection() { - // logger.info("Attempting to start IPC"); - - this.client = net.createConnection(this.socketFile) - .on('connect', () => { - clearInterval(this.reconnectInterval); - this.reconnectInterval = false; - logger.info("IPC Connected."); - }) - .on("ready", () => { - this.connected = true; - }) - .on('data', (data) => { - let json_data; - switch (data[0]) { - case DATATYPE.STATES: - try { - json_data = JSON.parse(data.toString("ascii", 1)); - } catch (e) { - logger.warning("Could not parse json data from neoruntime"); - return; - } - - if (json_data.hasOwnProperty("globvars")) { - forEachDiff(json_data["globvars"], this.globvars, (key, newVal) => { - this.eventEmitter.emit("change", key, newVal); - }); - this.globvars = json_data["globvars"]; - } - if (json_data.hasOwnProperty("variables")) { - forEachDiff(json_data["variables"], this.variables, (key, newVal) => { - this.eventEmitter.emit("change", `variable/${key}`, newVal); - }); - this.variables = json_data["variables"]; - } - break; - - case DATATYPE.MATRIX: - try { - json_data = JSON.parse(data.toString("ascii", 1)); - } catch (e) { - logger.warning("Could not parse json data from neoruntime"); - console.log(e); - } - this.eventEmitter.emit("matrix", json_data); - break; - - case DATATYPE.STRIP_BUF: - this.eventEmitter.emit("strip_buffer", Array.from(data.values()).slice(1)); - break; - - default: - logger.info(data); - } - - }) - .on("timeout", () => { - logger.info("IPC Timeout"); - }) - .on("close", (hadError) => { - logger.info("IPC Close, hadError: ", hadError); - this.connected = false; - this.reconnect(); - }) - .on("end", () => { - logger.info("IPC End"); - this.connected = false; - }) - .on('error', (data) => { - logger.info('IPC Server not active.'); - this.connected = false; - this.reconnect(); - }) - ; - } - - /** - * Will send a command to the socket if we have a active connection, - * if not it will just drop the command. there is no queue implemented - * for such events. */ - sendCommand(commandType, name, value) { - if (this.connected) { - let buf; - - switch (commandType) { - case (COMMAND.SET_GLOB): - buf = Buffer.allocUnsafe(3); - buf[1] = name; - buf[2] = value; - break; - - case (COMMAND.SET_VAR): - if (name.length > 32) { return {success: false, reason: "name too long", detail: "max size of name is 32 bytes"}; } - if (value.length > 93) { return {success: false, reason: "value too long", detail: "max size of value is 93 bytes"}; } - buf = Buffer.allocUnsafe(3 + name.length + value.length); - buf[1] = name.length; - buf[2] = value.length; - buf.write(name, 3, name.length, "ascii"); - buf.write(value, 3+name.length, value.length, "ascii"); - break; - - case (COMMAND.SET_SEND_STRIP_BUF): - buf = Buffer.allocUnsafe(2); - buf[1] = (name) ? 1 : 0; - break; - - default: - logger.warning(`IPC UNKNOWN COMMANDTYPE ${commandType}`) - return {success: false, reason: "ipc command unknown", detail: commandType}; - } - - buf[0] = commandType; - this.client.write(buf); - return {success: true} - } - return {success: false, reason: "socket not connected", detail: "This usually means the python script is not running"}; - } - -} - -const isObject = v => v && typeof v === 'object'; - -/** - * Will call callback on all the differences between the dicts - */ -function forEachDiff(dict1, dict2, callback) { - for (const key of new Set([...Object.keys(dict1), ...Object.keys(dict2)])) { - if (isObject(dict1[key]) && isObject(dict2[key])) { - if (dict1[key].value !== dict2[key].value) { - callback(key, dict1[key]); - } - } else if (dict1[key] !== dict2[key]) { - if (isObject(dict2[key]) && (dict1[key] == null)) { - dict2[key].value = null; - callback(key, dict2[key]) - } else { - callback(key, dict1[key]); - } - } - } -} - -module.exports = {IPC, COMMAND, GLOBVAR}; diff --git a/src/NeoRuntimeManager/RuntimeProcess.cjs b/src/NeoRuntimeManager/RuntimeProcess.cjs new file mode 100644 index 0000000..be78fa9 --- /dev/null +++ b/src/NeoRuntimeManager/RuntimeProcess.cjs @@ -0,0 +1,106 @@ +let fs = require("fs-extra"); +let spawn = require("child_process"); + +class RuntimeProcess { + + constructor(_modePath, _eventEmitter) { + this.modePath = _modePath; + this.logfile = `${this.modePath}/mode.log`; + this.errfile = `${this.modePath}/mode.error`; + + this.stdout = ""; + this.stderr = ""; + + this.fl = false; + this.proc = null; + + this.isRunning = false; + this.exitCode = null; + + this.eventEmitter = _eventEmitter; + } + + start() { + if (this.isRunning) { + console.log("PROCESS ALREADY RUNNING"); + return {success: false, reason: "already running"}; + } + this.isRunning = true; + this.proc = spawn.spawn( + // `${__appdir}/NeoRuntime/Runtime/venv/bin/python`, + "python3", + [ + "-u", // This makes us able to get real-time output + `${__appdir}/NeoRuntime/Runtime/neo_runtime.py`, + `--strip-config="${__configdir}/strip.ini"`, + `--mode-path="${this.modePath}"`, + `--mode-entry=script` + ] + ); + + this.proc.on('error', (err) => { + console.log(err); + }); + + fs.ensureFileSync(this.logfile); + fs.ensureFileSync(this.errfile); + this.eventEmitter.emit("proc:start"); + + this.proc.stdout.on('data', (_stdout) => { + let stdout_str = _stdout.toString(); + fs.appendFile(this.logfile, `[${timestamp()}]: ` + stdout_str); + this.eventEmitter.emit("proc:stdout", stdout_str); + }); + + this.proc.stdout.on('end', () => { + fs.appendFile(this.logfile, "\n"); + }); + + this.proc.stderr.on('data', (_stderr) => { + let stderr_str = _stderr.toString(); + fs.appendFile(this.errfile, `[${timestamp()}]: ` + stderr_str); + this.eventEmitter.emit("proc:stderr", stderr_str); + }); + + this.proc.stderr.on('end', () => { + fs.appendFile(this.logfile, "\n"); + }); + + this.proc.on('close', (code) => { + if (code) { + fs.appendFile(this.logfile, `[${timestamp()}]: ` + "Script exited with code " + code.toString()); + } + this.eventEmitter.emit("proc:exit", 0); + this.isRunning = false; + this.exitCode = code; + }); + + return {success: true}; + } + + stop(restart=false) { + try { + if (restart) { + this.proc.once("close", () => { + setTimeout(() => this.start(), 500); + }); + } + this.proc.kill("SIGINT"); + return {success: true} + } catch (err) { + console.log(err); + return {success:false, reason:err} + } + } +} + +/** + * Creates and returns a timestamp that can be used in logfiles. + * + * @return {string} timestamp + */ +function timestamp() { + return (new Date()).toISOString(); +} + +module.exports = RuntimeProcess; diff --git a/src/NeoRuntimeManager/RuntimeProcess.js b/src/NeoRuntimeManager/RuntimeProcess.js deleted file mode 100644 index be78fa9..0000000 --- a/src/NeoRuntimeManager/RuntimeProcess.js +++ /dev/null @@ -1,106 +0,0 @@ -let fs = require("fs-extra"); -let spawn = require("child_process"); - -class RuntimeProcess { - - constructor(_modePath, _eventEmitter) { - this.modePath = _modePath; - this.logfile = `${this.modePath}/mode.log`; - this.errfile = `${this.modePath}/mode.error`; - - this.stdout = ""; - this.stderr = ""; - - this.fl = false; - this.proc = null; - - this.isRunning = false; - this.exitCode = null; - - this.eventEmitter = _eventEmitter; - } - - start() { - if (this.isRunning) { - console.log("PROCESS ALREADY RUNNING"); - return {success: false, reason: "already running"}; - } - this.isRunning = true; - this.proc = spawn.spawn( - // `${__appdir}/NeoRuntime/Runtime/venv/bin/python`, - "python3", - [ - "-u", // This makes us able to get real-time output - `${__appdir}/NeoRuntime/Runtime/neo_runtime.py`, - `--strip-config="${__configdir}/strip.ini"`, - `--mode-path="${this.modePath}"`, - `--mode-entry=script` - ] - ); - - this.proc.on('error', (err) => { - console.log(err); - }); - - fs.ensureFileSync(this.logfile); - fs.ensureFileSync(this.errfile); - this.eventEmitter.emit("proc:start"); - - this.proc.stdout.on('data', (_stdout) => { - let stdout_str = _stdout.toString(); - fs.appendFile(this.logfile, `[${timestamp()}]: ` + stdout_str); - this.eventEmitter.emit("proc:stdout", stdout_str); - }); - - this.proc.stdout.on('end', () => { - fs.appendFile(this.logfile, "\n"); - }); - - this.proc.stderr.on('data', (_stderr) => { - let stderr_str = _stderr.toString(); - fs.appendFile(this.errfile, `[${timestamp()}]: ` + stderr_str); - this.eventEmitter.emit("proc:stderr", stderr_str); - }); - - this.proc.stderr.on('end', () => { - fs.appendFile(this.logfile, "\n"); - }); - - this.proc.on('close', (code) => { - if (code) { - fs.appendFile(this.logfile, `[${timestamp()}]: ` + "Script exited with code " + code.toString()); - } - this.eventEmitter.emit("proc:exit", 0); - this.isRunning = false; - this.exitCode = code; - }); - - return {success: true}; - } - - stop(restart=false) { - try { - if (restart) { - this.proc.once("close", () => { - setTimeout(() => this.start(), 500); - }); - } - this.proc.kill("SIGINT"); - return {success: true} - } catch (err) { - console.log(err); - return {success:false, reason:err} - } - } -} - -/** - * Creates and returns a timestamp that can be used in logfiles. - * - * @return {string} timestamp - */ -function timestamp() { - return (new Date()).toISOString(); -} - -module.exports = RuntimeProcess; diff --git a/src/NeoRuntimeManager/index.cjs b/src/NeoRuntimeManager/index.cjs new file mode 100644 index 0000000..81bd7e8 --- /dev/null +++ b/src/NeoRuntimeManager/index.cjs @@ -0,0 +1,339 @@ +/** + * This module is used to execute and communicate with a python NeoRuntime instance. + * + * @author jakobst1n. + * @since 19.12.2019 + */ + +const fs = require("fs"); +const fsPromises = fs.promises; +const RuntimeProcess = require("./RuntimeProcess.cjs"); +const IPC = require("./IPC.cjs"); +const logger = require("../Logger/index.cjs"); +const EventEmitter = require('events'); + +/** @type {object} this should be a pointer to a object referencing all neoModules (see app.js) */ +let neoModules; + +/** @type {string} Currently active mode */ +let modeId = null; +/** @type {int} Last exit code of a mode */ +let modeExitCode = 0; +/** @type {RuntimeProcess} This is the current RuntimeProcess instance */ +let runtimeProcess = null; +/** @type {IPC} The IPC instance, used to communicate with the script */ +let ipc = null; +/** @type {EventEmitter} This is used to emit events when things change */ +const eventEmitter = new EventEmitter(); +/** @type {boolean} If this is true, we will not do things the usual way */ +let modeDebuggerActive = false; +/** @type {string} Should be the modeId the debugger is attached to */ +let modeDebuggerId = null; +/** @type {object} Handler for proc:start when debugger is active */ +let modeDebuggerProcStartHandler = null; +/** @type {object} The last received matrix setup */ +let matrix = null; +/** @type {object} intervall for sending current state */ +let debugModeStateEmitIntervall = null; + +eventEmitter.on("proc:exit", (code) => modeExitCode = code); +eventEmitter.on("matrix", (_matrix) => matrix = _matrix); + +/** + * Check if a path id actually a mode (if it is a folder with a script.py file) + * + * @param {string} path - Path to check. + * + * @return {boolean} wether the path points to a valid mode. + */ +function isMode(path) { + if (!fs.existsSync(path)) { return false; } + let folderStat = fs.statSync(path); + if (!folderStat.isDirectory()) { return false; } + if (!fs.existsSync(path + "/script.py")) { return false; } + return true; +} + +/** + * Get all ids of modes that can be set. + * + * @returns {array} All modeids + */ +function listModes() { + let modeDirs = [ + ["builtin/", fs.readdirSync(__appdir + "/NeoRuntime/builtin")], + ["remote/", fs.readdirSync(__datadir + "/remoteCode")], + ["user/", fs.readdirSync(__datadir + "/userCode")] + ] + let validModes = []; + for (modeDir of modeDirs) { + for (modeName of modeDir[1]) { + let modeId = `${modeDir[0]}${modeName}`; + if (isMode(getModePath(modeId))) { + validModes.push(modeId); + } + } + } + return validModes; +} + +/** + * Change mode, stop the old one and start the new one. + * + * @param {string} _modeId - Id of the mode to change to. + * + * @return {object} A standardform return object. + */ +function setMode(_modeId) { + if (modeDebuggerActive && (_modeId != modeDebuggerId)) { + return {success: false, reason: "debugger active", detail: "Cannot change mode when debugger is active."} + } + if (!isMode(getModePath(_modeId))) { + console.log(`Invalid mode "${_modeId}".`); + return {success: false, reason: "unknown modeId"}; + } + logger.info(`Changing mode to "${_modeId}".`); + + stopMode(); + + modeId = _modeId; + neoModules.userData.config.activeMode = modeId; + eventEmitter.emit("change", "mode", modeId); + + runtimeProcess = new RuntimeProcess(getModePath(_modeId), eventEmitter); + startMode(); + + return {success: true} +}; + +/** + * Get current mode + * + * @return {string} current modeId + */ +function currentMode() { + return modeId; +} + +/** + * Will attempt to stop current mode + * + * @return {object} A standardform return object. + */ +function stopMode(restart=false) { + if (modeRunning()) { + runtimeProcess.stop(restart); + } + return {success: true} +}; + +/** + * Will attempt to start current mode + * + * @return {object} A standardform return object. + */ +function startMode() { + if (runtimeProcess === null) { + return {success: false, reason: "no runtimeprocess", detail: "Runtimeprocess not set, did you mean to call setMode?"}; + } + return runtimeProcess.start(); +}; + +/** + * Will attempt to restart current mode + * + * @return {object} A standardform return object. + */ +function restartMode() { + return stopMode(true); +}; + +/** + * Checks if mode is running currently + * + * @return {boolean} if mode is running + */ +function modeRunning() { + if (runtimeProcess === null) { return false; } + return runtimeProcess.isRunning; +}; + +/** + * Get the full system path to a mode + * + * @param {string} modeId + * + * @return {string} Full path of mode + */ +function getModePath(modeId) { + let path = modeId.split("/"); + let location = path.splice(0, 1).toString(); + if (location === "user") { path = __datadir + "/userCode/" + path.join("/"); } + if (location === "remote") { path = __datadir + "/remoteCode/" + path.join("/"); } + if (location === "builtin") { path = __appdir + "/NeoRuntime/builtin/" + path.join("/"); } + return path; +} + +/** + * Function that returns all globvars (brightness, power_on) as the values they + * had last time we heard from the python script. + * + * @return {object} + */ +function getGlobvars() { + if (!modeRunning()) { return {}; } + return ipc.globvars; +} + +/** + * Sets value of a globvar power_on/brightness. + * + * @param {string} name - Name of the variable power_on/brightness + * @param {any} value - The value the variable should be set to + * + * @return {object} Standardform return object + */ +function setGlobvar(name, value) { + if (!modeRunning()) { return; } + + switch(name) { + case "power_on": + return ipc.sendCommand(IPC.COMMAND.SET_GLOB, IPC.GLOBVAR.POWER_ON, (value) ? 1 : 0); + case "brightness": + return ipc.sendCommand(IPC.COMMAND.SET_GLOB, IPC.GLOBVAR.BRIGHTNESS, value); + default: + return {success: false, reason: "unknown globvar", detail: name}; + } +} + +/** + * Get all variables declared in mode + * + * @return {object} + */ +function getVariables() { + if (!modeRunning()) { return {}; } + return ipc.variables; +} + +/** + * Sets value of a variable + * + * @param {string} name - Name of the variable + * @param {any} value - The value the variable should be set to + * + * @return {object} Standardform return object + */ +function setVariable(name, value) { + if (!modeRunning()) { return; } + return ipc.sendCommand(IPC.COMMAND.SET_VAR, name, value); +} + +/** + * A function intented to be used in an interval to emit + * the current debug-state. + * + */ +function debugModeEmitState() { + eventEmitter.emit("debugger:state", { + mode: modeDebuggerId, + running: runtimeProcess.isRunning, + debugMode: modeDebuggerActive, + matrix: matrix + }); +} + +/** + * Start debugger for a mode + * + * @param {string} modeId - The mode to debug + * + * @return {object} Standardform return object + */ +function startDebugger(debuggerModeId) { + if (debuggerModeId.substr(0, 5) !== "user/") { return {success: false, reason: "not user mode"}; } + if (!isMode(getModePath(debuggerModeId))) { return {success: false, reason: "unknown modeId"}; } + if (modeDebuggerActive) { return {success: false, reason: "debugger already active"}; } + logger.info(`Starting debugger for ${debuggerModeId}`); + + if (modeDebuggerProcStartHandler == null) { + modeDebuggerProcStartHandler = eventEmitter.on("proc:start", () => { + setTimeout(() => { + ipc.sendCommand(IPC.COMMAND.SET_SEND_STRIP_BUF, true); + }, 2000); + }); + } else { + console.log(modeDebuggerProcStartHandler); + } + + if (debugModeStateEmitIntervall == null) { + debugModeStateEmitIntervall = setInterval(debugModeEmitState, 1000); + } + + modeDebuggerActive = true; + modeDebuggerId = debuggerModeId; + setTimeout(() => { + setMode(debuggerModeId); + }, 300); + return {success: true, code: fs.readFileSync(getModePath(debuggerModeId) + "/script.py").toString()} +} + +/** + * Save mode + */ +function saveModeCode(_modeId, code) { + if (!modeDebuggerActive) { return {success: false, reason: "debugger not active"}; }; + if (_modeId != modeDebuggerId) { return {success: false, reason: "modeid not the same as debuggermodeid"}; }; + fs.writeFileSync(getModePath(`${modeDebuggerId}/script.py`), code); + return {success: true}; +} + +/** + * Stop the active debugger + * + * @return {object} Standardform return object + */ +function stopDebugger() { + if (!modeDebuggerActive) { return {success: true, detail: "No debugger active"} } + logger.info(`Stopping debugger`); + modeDebuggerActive = false; + eventEmitter.removeAllListeners("proc:start", modeDebuggerProcStartHandler); + modeDebuggerProcStartHandler = null; + + clearInterval(debugModeStateEmitIntervall); + debugModeStateEmitIntervall = null; + + ipc.sendCommand(IPC.COMMAND.SET_SEND_STRIP_BUF, false); + return {success: true} +} + +module.exports = (_neoModules) => { + neoModules = _neoModules; + ipc = new IPC.IPC(neoModules.userData.config.neoRuntimeIPC.socketFile, eventEmitter); + return { + event: eventEmitter, + modes: listModes, + mode: { + current: currentMode, + set: (modeId) => setMode(modeId), + status: { + modeRunning: modeRunning(), + modeExitCode: modeExitCode + }, + globvars: { + get: getGlobvars, + set: setGlobvar + }, + variables: { + get: getVariables, + set: setVariable + } + }, + getModePath, + isMode, + modeRunning, + startDebugger, stopDebugger, saveModeCode, + startMode, stopMode, restartMode, + matrix + } +}; diff --git a/src/NeoRuntimeManager/index.js b/src/NeoRuntimeManager/index.js deleted file mode 100644 index 5989f61..0000000 --- a/src/NeoRuntimeManager/index.js +++ /dev/null @@ -1,339 +0,0 @@ -/** - * This module is used to execute and communicate with a python NeoRuntime instance. - * - * @author jakobst1n. - * @since 19.12.2019 - */ - -const fs = require("fs"); -const fsPromises = fs.promises; -const RuntimeProcess = require("./RuntimeProcess"); -const IPC = require("./IPC"); -const logger = require(__appdir + "/src/Logger"); -const EventEmitter = require('events'); - -/** @type {object} this should be a pointer to a object referencing all neoModules (see app.js) */ -let neoModules; - -/** @type {string} Currently active mode */ -let modeId = null; -/** @type {int} Last exit code of a mode */ -let modeExitCode = 0; -/** @type {RuntimeProcess} This is the current RuntimeProcess instance */ -let runtimeProcess = null; -/** @type {IPC} The IPC instance, used to communicate with the script */ -let ipc = null; -/** @type {EventEmitter} This is used to emit events when things change */ -const eventEmitter = new EventEmitter(); -/** @type {boolean} If this is true, we will not do things the usual way */ -let modeDebuggerActive = false; -/** @type {string} Should be the modeId the debugger is attached to */ -let modeDebuggerId = null; -/** @type {object} Handler for proc:start when debugger is active */ -let modeDebuggerProcStartHandler = null; -/** @type {object} The last received matrix setup */ -let matrix = null; -/** @type {object} intervall for sending current state */ -let debugModeStateEmitIntervall = null; - -eventEmitter.on("proc:exit", (code) => modeExitCode = code); -eventEmitter.on("matrix", (_matrix) => matrix = _matrix); - -/** - * Check if a path id actually a mode (if it is a folder with a script.py file) - * - * @param {string} path - Path to check. - * - * @return {boolean} wether the path points to a valid mode. - */ -function isMode(path) { - if (!fs.existsSync(path)) { return false; } - let folderStat = fs.statSync(path); - if (!folderStat.isDirectory()) { return false; } - if (!fs.existsSync(path + "/script.py")) { return false; } - return true; -} - -/** - * Get all ids of modes that can be set. - * - * @returns {array} All modeids - */ -function listModes() { - let modeDirs = [ - ["builtin/", fs.readdirSync(__appdir + "/NeoRuntime/builtin")], - ["remote/", fs.readdirSync(__datadir + "/remoteCode")], - ["user/", fs.readdirSync(__datadir + "/userCode")] - ] - let validModes = []; - for (modeDir of modeDirs) { - for (modeName of modeDir[1]) { - let modeId = `${modeDir[0]}${modeName}`; - if (isMode(getModePath(modeId))) { - validModes.push(modeId); - } - } - } - return validModes; -} - -/** - * Change mode, stop the old one and start the new one. - * - * @param {string} _modeId - Id of the mode to change to. - * - * @return {object} A standardform return object. - */ -function setMode(_modeId) { - if (modeDebuggerActive && (_modeId != modeDebuggerId)) { - return {success: false, reason: "debugger active", detail: "Cannot change mode when debugger is active."} - } - if (!isMode(getModePath(_modeId))) { - console.log(`Invalid mode "${_modeId}".`); - return {success: false, reason: "unknown modeId"}; - } - logger.info(`Changing mode to "${_modeId}".`); - - stopMode(); - - modeId = _modeId; - neoModules.userData.config.activeMode = modeId; - eventEmitter.emit("change", "mode", modeId); - - runtimeProcess = new RuntimeProcess(getModePath(_modeId), eventEmitter); - startMode(); - - return {success: true} -}; - -/** - * Get current mode - * - * @return {string} current modeId - */ -function currentMode() { - return modeId; -} - -/** - * Will attempt to stop current mode - * - * @return {object} A standardform return object. - */ -function stopMode(restart=false) { - if (modeRunning()) { - runtimeProcess.stop(restart); - } - return {success: true} -}; - -/** - * Will attempt to start current mode - * - * @return {object} A standardform return object. - */ -function startMode() { - if (runtimeProcess === null) { - return {success: false, reason: "no runtimeprocess", detail: "Runtimeprocess not set, did you mean to call setMode?"}; - } - return runtimeProcess.start(); -}; - -/** - * Will attempt to restart current mode - * - * @return {object} A standardform return object. - */ -function restartMode() { - return stopMode(true); -}; - -/** - * Checks if mode is running currently - * - * @return {boolean} if mode is running - */ -function modeRunning() { - if (runtimeProcess === null) { return false; } - return runtimeProcess.isRunning; -}; - -/** - * Get the full system path to a mode - * - * @param {string} modeId - * - * @return {string} Full path of mode - */ -function getModePath(modeId) { - let path = modeId.split("/"); - let location = path.splice(0, 1).toString(); - if (location === "user") { path = __datadir + "/userCode/" + path.join("/"); } - if (location === "remote") { path = __datadir + "/remoteCode/" + path.join("/"); } - if (location === "builtin") { path = __appdir + "/NeoRuntime/builtin/" + path.join("/"); } - return path; -} - -/** - * Function that returns all globvars (brightness, power_on) as the values they - * had last time we heard from the python script. - * - * @return {object} - */ -function getGlobvars() { - if (!modeRunning()) { return {}; } - return ipc.globvars; -} - -/** - * Sets value of a globvar power_on/brightness. - * - * @param {string} name - Name of the variable power_on/brightness - * @param {any} value - The value the variable should be set to - * - * @return {object} Standardform return object - */ -function setGlobvar(name, value) { - if (!modeRunning()) { return; } - - switch(name) { - case "power_on": - return ipc.sendCommand(IPC.COMMAND.SET_GLOB, IPC.GLOBVAR.POWER_ON, (value) ? 1 : 0); - case "brightness": - return ipc.sendCommand(IPC.COMMAND.SET_GLOB, IPC.GLOBVAR.BRIGHTNESS, value); - default: - return {success: false, reason: "unknown globvar", detail: name}; - } -} - -/** - * Get all variables declared in mode - * - * @return {object} - */ -function getVariables() { - if (!modeRunning()) { return {}; } - return ipc.variables; -} - -/** - * Sets value of a variable - * - * @param {string} name - Name of the variable - * @param {any} value - The value the variable should be set to - * - * @return {object} Standardform return object - */ -function setVariable(name, value) { - if (!modeRunning()) { return; } - return ipc.sendCommand(IPC.COMMAND.SET_VAR, name, value); -} - -/** - * A function intented to be used in an interval to emit - * the current debug-state. - * - */ -function debugModeEmitState() { - eventEmitter.emit("debugger:state", { - mode: modeDebuggerId, - running: runtimeProcess.isRunning, - debugMode: modeDebuggerActive, - matrix: matrix - }); -} - -/** - * Start debugger for a mode - * - * @param {string} modeId - The mode to debug - * - * @return {object} Standardform return object - */ -function startDebugger(debuggerModeId) { - if (debuggerModeId.substr(0, 5) !== "user/") { return {success: false, reason: "not user mode"}; } - if (!isMode(getModePath(debuggerModeId))) { return {success: false, reason: "unknown modeId"}; } - if (modeDebuggerActive) { return {success: false, reason: "debugger already active"}; } - logger.info(`Starting debugger for ${debuggerModeId}`); - - if (modeDebuggerProcStartHandler == null) { - modeDebuggerProcStartHandler = eventEmitter.on("proc:start", () => { - setTimeout(() => { - ipc.sendCommand(IPC.COMMAND.SET_SEND_STRIP_BUF, true); - }, 2000); - }); - } else { - console.log(modeDebuggerProcStartHandler); - } - - if (debugModeStateEmitIntervall == null) { - debugModeStateEmitIntervall = setInterval(debugModeEmitState, 1000); - } - - modeDebuggerActive = true; - modeDebuggerId = debuggerModeId; - setTimeout(() => { - setMode(debuggerModeId); - }, 300); - return {success: true, code: fs.readFileSync(getModePath(debuggerModeId) + "/script.py").toString()} -} - -/** - * Save mode - */ -function saveModeCode(_modeId, code) { - if (!modeDebuggerActive) { return {success: false, reason: "debugger not active"}; }; - if (_modeId != modeDebuggerId) { return {success: false, reason: "modeid not the same as debuggermodeid"}; }; - fs.writeFileSync(getModePath(`${modeDebuggerId}/script.py`), code); - return {success: true}; -} - -/** - * Stop the active debugger - * - * @return {object} Standardform return object - */ -function stopDebugger() { - if (!modeDebuggerActive) { return {success: true, detail: "No debugger active"} } - logger.info(`Stopping debugger`); - modeDebuggerActive = false; - eventEmitter.removeAllListeners("proc:start", modeDebuggerProcStartHandler); - modeDebuggerProcStartHandler = null; - - clearInterval(debugModeStateEmitIntervall); - debugModeStateEmitIntervall = null; - - ipc.sendCommand(IPC.COMMAND.SET_SEND_STRIP_BUF, false); - return {success: true} -} - -module.exports = (_neoModules) => { - neoModules = _neoModules; - ipc = new IPC.IPC(neoModules.userData.config.neoRuntimeIPC.socketFile, eventEmitter); - return { - event: eventEmitter, - modes: listModes, - mode: { - current: currentMode, - set: (modeId) => setMode(modeId), - status: { - modeRunning: modeRunning(), - modeExitCode: modeExitCode - }, - globvars: { - get: getGlobvars, - set: setGlobvar - }, - variables: { - get: getVariables, - set: setVariable - } - }, - getModePath, - isMode, - modeRunning, - startDebugger, stopDebugger, saveModeCode, - startMode, stopMode, restartMode, - matrix - } -}; -- cgit v1.2.3