diff --git a/.gitignore b/.gitignore index 83af53e..f3eefe5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ node_modules/ *.log *.txt *.env -!requirements.txt \ No newline at end of file +!requirements.txt +*testOP25Dir/ \ No newline at end of file diff --git a/Client/PDAB b/Client/PDAB new file mode 160000 index 0000000..51027d7 --- /dev/null +++ b/Client/PDAB @@ -0,0 +1 @@ +Subproject commit 51027d794d02a08285921f8d64b42f03a723b017 diff --git a/Client/controllers/botController.js b/Client/controllers/botController.js index 7050977..6724cf4 100644 --- a/Client/controllers/botController.js +++ b/Client/controllers/botController.js @@ -4,6 +4,7 @@ const log = new DebugBuilder("client", "clientController"); const spawn = require('child_process').spawn; const { resolve } = require("path"); +const { closeProcessWrapper } = require("../utilities/utilities"); // Global vars let pythonProcess; @@ -75,11 +76,7 @@ exports.leaveServer = async (req, res) => { log.INFO("Leaving the server"); if (!pythonProcess) return res.sendStatus(200) - await pythonProcess.kill(2); - await setTimeout(async () => { - if (pythonProcess) await pythonProcess.kill(9); - }, 25000) - pythonProcess = undefined; + pythonProcess = await closeProcessWrapper(pythonProcess); return res.sendStatus(202); } \ No newline at end of file diff --git a/Client/controllers/radioController.js b/Client/controllers/radioController.js index 9a7816a..3e8b027 100644 --- a/Client/controllers/radioController.js +++ b/Client/controllers/radioController.js @@ -7,6 +7,7 @@ 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"); @@ -16,45 +17,137 @@ let radioChildProcess, tempRes, radioConfigPath; /** * Closes the radio executable if it's in one */ -exports.closeRadioSession = (req, res) => { - if (!radioChildProcess) return res.sendStatus(200) - tempRes = res; - radioChildProcess.kill(); - radioChildProcess = undefined; +exports.closeRadioSession = async (req, res) => { + if (!radioChildProcess) return res.sendStatus(204); + radioChildProcess = await closeProcessWrapper(radioChildProcess); + if (!radioChildProcess) return res.sendStatus(200); } /** * Change the current 'cfg.json' file to the preset specified * @param {string} presetName */ -exports.changeCurrentConfig = (req, res) => { - // Check if the given config is saved - log.DEBUG("[/radio/changeCurrentConfig] - Checking if provided preset is in the config"); - if (!checkIfPresetExists(req.body.presetName)) return res.status(500).JSON("No preset with given name found in config"); // No preset with the given name is in the config +exports.changeCurrentConfig = async (req, res) => { + const presetName = req.body.presetName; + if (!presetName) return res.status(500).json("You must include the preset name") - // Check if the current config is the same as the preset given - const currentConfig = readOP25Config(); - if (currentConfig.channels && currentConfig.channels.name === req.body.presetName) { - log.DEBUG("[/radio/changeCurrentConfig] - Current config is the same as the preset given"); - return res.sendStatus(202); + const updatedConfigObject = await this.changeCurrentConfigWrapper(presetName); + + // No change was made to the config + if (!updatedConfigObject) return res.sendStatus(200); + + // Error was encountered + if (typeof updatedConfigObject === "string") return res.status(500).json(updatedConfigObject); + + // 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); + + // throw an error to the client if the wrapper ran into an error + if (typeof radioSessionResult === "string") return res.status(500).json(updatedConfigObject); } - - // Convert radioPreset to OP25 'cfg.json. file - log.DEBUG("[/radio/changeCurrentConfig] - Converting radioPreset to OP25 config"); - const updatedConfigObject = convertRadioPresetsToOP25Config(req.body.presetName); - - // Replace current JSON file with the updated file - writeOP25Config(updatedConfigObject, () => { - res.sendStatus(200); - }) + + return res.sendStatus(202); } /** * Open a new OP25 process tuned to the specified system */ -exports.openRadioSession = () => { - if (radioChildProcess) closeRadioSession(); - radioChildProcess = spawn(getRadioBinPath()); +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); + + // throw an error to the client if the wrapper ran into an error + if (typeof radioSessionResult === "string") return res.status(500).json(updatedConfigObject); + + return res.sendStatus(200); +} + +/** + * 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 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; + }) + } /** @@ -88,8 +181,10 @@ function writeOP25Config(config, callback = undefined) { */ function readOP25Config() { const configPath = getRadioConfigPath(); - log.DEBUG(`Reading from config path: '${configPath}'`); - return JSON.parse(fs.readFileSync(configPath)); + log.DEBUG(`Reading from config path: '${configPath}'`); + const readFile = fs.readFileSync(configPath); + log.VERBOSE("File Contents: ", readFile.toString()); + return JSON.parse(readFile); } /** @@ -107,6 +202,7 @@ function getRadioConfigPath(){ */ 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; } diff --git a/Client/routes/radio.js b/Client/routes/radio.js index c5f10ac..73d7241 100644 --- a/Client/routes/radio.js +++ b/Client/routes/radio.js @@ -15,6 +15,7 @@ router.post('/start', radioController.openRadioSession); /** * POST Close the current radio session + * Response from the radio: 200: closed; 204: not connected */ router.post('/stop', radioController.closeRadioSession); diff --git a/Client/utilities/utilities.js b/Client/utilities/utilities.js index c5879f6..12b86fc 100644 --- a/Client/utilities/utilities.js +++ b/Client/utilities/utilities.js @@ -8,6 +8,32 @@ const log = new DebugBuilder("client", "executeConsoleCommands"); const execCommand = promisify(exec); +/** + * + * @param {*} process The process to close + * @returns {undefined} Undefined to replace the existing process in the parent + */ +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; +} + + +/** + * + * @param {*} consoleCommand + * @returns + */ exports.executeAsyncConsoleCommand = async function executeAsyncConsoleCommand(consoleCommand) { // Check to see if the command is a real command // TODO needs to be improved