From e1c2ce648405f586e1cd89cb200a9c4d2d71a0d6 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sat, 20 May 2023 15:18:50 -0400 Subject: [PATCH] Updating and streamlining radio controller side --- Client/app.js | 5 +- Client/controllers/radioController.js | 199 ++----------------- Client/utilities/radioConfigHelper.js | 2 +- Client/utilities/utilities.js | 263 ++++++++++++++++++++++---- 4 files changed, 241 insertions(+), 228 deletions(-) diff --git a/Client/app.js b/Client/app.js index ec225fd..09ff507 100644 --- a/Client/app.js +++ b/Client/app.js @@ -12,6 +12,7 @@ var indexRouter = require('./routes/index'); var botRouter = require('./routes/bot'); var clientRouter = require('./routes/client'); var radioRouter = require('./routes/radio'); +var { attachRadioSessionToRequest } = require('./controllers/radioController'); const log = new DebugBuilder("client", "app"); @@ -33,13 +34,13 @@ app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); // Discord bot control route -app.use('/bot', botRouter); +app.use('/bot', attachRadioSessionToRequest, botRouter); // Local client control route app.use("/client", clientRouter); // Local radio controller route -app.use("/radio", radioRouter); +app.use("/radio", attachRadioSessionToRequest, radioRouter); // catch 404 and forward to error handler app.use((req, res, next) => { diff --git a/Client/controllers/radioController.js b/Client/controllers/radioController.js index 3e8b027..e3120d9 100644 --- a/Client/controllers/radioController.js +++ b/Client/controllers/radioController.js @@ -2,24 +2,18 @@ const { DebugBuilder } = require("../utilities/debugBuilder.js"); const log = new DebugBuilder("client", "radioController"); // Modules -const { resolve, dirname } = require('path'); require('dotenv').config(); -const fs = require('fs'); -const radioConfigHelper = require("../utilities/radioConfigHelper"); -const presetWrappers = require("../utilities/updatePresets"); -const { closeProcessWrapper } = require("../utilities/utilities"); -const spawn = require('child_process').spawn; -const converter = require("convert-units"); +const { closeProcessWrapper, changeCurrentConfigWrapper, openRadioSessionWrapper } = require("../utilities/utilities"); -const radioBinPath = process.env.OP25_BIN_PATH; -let radioChildProcess, tempRes, radioConfigPath; +let radioChildProcess; /** * Closes the radio executable if it's in one */ exports.closeRadioSession = async (req, res) => { - if (!radioChildProcess) return res.sendStatus(204); - radioChildProcess = await closeProcessWrapper(radioChildProcess); + if (!radioChildProcess || !req.body.radioSession) return res.sendStatus(204); + if (radioChildProcess) radioChildProcess = await closeProcessWrapper(radioChildProcess); + if (req.body.radioSession) req.body.radioSession = await closeProcessWrapper(req.body.radioSession); if (!radioChildProcess) return res.sendStatus(200); } @@ -31,7 +25,7 @@ exports.changeCurrentConfig = async (req, res) => { const presetName = req.body.presetName; if (!presetName) return res.status(500).json("You must include the preset name") - const updatedConfigObject = await this.changeCurrentConfigWrapper(presetName); + const updatedConfigObject = await changeCurrentConfigWrapper(presetName); // No change was made to the config if (!updatedConfigObject) return res.sendStatus(200); @@ -42,7 +36,7 @@ exports.changeCurrentConfig = async (req, res) => { // There was a change made to the config, reopening the radio session if it was open if (radioChildProcess) { log.DEBUG("Radio session open, restarting to accept the new config"); - const radioSessionResult = await this.openRadioSessionWrapper(radioChildProcess, presetName); + const radioSessionResult = await openRadioSessionWrapper(radioChildProcess, presetName); // throw an error to the client if the wrapper ran into an error if (typeof radioSessionResult === "string") return res.status(500).json(updatedConfigObject); @@ -58,7 +52,7 @@ exports.openRadioSession = async (req, res) => { const presetName = req.body.presetName; if(!presetName) return res.status(500).json({"message": "You must include the preset name to start the radio session with"}) - radioChildProcess = await this.openRadioSessionWrapper(radioChildProcess, presetName); + radioChildProcess = await openRadioSessionWrapper(radioChildProcess, presetName); // throw an error to the client if the wrapper ran into an error if (typeof radioSessionResult === "string") return res.status(500).json(updatedConfigObject); @@ -67,178 +61,13 @@ exports.openRadioSession = async (req, res) => { } /** - * This wrapper closes any open radio sessions and the opens a new one + * Attach the radio session to the request to be used elsewhere * - * @returns {radioChildProcess} The process of the radio session for use + * @param {*} req + * @param {*} res */ -exports.openRadioSessionWrapper = async (radioChildProcess, presetName) => { - if (radioChildProcess) radioChildProcess = await closeProcessWrapper(radioChildProcess); - - const configChangeResult = await this.changeCurrentConfigWrapper(presetName); - - // Throw an error to the client if the config change ran into an error - if (typeof configChangeResult === "string") return configChangeResult; - - if (process.platform === "win32") { - log.DEBUG("Starting Windows OP25"); - radioChildProcess = await spawn("C:\\Python310\\python.exe", [getRadioBinPath(), "-c", getRadioConfigPath()], { cwd: dirname(getRadioBinPath()) }); - } - else { - log.DEBUG("Starting Linux OP25"); - radioChildProcess = await spawn(getRadioBinPath(), ["-c", getRadioConfigPath()], { cwd: dirname(getRadioBinPath()) }); - } - - log.VERBOSE("Radio Process: ", radioChildProcess); - - let fullOutput; - radioChildProcess.stdout.setEncoding('utf8'); - radioChildProcess.stdout.on("data", (data) => { - log.VERBOSE("From Process: ", data); - fullOutput += data.toString(); - }); - - radioChildProcess.stderr.on('data', (data) => { - log.VERBOSE(`stderr: ${data}`); - fullOutput += data.toString(); - }); - - radioChildProcess.on('close', (code) => { - log.DEBUG(`child process exited with code ${code}`); - log.VERBOSE("Full output from radio: ", fullOutput); - }); - - radioChildProcess.on("error", (code, signal) => { - log.ERROR("Error from the radio process: ", code, signal); - }); - - // Starting the radio application - - return radioChildProcess -} - -/** - * - * @param {*} presetName - * @returns - */ -exports.changeCurrentConfigWrapper = async (presetName) => { - // Check if the given config is saved - log.DEBUG("Checking if provided preset is in the config"); - const presetIsPresent = await checkIfPresetExists(presetName); - if (!presetIsPresent) return "No preset with given name found in config"; // No preset with the given name is in the config - - // Check if the current config is the same as the preset given - try { - const currentConfig = readOP25Config(); - if (currentConfig.channels && currentConfig.channels.name === presetName) { - log.DEBUG("Current config is the same as the preset given"); - return undefined; - } - } - catch (err) { - log.WARN("Problem reading the config file, overwriting with the new config", err); - } - - // Convert radioPreset to OP25 'cfg.json. file - log.DEBUG("Converting radioPreset to OP25 config"); - const updatedConfigObject = convertRadioPresetsToOP25Config(presetName); - - // Replace current JSON file with the updated file - writeOP25Config(updatedConfigObject, () => { - return updatedConfigObject; - }) - -} - -/** - * Get the location of the 'multi_rx.py' binary from the config - */ -function getRadioBinPath(){ - return resolve(radioBinPath); -} - -/** - * Write the given config to the JSON file in OP25 the bin dir - * @param config The full config to be written to the file - * @param {function} callback The function to be called when this wrapper completes - */ -function writeOP25Config(config, callback = undefined) { - log.DEBUG("Updating OP25 config with: ", config); - fs.writeFile(getRadioConfigPath(), JSON.stringify(config), (err) => { - // Error checking - if (err) { - log.ERROR(err); - throw err; - } - log.DEBUG("Write Complete"); - if (callback) callback() - }); -} - -/** - * Get the current config file in use by OP25 - * @returns {object|*} The parsed config object currently set in OP25 - */ -function readOP25Config() { - const configPath = getRadioConfigPath(); - log.DEBUG(`Reading from config path: '${configPath}'`); - const readFile = fs.readFileSync(configPath); - log.VERBOSE("File Contents: ", readFile.toString()); - return JSON.parse(readFile); -} - -/** - * Get the path of the config for the radio app (OP25) and set the global variable - */ -function getRadioConfigPath(){ - let radioConfigDirPath = dirname(getRadioBinPath()); - return resolve(`${radioConfigDirPath}/cfg.json`); -} - -/** - * Check to see if the preset name exists in the config - * @param {string} presetName The system name as saved in the preset - * @returns {true||false} - */ -function checkIfPresetExists(presetName) { - const savedPresets = presetWrappers.getPresets(); - log.DEBUG("Found presets: ", savedPresets, presetName, Object.keys(savedPresets).includes(presetName)); - if (!Object.keys(savedPresets).includes(presetName)) return false; - else return true; -} - -/** - * Convert a radioPreset to OP25's cfg.json file - */ -function convertRadioPresetsToOP25Config(presetName){ - const savedPresets = presetWrappers.getPresets(); - let frequencyString = ""; - for (const frequency of savedPresets[presetName].frequencies){ - frequencyString += `${converter(frequency).from("Hz").to("MHz")},` - } - frequencyString = frequencyString.slice(0, -1); - - let updatedOP25Config; - switch (savedPresets[presetName].mode){ - case "p25": - updatedOP25Config = new radioConfigHelper.P25({ - "systemName": presetName, - "controlChannelsString": frequencyString, - "tagsFile": savedPresets[presetName].trunkFile - }); - break; - case "nbfm": - //code for nbfm here - updatedOP25Config = new radioConfigHelper.NBFM({ - "frequency": frequencyString, - "systemName": presetName - }); - break; - default: - throw new Error("Radio mode of selected preset not recognized"); - } - - log.DEBUG(updatedOP25Config); - return updatedOP25Config; +exports.attachRadioSessionToRequest = async (req, res, next) => { + req.body.radioSession = radioChildProcess; + next(); } diff --git a/Client/utilities/radioConfigHelper.js b/Client/utilities/radioConfigHelper.js index a843662..e17f408 100644 --- a/Client/utilities/radioConfigHelper.js +++ b/Client/utilities/radioConfigHelper.js @@ -134,7 +134,7 @@ class audioConfig { "instance_name": "audio0", "device_name": deviceName, "udp_port": port, - "audio_gain": 1.0, + "audio_gain": 2.0, "number_channels": 1 }]; } diff --git a/Client/utilities/utilities.js b/Client/utilities/utilities.js index 12b86fc..3009b73 100644 --- a/Client/utilities/utilities.js +++ b/Client/utilities/utilities.js @@ -1,34 +1,44 @@ // Modules const { promisify } = require('util'); -const { exec } = require("child_process"); +const { exec, spawn } = require("child_process"); +const { resolve, dirname } = require('path'); +const radioConfigHelper = require("../utilities/radioConfigHelper"); +const presetWrappers = require("../utilities/updatePresets"); +const converter = require("convert-units"); +const fs = require('fs'); +require('dotenv').config(); // Debug const { DebugBuilder } = require("../utilities/debugBuilder.js"); // Global Vars const log = new DebugBuilder("client", "executeConsoleCommands"); const execCommand = promisify(exec); - +const radioBinPath = process.env.OP25_BIN_PATH; /** - * - * @param {*} process The process to close - * @returns {undefined} Undefined to replace the existing process in the parent + * An object containing the variables needed to run the local node */ -exports.closeProcessWrapper = async (process) => { - log.INFO("Leaving the server"); - if (!process) return undefined; - - // Try to close the process gracefully - await process.kill(2); - - // Wait 25 seconds and see if the process is still open, if it is force it close - await setTimeout(async () => { - if (process) await process.kill(9); - }, 25000) - - return undefined; +exports.nodeObject = class nodeObject { + /** + * + * @param {*} param0._id The ID of the node + * @param {*} param0._name The name of the node + * @param {*} param0._ip The IP that the master can contact the node at + * @param {*} param0._port The port that the client is listening on + * @param {*} param0._location The physical location of the node + * @param {*} param0._online An integer representation of the online status of the bot, ie 0=off, 1=on + * @param {*} param0._nearbySystems An object array of nearby systems + */ + constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null, _online = null }) { + this.id = _id; + this.name = _name; + this.ip = _ip; + this.port = _port; + this.location = _location; + this.nearbySystems = _nearbySystems; + this.online = _online; + } } - /** * * @param {*} consoleCommand @@ -53,29 +63,202 @@ exports.executeAsyncConsoleCommand = async function executeAsyncConsoleCommand(c return output; } + /** * + * @param {*} process The process to close + * @returns {undefined} Undefined to replace the existing process in the parent */ -class nodeObject { - /** - * - * @param {*} param0._id The ID of the node - * @param {*} param0._name The name of the node - * @param {*} param0._ip The IP that the master can contact the node at - * @param {*} param0._port The port that the client is listening on - * @param {*} param0._location The physical location of the node - * @param {*} param0._online An integer representation of the online status of the bot, ie 0=off, 1=on - * @param {*} param0._nearbySystems An object array of nearby systems - */ - constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null, _online = null }) { - this.id = _id; - this.name = _name; - this.ip = _ip; - this.port = _port; - this.location = _location; - this.nearbySystems = _nearbySystems; - this.online = _online; - } +exports.closeProcessWrapper = async (process) => { + log.INFO("Leaving the server"); + if (!process) return undefined; + + // Try to close the process gracefully + await process.kill(2); + + // Wait 25 seconds and see if the process is still open, if it is force it close + await setTimeout(async () => { + if (process) await process.kill(9); + }, 25000) + + return undefined; } -exports.nodeObject = nodeObject; \ No newline at end of file +/** + * This wrapper closes any open radio sessions and the opens a new one + * + * @returns {radioChildProcess} The process of the radio session for use + */ +exports.openRadioSessionWrapper = async (radioChildProcess, presetName) => { + if (radioChildProcess) radioChildProcess = await this.closeProcessWrapper(radioChildProcess); + + const configChangeResult = await this.changeCurrentConfigWrapper(presetName); + + // Throw an error to the client if the config change ran into an error + if (typeof configChangeResult === "string") return configChangeResult; + + if (process.platform === "win32") { + log.DEBUG("Starting Windows OP25"); + radioChildProcess = await spawn("C:\\Python310\\python.exe", [getRadioBinPath(), "-c", getRadioConfigPath()], { cwd: dirname(getRadioBinPath()) }); + } + else { + log.DEBUG("Starting Linux OP25"); + radioChildProcess = await spawn(getRadioBinPath(), ["-c", getRadioConfigPath()], { cwd: dirname(getRadioBinPath()) }); + } + + log.VERBOSE("Radio Process: ", radioChildProcess); + + let fullOutput; + radioChildProcess.stdout.setEncoding('utf8'); + radioChildProcess.stdout.on("data", (data) => { + log.VERBOSE("From Process: ", data); + fullOutput += data.toString(); + }); + + radioChildProcess.stderr.on('data', (data) => { + log.VERBOSE(`stderr: ${data}`); + fullOutput += data.toString(); + }); + + radioChildProcess.on('close', (code) => { + log.DEBUG(`child process exited with code ${code}`); + log.VERBOSE("Full output from radio: ", fullOutput); + }); + + radioChildProcess.on("error", (code, signal) => { + log.ERROR("Error from the radio process: ", code, signal); + }); + + // Starting the radio application + + return radioChildProcess +} + +/** + * Update the OP25 config with a preset + * + * @param {*} presetName The preset name to update the OP25 config file with + * @returns + */ +exports.changeCurrentConfigWrapper = async (presetName) => { + // Check if the given config is saved + log.DEBUG("Checking if provided preset is in the config"); + const presetIsPresent = await checkIfPresetExists(presetName); + if (!presetIsPresent) return "No preset with given name found in config"; // No preset with the given name is in the config + + // Check if the current config is the same as the preset given + try { + const currentConfig = readOP25Config(); + if (currentConfig.channels && currentConfig.channels.name === presetName) { + log.DEBUG("Current config is the same as the preset given"); + return undefined; + } + } + catch (err) { + log.WARN("Problem reading the config file, overwriting with the new config", err); + } + + // Convert radioPreset to OP25 'cfg.json. file + log.DEBUG("Converting radioPreset to OP25 config"); + const updatedConfigObject = convertRadioPresetsToOP25Config(presetName); + + // Replace current JSON file with the updated file + writeOP25Config(updatedConfigObject, () => { + return updatedConfigObject; + }) + +} + +/** + * Get the location of the 'multi_rx.py' binary from the config + */ +function getRadioBinPath(){ + return resolve(radioBinPath); +} + +/** + * Get the path of the config for the radio app (OP25) and set the global variable + */ +function getRadioConfigPath(){ + let radioConfigDirPath = dirname(getRadioBinPath()); + return resolve(`${radioConfigDirPath}/cfg.json`); +} + +/** + * Write the given config to the JSON file in OP25 the bin dir + * @param config The full config to be written to the file + * @param {function} callback The function to be called when this wrapper completes + */ +function writeOP25Config(config, callback = undefined) { + log.DEBUG("Updating OP25 config with: ", config); + fs.writeFile(getRadioConfigPath(), JSON.stringify(config), (err) => { + // Error checking + if (err) { + log.ERROR(err); + throw err; + } + log.DEBUG("Write Complete"); + if (callback) callback() + }); +} + +/** + * Get the current config file in use by OP25 + * @returns {object|*} The parsed config object currently set in OP25 + */ +function readOP25Config() { + const configPath = getRadioConfigPath(); + log.DEBUG(`Reading from config path: '${configPath}'`); + const readFile = fs.readFileSync(configPath); + log.VERBOSE("File Contents: ", readFile.toString()); + return JSON.parse(readFile); +} + + + +/** + * Check to see if the preset name exists in the config + * @param {string} presetName The system name as saved in the preset + * @returns {true||false} + */ +function checkIfPresetExists(presetName) { + const savedPresets = presetWrappers.getPresets(); + log.DEBUG("Found presets: ", savedPresets, presetName, Object.keys(savedPresets).includes(presetName)); + if (!Object.keys(savedPresets).includes(presetName)) return false; + else return true; +} + +/** + * Convert a radioPreset to OP25's cfg.json file + */ +function convertRadioPresetsToOP25Config(presetName){ + const savedPresets = presetWrappers.getPresets(); + let frequencyString = ""; + for (const frequency of savedPresets[presetName].frequencies){ + frequencyString += `${converter(frequency).from("Hz").to("MHz")},` + } + frequencyString = frequencyString.slice(0, -1); + + let updatedOP25Config; + switch (savedPresets[presetName].mode){ + case "p25": + updatedOP25Config = new radioConfigHelper.P25({ + "systemName": presetName, + "controlChannelsString": frequencyString, + "tagsFile": savedPresets[presetName].trunkFile + }); + break; + case "nbfm": + //code for nbfm here + updatedOP25Config = new radioConfigHelper.NBFM({ + "frequency": frequencyString, + "systemName": presetName + }); + break; + default: + throw new Error("Radio mode of selected preset not recognized"); + } + + log.DEBUG(updatedOP25Config); + return updatedOP25Config; +} \ No newline at end of file