// Modules const { promisify } = require('util'); 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; /** * An object containing the variables needed to run the local node */ 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._nearbySystems An object array of nearby systems */ constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null }) { this.id = _id; this.name = _name; this.ip = _ip; this.port = _port; this.location = _location; this.nearbySystems = _nearbySystems; } } /** * * @param {*} consoleCommand * @returns */ exports.executeAsyncConsoleCommand = async function executeAsyncConsoleCommand(consoleCommand) { // Check to see if the command is a real command // TODO needs to be improved const acceptableCommands = [ "arecord -L", 'ipconfig', 'ip addr' ]; if (!acceptableCommands.includes(consoleCommand)) { log.WARN("Console command is not acceptable: ", consoleCommand); return undefined; } log.DEBUG("Running console command: ", consoleCommand); const tempOutput = await execCommand(consoleCommand); const output = tempOutput.stdout.trim(); log.DEBUG("Executed Console Command Response: ", output) // TODO add some error checking return output; } /** * * @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; } /** * 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; } // Convert a buffer from the DB to JSON object exports.BufferToJson = (buffer) => { return JSON.parse(buffer.toString()); } /** * Check to see if the input is a valid JSON string * * @param {*} str The string to check for valud JSON * @returns {true|false} */ exports.isJsonString = (str) => { try { JSON.parse(str); } catch (e) { return false; } return true; }