Updating and streamlining radio controller side
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user