// Debug 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 radioBinPath = process.env.OP25_BIN_PATH; let radioChildProcess, tempRes, radioConfigPath; /** * 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) return res.sendStatus(200); } /** * Change the current 'cfg.json' file to the preset specified * @param {string} presetName */ 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); // 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); } return res.sendStatus(202); } /** * Open a new OP25 process tuned to the specified system */ 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; }) } /** * 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; }