8 Commits

26 changed files with 183 additions and 104 deletions

View File

@@ -1,86 +0,0 @@
import { DebugBuilder } from "../modules/debugger.mjs";
const log = new DebugBuilder("client", "op25Handler");
import { P25ConfigGenerator, NBFMConfigGenerator } from './modules/op25ConfigGenerators.mjs';
import { getAllPresets } from '../modules/radioPresetHandler.mjs';
import { startService, stopService } from '../modules/serviceHandler.mjs';
import dotenv from 'dotenv';
dotenv.config()
let currentSystem = undefined;
/**
* Creates configuration based on the preset and restarts the OP25 service.
* @param {Object} preset The preset object containing system configuration.
* @returns {Promise<void>}
*/
const createConfigAndRestartService = async (systemName, preset) => {
const { mode, frequencies, trunkFile, whitelistFile } = preset;
let generator;
if (mode === 'p25') {
log.INFO("Using P25 Config Generator based on preset mode", systemName, mode);
generator = new P25ConfigGenerator({
systemName,
controlChannels: frequencies,
tagsFile: trunkFile,
whitelistFile: whitelistFile !== 'none' ? whitelistFile : undefined
});
} else if (mode === 'nbfm') {
log.INFO("Using NBFM Config Generator based on preset mode", systemName, mode);
generator = new NBFMConfigGenerator({
systemName,
frequencies,
tagsFile: trunkFile
});
} else {
throw new Error(`Unsupported mode: ${mode}`);
}
const op25FilePath = process.env.OP25_FULL_PATH || './'; // Default to current directory if OP25_FULL_PATH is not set
const op25ConfigPath = `${op25FilePath}${op25FilePath.endsWith('/') ? 'active.cfg.json' : '/active.cfg.json'}`;
await generator.exportToFile(op25ConfigPath);
// Restart the service
await stopService('op25-multi_rx');
await startService('op25-multi_rx');
};
/**
* Opens the OP25 service for the specified system.
* @param {string} systemName The name of the system to open.
* @returns {Promise<void>}
*/
export const openOP25 = async (systemName) => {
currentSystem = systemName;
// Retrieve preset for the specified system name
const presets = await getAllPresets();
const preset = presets[systemName];
log.INFO("Found preset:", preset);
if (!preset) {
throw new Error(`Preset for system "${systemName}" not found.`);
}
await createConfigAndRestartService(systemName, preset);
};
/**
* Closes the OP25 service.
* @returns {Promise<void>}
*/
export const closeOP25 = async () => {
currentSystem = undefined;
await stopService('op25-multi_rx');
};
/**
* Gets the current system.
* @returns {Promise<string | undefined>} The name of the current system.
*/
export const getCurrentSystem = async () => {
return currentSystem;
};

View File

@@ -2,7 +2,7 @@
"name": "drb-client", "name": "drb-client",
"version": "3.0.0", "version": "3.0.0",
"description": "", "description": "",
"main": "client.js", "main": "src/client.js",
"scripts": { "scripts": {
"test": "mocha --timeout 10000" "test": "mocha --timeout 10000"
}, },
@@ -12,6 +12,7 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@discordjs/voice": "^0.17.0", "@discordjs/voice": "^0.17.0",
"axios": "^1.7.7",
"convert-units": "^2.3.4", "convert-units": "^2.3.4",
"discord.js": "^14.15.3", "discord.js": "^14.15.3",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",

View File

@@ -94,8 +94,8 @@ rm -rf /usr/lib/python3.11/EXTERNALLY-MANAGED
# Getting the Python DAB # Getting the Python DAB
echo "Installing PDAB and Dependencies" echo "Installing PDAB and Dependencies"
git clone -b DRBv3 https://git.vpn.cusano.net/logan/Python-Discord-Audio-Bot.git ./discordAudioBot/pdab git clone -b DRBv3 https://git.vpn.cusano.net/logan/Python-Discord-Audio-Bot.git ./pdab
pip3 install -r ./discordAudioBot/pdab/requirements.txt pip3 install -r ./pdab/requirements.txt
# Create a systemd service file for the DRB Client # Create a systemd service file for the DRB Client
echo "Adding DRB Node service..." echo "Adding DRB Node service..."

View File

@@ -25,7 +25,7 @@ let botCallback;
export const initDiscordBotClient = (clientId, callback, runPDAB = true) => { export const initDiscordBotClient = (clientId, callback, runPDAB = true) => {
botCallback = callback; botCallback = callback;
if (runPDAB) launchProcess("python", [join(__dirname, "./pdab/main.py"), process.env.AUDIO_DEVICE_ID, clientId, port], false, false, join(__dirname, "./pdab")); if (runPDAB) launchProcess("python", [join(__dirname, "../../pdab/main.py"), process.env.AUDIO_DEVICE_ID, clientId, port], false, false, join(__dirname, "../../pdab"));
pdabProcess = true; // TODO - Make this more dynamic pdabProcess = true; // TODO - Make this more dynamic
} }

View File

@@ -1,8 +1,8 @@
import { DebugBuilder } from "./debugger.mjs"; import { DebugBuilder } from "./debugger.mjs";
const log = new DebugBuilder("client", "selfUpdater"); const log = new DebugBuilder("client", "selfUpdater");
import simpleGit from 'simple-git'; import simpleGit from 'simple-git';
import { restartService } from './serviceHandler.mjs' import { restartService } from './serviceHandler.mjs';
import { launchProcess } from './subprocessHandler.mjs' import { launchProcess } from './subprocessHandler.mjs';
const git = simpleGit(); const git = simpleGit();
@@ -12,27 +12,31 @@ export const checkForUpdates = async () => {
// Fetch remote changes // Fetch remote changes
await git.fetch(); await git.fetch();
// Get the latest commit hash // Get the current branch
const latestCommitHash = await git.revparse(['@{u}']); const currentBranch = await git.revparse(['--abbrev-ref', 'HEAD']);
log.INFO(`Current branch is ${currentBranch}`);
// Get the latest commit hash for the current branch
const latestCommitHash = await git.revparse([`${currentBranch}@{u}`]);
// Compare with the local commit hash // Compare with the local commit hash
const localCommitHash = await git.revparse(['HEAD']); const localCommitHash = await git.revparse(['HEAD']);
if (latestCommitHash !== localCommitHash) { if (latestCommitHash !== localCommitHash) {
log.INFO('An update is available. Updating...'); log.INFO(`An update is available on branch ${currentBranch}. Updating...`);
// Check if there have been any changes to the code // Check if there have been any changes to the code
const gitStatus = await git.status() const gitStatus = await git.status();
log.INFO(gitStatus); log.INFO(gitStatus);
if (gitStatus.modified.length > 0){ if (gitStatus.modified.length > 0){
// There is locally modified code // There is locally modified code
log.INFO("There is locally modified code, resetting..."); log.INFO("There is locally modified code, stashing changes...");
await git.stash(); await git.stash();
await git.reset('hard', ['origin/master']);
} }
// Pull the latest changes from the remote repository // Ensure we are on the correct branch and pull the latest changes
await git.pull(); await git.checkout(currentBranch);
await git.pull('origin', currentBranch);
// Run the post-update script // Run the post-update script
log.INFO('Running post-update script...'); log.INFO('Running post-update script...');
@@ -42,10 +46,10 @@ export const checkForUpdates = async () => {
log.INFO('Update completed successfully. Restarting the application...'); log.INFO('Update completed successfully. Restarting the application...');
restartApplication(); restartApplication();
return true return true;
} else { } else {
log.INFO('The application is up to date.'); log.INFO('The application is up to date.');
return false return false;
} }
} catch (error) { } catch (error) {
log.ERROR('Error checking for updates:', error); log.ERROR('Error checking for updates:', error);
@@ -56,4 +60,4 @@ export const checkForUpdates = async () => {
export const restartApplication = () => { export const restartApplication = () => {
log.INFO('Restarting the application...'); log.INFO('Restarting the application...');
restartService('discord-radio-bot'); restartService('discord-radio-bot');
} };

View File

@@ -0,0 +1,160 @@
import { DebugBuilder } from "../modules/debugger.mjs";
const log = new DebugBuilder("client", "op25Handler");
import { P25ConfigGenerator, NBFMConfigGenerator } from './modules/op25ConfigGenerators.mjs';
import { getAllPresets } from '../modules/radioPresetHandler.mjs';
import { startService, stopService } from '../modules/serviceHandler.mjs';
import axios from 'axios'; // Import axios for HTTP requests
import dotenv from 'dotenv';
dotenv.config();
let currentSystem = undefined;
let crashDetectionInterval; // Variable to store the crash detection interval ID
// Sleep utility to add delays between retries
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
/**
* Checks the health of the OP25 web portal by making an HTTP HEAD request.
* If the portal does not respond or there is an issue, retries a specified number of times.
* If all retry attempts fail, it restarts the OP25 service.
*
* @async
* @function checkServiceHealth
* @returns {Promise<void>} Resolves if the web portal is healthy or after the restart process is triggered.
* @throws Will log errors related to the health check or service restart.
*/
const checkServiceHealth = async () => {
try {
log.INFO("Checking OP25 web portal health...");
// Perform an HTTP HEAD request to the web portal with a 5-second timeout
await axios({ method: "get", url: 'http://localhost:8081', timeout: 5000 });
log.INFO("Web portal is healthy.");
} catch (error) {
if (error.code === 'ECONNABORTED') {
log.ERROR("Request timed out. The web portal took too long to respond.");
} else if (error.response) {
log.ERROR(`Web portal responded with status ${error.response.status}: ${error.response.statusText}`);
} else if (error.request) {
log.ERROR("No response received from web portal.");
} else {
log.ERROR(`Unexpected error occurred: ${error.message}`);
}
// Retry mechanism
const retryAttempts = 3;
const delayBetweenRetries = 3000; // 3 seconds delay
for (let i = 1; i <= retryAttempts; i++) {
log.INFO(`Retrying to check web portal health... Attempt ${i}/${retryAttempts}`);
try {
await sleep(delayBetweenRetries); // Add delay before retrying
await axios({ method: "get", url: 'http://localhost:8081', timeout: 5000 });
log.INFO("Web portal is healthy on retry.");
return;
} catch (retryError) {
log.ERROR(`Retry ${i} failed: ${retryError.message}`);
if (i === retryAttempts) {
log.ERROR("All retry attempts failed. Restarting the service...");
await restartOp25();
}
}
}
}
};
/**
* Creates configuration based on the preset and restarts the OP25 service.
* @param {Object} preset The preset object containing system configuration.
* @returns {Promise<void>}
*/
const createConfigAndRestartService = async (systemName, preset) => {
const { mode, frequencies, trunkFile, whitelistFile } = preset;
let generator;
if (mode === 'p25') {
log.INFO("Using P25 Config Generator based on preset mode", systemName, mode);
generator = new P25ConfigGenerator({
systemName,
controlChannels: frequencies,
tagsFile: trunkFile,
whitelistFile: whitelistFile !== 'none' ? whitelistFile : undefined
});
} else if (mode === 'nbfm') {
log.INFO("Using NBFM Config Generator based on preset mode", systemName, mode);
generator = new NBFMConfigGenerator({
systemName,
frequencies,
tagsFile: trunkFile
});
} else {
throw new Error(`Unsupported mode: ${mode}`);
}
const op25FilePath = process.env.OP25_FULL_PATH || './'; // Default to current directory if OP25_FULL_PATH is not set
const op25ConfigPath = `${op25FilePath}${op25FilePath.endsWith('/') ? 'active.cfg.json' : '/active.cfg.json'}`;
await generator.exportToFile(op25ConfigPath);
await restartOp25();
};
/**
* Opens the OP25 service for the specified system.
* @param {string} systemName The name of the system to open.
* @returns {Promise<void>}
*/
export const openOP25 = async (systemName) => {
currentSystem = systemName;
// Retrieve preset for the specified system name
const presets = await getAllPresets();
const preset = presets[systemName];
log.INFO("Found preset:", preset);
if (!preset) {
throw new Error(`Preset for system "${systemName}" not found.`);
}
await createConfigAndRestartService(systemName, preset);
// Start OP25 crash detection
if (!crashDetectionInterval) {
crashDetectionInterval = setInterval(checkServiceHealth, 30000); // Check every 30 seconds
log.INFO("Started crash detection.");
}
};
/**
* Restarts the OP25 service without changing the config.
* @returns {Promise<void>}
*/
export const restartOp25 = async () => {
// Restart the service
await stopService('op25-multi_rx');
await startService('op25-multi_rx');
}
/**
* Closes the OP25 service.
* @returns {Promise<void>}
*/
export const closeOP25 = async () => {
currentSystem = undefined;
await stopService('op25-multi_rx');
// Stop crash detection
if (crashDetectionInterval) {
clearInterval(crashDetectionInterval);
crashDetectionInterval = null;
log.INFO("Stopped crash detection.");
}
};
/**
* Gets the current system.
* @returns {Promise<string | undefined>} The name of the current system.
*/
export const getCurrentSystem = async () => {
return currentSystem;
};