Inital move (minus WIP tests)
This commit is contained in:
301
.gitignore
vendored
Normal file
301
.gitignore
vendored
Normal file
@@ -0,0 +1,301 @@
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
/.vscode
|
||||
|
||||
# Ignore the config dirs
|
||||
config/
|
||||
|
||||
# Ignore the OP25 directory we will create
|
||||
op25/
|
||||
47
client.js
Normal file
47
client.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { generateUniqueID } from './modules/baseUtils.mjs';
|
||||
import { updateId } from './modules/updateConfig.mjs';
|
||||
import { ClientNodeConfig } from './modules/clientObjectDefinitions.mjs';
|
||||
import { initSocketConnection } from './modules/socketClient.mjs';
|
||||
import { checkForUpdates } from './modules/selfUpdater.mjs'
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
var localNodeConfig = new ClientNodeConfig({})
|
||||
|
||||
async function boot() {
|
||||
// Check if there have been any updates
|
||||
await checkForUpdates();
|
||||
|
||||
if (localNodeConfig.node.nuid === undefined || localNodeConfig.node.nuid === '' || localNodeConfig.node.nuid === '0' || localNodeConfig.node.nuid === 0) {
|
||||
// Run the first time boot sequence
|
||||
await firstTimeBoot();
|
||||
}
|
||||
|
||||
// Initialize the socket connection with the server
|
||||
return initSocketConnection(localNodeConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the first time the client boots on a pc
|
||||
* @returns {any}
|
||||
*/
|
||||
async function firstTimeBoot() {
|
||||
// Generate a new ID for the node
|
||||
localNodeConfig.node.nuid = await generateUniqueID();
|
||||
console.log(`Generated a new unique ID for this node: '${localNodeConfig.node.nuid}'`);
|
||||
|
||||
// Update the config with the new ID
|
||||
await updateId(localNodeConfig.node.nuid);
|
||||
console.log("Updated the config with the new node ID");
|
||||
// TODO - Create the config file with the ID given and replace the update above
|
||||
// TODO - Implement web server so users can update radio systems easily
|
||||
// TODO - Implement logic to check if the presets are set
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Boot the client application
|
||||
boot().then((openSocket) => {
|
||||
console.log(openSocket, "Booted Sucessfully");
|
||||
})
|
||||
138
discordAudioBot/pdabHandler.mjs
Normal file
138
discordAudioBot/pdabHandler.mjs
Normal file
@@ -0,0 +1,138 @@
|
||||
// server.js
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import { launchProcess } from '../modules/subprocessHandler.mjs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server);
|
||||
let pdabProcess = false;
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
let botCallback;
|
||||
|
||||
export const initDiscordBotClient = (clientId, callback, runPDAB = true) => {
|
||||
botCallback = callback;
|
||||
|
||||
if (runPDAB) launchProcess("python", [join(__dirname, "./pdab/main.py"), process.env.AUDIO_DEVICE_ID, clientId, port], false, join(__dirname, "./pdab"));
|
||||
pdabProcess = true; // TODO - Make this more dynamic
|
||||
}
|
||||
|
||||
export const startPdabSocketServer = () => {
|
||||
const port = process.env.PDAB_PORT || 3000;
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log('A user connected');
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('User disconnected');
|
||||
});
|
||||
|
||||
// Listen for the discord client ready event
|
||||
socket.on('discord_ready', (message) => {
|
||||
console.log("Message from local client", message);
|
||||
botCallback();
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, async () => {
|
||||
console.log(`Server is running on port ${port}`);
|
||||
});
|
||||
return
|
||||
}
|
||||
|
||||
export const closePdabSocketServer = () => {
|
||||
if (io.sockets && io.sockets.length > 0) {
|
||||
io.sockets.forEach(socket => {
|
||||
socket.destroy();
|
||||
})
|
||||
}
|
||||
return io.close();
|
||||
}
|
||||
|
||||
// Function to emit a command to join a voice channel
|
||||
export const connectToChannel = (channelId) => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('join_server', { channelId: channelId }, (status, value) => {
|
||||
console.log("Status returned from bot:", status, value);
|
||||
res(value[0]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Function to emit a command to leave a voice channel
|
||||
export const leaveVoiceChannel = async (guildId) => {
|
||||
return await new Promise((res) => {
|
||||
io.timeout(25000).emit('leave_server', { guildId: guildId }, (status, clientRemainsOpen) => {
|
||||
console.log("Discord client remains open?", clientRemainsOpen);
|
||||
res(clientRemainsOpen[0])
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Set the presense of the discord client
|
||||
export const setDiscordClientPrsense = (system) => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('set_system', { system: system }, (status) => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Placeholder functions (replace with actual implementation)
|
||||
export const checkIfConnectedToVC = async (guildId) => {
|
||||
console.log("Pdab process var:", pdabProcess);
|
||||
|
||||
if (!pdabProcess) return false;
|
||||
|
||||
return await new Promise((res) => {
|
||||
io.timeout(25000).emit('check_discord_vc_connected', { guildId: guildId }, (status, result) => {
|
||||
console.log(`Discord VC connected for guild ${guildId}: ${result}`);
|
||||
res((result[0]));
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
export const requestDiscordUsername = (guildId) => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('request_discord_username', { guildId: guildId }, (status, result) => {
|
||||
console.log(`Discord username: ${result[0]}`);
|
||||
res(result[0]);
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
export const checkIfClientIsOpen = () => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('check_client_is_open', (status, result) => {
|
||||
console.log(`Client is open: ${result}`);
|
||||
res(result[0])
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const requestDiscordID = () => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('request_discord_id', (status, result) => {
|
||||
console.log(`Discord ID: ${result}`);
|
||||
res(result[0]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const requestDiscordClientClose = () => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('request_client_close');
|
||||
pdabProcess = false;
|
||||
res();
|
||||
});
|
||||
};
|
||||
122
discordAudioBot/pdabWrappers.mjs
Normal file
122
discordAudioBot/pdabWrappers.mjs
Normal file
@@ -0,0 +1,122 @@
|
||||
import { connectToChannel, leaveVoiceChannel, checkIfConnectedToVC, initDiscordBotClient, requestDiscordUsername, requestDiscordID, requestDiscordClientClose, closePdabSocketServer, setDiscordClientPrsense, startPdabSocketServer } from './pdabHandler.mjs';
|
||||
import { openOP25, closeOP25 } from '../op25Handler/op25Handler.mjs';
|
||||
|
||||
let activeDiscordClient = undefined;
|
||||
|
||||
/**
|
||||
* Join the requested server VC and listen to the requested system
|
||||
* @param {object} joinData The object containing all the information to join the server
|
||||
*/
|
||||
export const joinDiscordVC = async (joinData) => {
|
||||
console.log("Join requested: ", joinData);
|
||||
const connection = await new Promise(async (res) => {
|
||||
// Check if a client already exists
|
||||
console.log("Checking if there is a client open");
|
||||
if (!await checkIfClientIsOpen()) {
|
||||
console.log("There is no open client, starting it now");
|
||||
await startPdabSocketServer();
|
||||
// Open an instance of OP25
|
||||
console.log("Starting OP25")
|
||||
openOP25(joinData.system);
|
||||
|
||||
// Open a new client and join the requested channel with the requested ID
|
||||
initDiscordBotClient(joinData.clientID, () => {
|
||||
console.log("Started PDAB");
|
||||
|
||||
console.log("Setting the presense of the bot");
|
||||
setDiscordClientPrsense(joinData.system);
|
||||
|
||||
// Add the client object to the IO instance
|
||||
console.log("Connecting to channel")
|
||||
connectToChannel(joinData.channelID, (connectionStatus) => {
|
||||
console.log("Bot Connected to VC:", connectionStatus);
|
||||
res(connectionStatus);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Join the requested channel with the requested ID
|
||||
console.log("There is an open client");
|
||||
|
||||
console.log("Connecting to channel")
|
||||
const connection = connectToChannel(joinData.channelID);
|
||||
console.log("Bot Connected to VC::");
|
||||
res(connection);
|
||||
}
|
||||
});
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave VC on the requested server
|
||||
* @param {string} guildId The guild ID to disconnect from VC
|
||||
*/
|
||||
export const leaveDiscordVC = async (guildId) => {
|
||||
console.log("Leave requested");
|
||||
if (await checkIfConnectedToVC(guildId)) {
|
||||
const clientRemainsOpen = await leaveVoiceChannel(guildId);
|
||||
console.log("Client should remain open: ", clientRemainsOpen);
|
||||
if (!clientRemainsOpen) {
|
||||
console.log("There are no open VC connections");
|
||||
await closeOP25();
|
||||
|
||||
// Close the python client
|
||||
await requestDiscordClientClose();
|
||||
|
||||
// Close the IPC server
|
||||
await closePdabSocketServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the bot is connected to a discord VC in the given server
|
||||
* @param {string} guildId The guild id to check the connection status in
|
||||
* @returns {boolean} If the node is connected to VC in the given guild
|
||||
*/
|
||||
export const checkIfDiscordVCConnected = async (guildId) => {
|
||||
console.log("Requested status check");
|
||||
if (await checkIfConnectedToVC(guildId)) {
|
||||
console.log("There is an open VC connection");
|
||||
return (true);
|
||||
} else {
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the username of the bot in a given guild
|
||||
* (there may be a server nickname given to the bot in a certain guild)
|
||||
* @param {string} guildId The guild id to check the connection status in
|
||||
* @returns {string} The username of the bot in the given guild's VC
|
||||
*/
|
||||
export const getDiscordUsername = async (guildId) => {
|
||||
console.log("Requested username");
|
||||
if (checkIfClientIsOpen()) {
|
||||
return await requestDiscordUsername(guildId)
|
||||
} else return (undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the currently running bot
|
||||
* @returns {string} The ID of the active client
|
||||
*/
|
||||
export const getDiscordID = async () => {
|
||||
console.log("Requested ID");
|
||||
if (checkIfClientIsOpen()) {
|
||||
return await requestDiscordID();
|
||||
}
|
||||
else return (undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if there is an open discord client
|
||||
* @returns {boolean} If the client is open or not
|
||||
*/
|
||||
export const checkIfClientIsOpen = async () => {
|
||||
if (activeDiscordClient) {
|
||||
return (true);
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
77
modules/baseUtils.mjs
Normal file
77
modules/baseUtils.mjs
Normal file
@@ -0,0 +1,77 @@
|
||||
import { networkInterfaces } from 'os';
|
||||
import { createHash, randomBytes } from 'crypto';
|
||||
import { promises, constants } from 'fs';
|
||||
import { dirname } from "path";
|
||||
|
||||
/**
|
||||
* Check to see if the input is a valid JSON string
|
||||
*
|
||||
* @param {*} str The string to check for valud JSON
|
||||
* @returns {true|false}
|
||||
*/
|
||||
export const isJsonString = (str) => {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if a path exists, creating it if it does not
|
||||
* @returns {undefined}
|
||||
*/
|
||||
export const ensureDirectoryExists = async (filePath) => {
|
||||
const directory = dirname(filePath);
|
||||
try {
|
||||
await promises.access(directory, constants.F_OK);
|
||||
} catch (error) {
|
||||
await promises.mkdir(directory, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generate a unique ID for a given device, this will also be unique on the same device at a different time.
|
||||
* @returns {string} A generated unique ID
|
||||
*/
|
||||
export const generateUniqueID = () => {
|
||||
const netInterfaces = networkInterfaces();
|
||||
let macAddress = '';
|
||||
|
||||
// Find the first non-internal MAC address
|
||||
for (const key in netInterfaces) {
|
||||
const iface = netInterfaces[key][0];
|
||||
if (!iface.internal) {
|
||||
macAddress = iface.mac;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no non-internal MAC address is found, fallback to a random value
|
||||
if (!macAddress) {
|
||||
macAddress = randomBytes(6).toString('hex').toUpperCase();
|
||||
}
|
||||
|
||||
// Use MAC address and current timestamp to create a unique ID
|
||||
const timestamp = Date.now();
|
||||
const uniqueID = createHash('sha256')
|
||||
.update(macAddress + timestamp)
|
||||
.digest('hex');
|
||||
|
||||
return uniqueID;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts the value after a specific pattern from a string using regular expressions.
|
||||
* @param {string} input - The input string.
|
||||
* @param {string} pattern - The pattern to match.
|
||||
* @returns {string|null} The value found after the pattern, or null if not found.
|
||||
*/
|
||||
export const extractValue = (input, pattern) => {
|
||||
const regex = new RegExp(`${pattern}`);
|
||||
const match = input.match(regex);
|
||||
return match ? match : null;
|
||||
};
|
||||
39
modules/cliHandler.mjs
Normal file
39
modules/cliHandler.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
import { spawn } from "child_process";
|
||||
|
||||
/**
|
||||
* Executes a command and retrieves its output.
|
||||
* @param {string} command - The command to execute.
|
||||
* @param {string[]} args - The arguments to pass to the command.
|
||||
* @returns {Promise<string>} A promise that resolves with the output of the command.
|
||||
*/
|
||||
export const executeCommand = (command, args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const childProcess = spawn(command, args);
|
||||
|
||||
let commandOutput = '';
|
||||
|
||||
childProcess.stdout.on('data', (data) => {
|
||||
commandOutput += data.toString();
|
||||
});
|
||||
|
||||
childProcess.stderr.on('data', (data) => {
|
||||
// Log any errors to stderr
|
||||
console.error(data.toString());
|
||||
});
|
||||
|
||||
childProcess.on('error', (error) => {
|
||||
// Reject the promise if there's an error executing the command
|
||||
reject(error);
|
||||
});
|
||||
|
||||
childProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
// Resolve the promise with the command output if it exits successfully
|
||||
resolve(commandOutput.trim());
|
||||
} else {
|
||||
// Reject the promise if the command exits with a non-zero code
|
||||
reject(new Error(`Command '${command}' exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
52
modules/clientObjectDefinitions.mjs
Normal file
52
modules/clientObjectDefinitions.mjs
Normal file
@@ -0,0 +1,52 @@
|
||||
import { getAllPresets } from "./radioPresetHandler.mjs";
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export class ClientNodeObject {
|
||||
/**
|
||||
*
|
||||
* @param {string} param0._nuid The ID of the node (Assigned by the client on first boot)
|
||||
* @param {string} param0._name The name of the node (Assigned by the user)
|
||||
* @param {string} param0._location The physical location of the node (Assigned by the user)
|
||||
* @param {object} param0._capabilities The capabilities of this node (Assigned by the user, and determined by the hardware)
|
||||
*/
|
||||
constructor({ _nuid = undefined, _name = undefined, _location = undefined, _capabilities = undefined }) {
|
||||
this.nuid = _nuid;
|
||||
this.name = _name;
|
||||
this.location = _location;
|
||||
this.capabilities = _capabilities
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** The configuration object for the node */
|
||||
export class ClientNodeConfig {
|
||||
/**
|
||||
*
|
||||
* @param {string} param0._nuid The ID of the node (Assigned by the client on first boot)
|
||||
* @param {string} param0._name The name of the node (Assigned by the user)
|
||||
* @param {string} param0._location The physical location of the node (Assigned by the user)
|
||||
* @param {object} param0._nearbySystems An object array of nearby systems (Assigned by the user)
|
||||
* @param {object} param0._capabilities The capabilities of this node (Assigned by the user, and determined by the hardware)
|
||||
*/
|
||||
constructor({
|
||||
_nuid = process.env.CLIENT_NUID,
|
||||
_name = process.env.CLIENT_NAME,
|
||||
_location = process.env.CLIENT_LOCATION,
|
||||
_nearbySystems = getAllPresets(),
|
||||
_capabilities = process.env.CLIENT_CAPABILITIES.split(", "),
|
||||
_serverIp = process.env.SERVER_IP,
|
||||
_serverPort = process.env.SERVER_PORT,
|
||||
}) {
|
||||
this.node = new ClientNodeObject({
|
||||
_nuid: _nuid, _name: _name, _location: _location, _capabilities: _capabilities
|
||||
});
|
||||
this.nearbySystems = _nearbySystems;
|
||||
this.serverIp = _serverIp;
|
||||
this.serverPort = _serverPort;
|
||||
}
|
||||
}
|
||||
144
modules/radioPresetHandler.mjs
Normal file
144
modules/radioPresetHandler.mjs
Normal file
@@ -0,0 +1,144 @@
|
||||
// Modules
|
||||
import { writeFile, existsSync, readFileSync } from 'fs';
|
||||
import { resolve } from "path";
|
||||
import { ensureDirectoryExists } from "./baseUtils.mjs";
|
||||
import convert_units from "convert-units";
|
||||
const { converter } = convert_units;
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
const configFilePath = process.env.CONFIG_PATH;
|
||||
|
||||
/**
|
||||
* Write the given presets to the JSON file
|
||||
* @param presets The preset or presets to be written
|
||||
* @param {function} callback The function to be called when this wrapper completes
|
||||
*/
|
||||
const writePresets = async (presets, callback = undefined) => {
|
||||
console.log(`${__dirname}`);
|
||||
await ensureDirectoryExists(configFilePath);
|
||||
writeFile(configFilePath, JSON.stringify(presets), (err) => {
|
||||
// Error checking
|
||||
if (err) throw err;
|
||||
console.log("Write Complete");
|
||||
if (callback) callback(); else return
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to ensure each value in the array is in Hz format
|
||||
* @param frequenciesArray
|
||||
* @returns {*[]}
|
||||
*/
|
||||
const sanitizeFrequencies = async (frequenciesArray) => {
|
||||
let sanitizedFrequencyArray = [];
|
||||
|
||||
for (const freq of frequenciesArray) {
|
||||
sanitizedFrequencyArray.push(convertFrequencyToHertz(freq));
|
||||
}
|
||||
|
||||
console.log("Sanitized Frequency Array", sanitizedFrequencyArray);
|
||||
return sanitizedFrequencyArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to convert a string or a float into the integer type needed to be saved
|
||||
* @param frequency Could be a string, number or float,
|
||||
* @returns {number|number|*} Return the value to be saved in Hz format ("154.875"MHz format = "154875000")
|
||||
*/
|
||||
const convertFrequencyToHertz = async (frequency) => {
|
||||
// check if the passed value is a number
|
||||
if (typeof frequency == 'number' && !isNaN(frequency)) {
|
||||
if (Number.isInteger(frequency)) {
|
||||
console.log(`${frequency} is an integer.`);
|
||||
// Check to see if the frequency has the correct length
|
||||
if (frequency >= 1000000) return frequency
|
||||
if (frequency >= 100 && frequency <= 999) return frequency * 1000000
|
||||
console.log("Frequency hasn't matched filters: ", frequency);
|
||||
}
|
||||
else {
|
||||
console.log(`${frequency} is a float value.`);
|
||||
// Convert to a string to remove the decimal in place and then correct the length
|
||||
return parseInt(converter(frequency).from("MHz").to("Hz"));
|
||||
}
|
||||
} else {
|
||||
console.log(`${frequency} is not a number`);
|
||||
frequency = convertFrequencyToHertz(parseFloat(frequency));
|
||||
|
||||
return parseInt(frequency)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the saved presets and returns a preset object
|
||||
* @returns {any} The object containing the different systems the bot is near
|
||||
*/
|
||||
export const getAllPresets = () => {
|
||||
const presetDir = resolve(configFilePath);
|
||||
console.log(`Getting presets from directory: '${presetDir}'`);
|
||||
if (existsSync(presetDir)) return JSON.parse(readFileSync(presetDir));
|
||||
else return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new preset to the radioPresets JSON file
|
||||
*
|
||||
* @param {string} systemName The name of the system being added
|
||||
* @param {Array} frequencies The frequency or frequencies the SDR should tune to for this system
|
||||
* @param {string} mode The listening mode the SDR should be using when listening to this frequency
|
||||
* @param {function} callback The callback function to call when completed
|
||||
* @param {string} trunkFile The file that contains all trunking information (if applicable to the selected listening mode)
|
||||
* @param {string} whitelistFile The file that contains the whitelisted talkgroups [optional]
|
||||
*/
|
||||
export const addNewPreset = (systemName, frequencies, mode, callback, trunkFile = undefined, whitelistFile = undefined) => {
|
||||
const presets = this.getPresets();
|
||||
// Create the preset for the new system
|
||||
presets[systemName] = {
|
||||
"frequencies": sanitizeFrequencies(frequencies),
|
||||
"mode": mode,
|
||||
"trunkFile": trunkFile ?? "none",
|
||||
"whitelistFile": whitelistFile ?? "none"
|
||||
}
|
||||
// Write the changes to the preset config file
|
||||
writePresets(presets, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the specified system
|
||||
*
|
||||
* @param {string} systemName The name of the system being modified
|
||||
* @param {function} callback The callback function to be called when the function completes
|
||||
* @param {Array} frequencies The frequency or frequencies the SDR should tune to for this system
|
||||
* @param {string} mode The listening mode the SDR should be using when listening to this frequency
|
||||
* @param {string} trunkFile The file that contains all trunking information (if applicable to the selected listening mode)
|
||||
* @param {string} whitelistFile The file that contains the whitelisted talkgroups [optional]
|
||||
*/
|
||||
export const updatePreset = (systemName, callback, { frequencies = undefined, mode = undefined, trunkFile = undefined, whitelistFile = undefined }) => {
|
||||
const presets = this.getPresets();
|
||||
// Check if a system name was passed
|
||||
if (systemName in presets) {
|
||||
// System name exists, checking to see if the keys are different
|
||||
if (frequencies && sanitizeFrequencies(frequencies) !== presets[systemName].frequencies) presets[systemName].frequencies = sanitizeFrequencies(frequencies);
|
||||
if (mode && mode !== presets[systemName].mode) presets[systemName].mode = mode;
|
||||
if (trunkFile && trunkFile !== presets[systemName].trunkFile || trunkFile === "") presets[systemName].trunkFile = trunkFile ?? "none";
|
||||
if (whitelistFile && whitelistFile !== presets[systemName].whitelistFile || whitelistFile === "") presets[systemName].whitelistFile = whitelistFile ?? "none";
|
||||
// Write the changes
|
||||
writePresets(presets, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified system
|
||||
*
|
||||
* @param {string} systemName The name of the system being modified
|
||||
* @param {function} callback The callback function to be called when the function completes
|
||||
*/
|
||||
export const removePreset = (systemName, callback) => {
|
||||
const presets = this.getPresets();
|
||||
// Check if a system name was passed
|
||||
if (systemName in presets) {
|
||||
delete presets[systemName];
|
||||
writePresets(presets, callback);
|
||||
}
|
||||
}
|
||||
57
modules/selfUpdater.mjs
Normal file
57
modules/selfUpdater.mjs
Normal file
@@ -0,0 +1,57 @@
|
||||
import simpleGit from 'simple-git';
|
||||
import { restartService } from './serviceHandler.mjs'
|
||||
import { launchProcess } from './subprocessHandler.mjs'
|
||||
|
||||
const git = simpleGit();
|
||||
|
||||
// Function to check for updates
|
||||
export const checkForUpdates = async () => {
|
||||
try {
|
||||
// Fetch remote changes
|
||||
await git.fetch();
|
||||
|
||||
// Get the latest commit hash
|
||||
const latestCommitHash = await git.revparse(['@{u}']);
|
||||
|
||||
// Compare with the local commit hash
|
||||
const localCommitHash = await git.revparse(['HEAD']);
|
||||
|
||||
if (latestCommitHash !== localCommitHash) {
|
||||
console.log('An update is available. Updating...');
|
||||
|
||||
// Check if there have been any changes to the code
|
||||
const gitStatus = await git.status()
|
||||
console.log(gitStatus);
|
||||
if (gitStatus.modified.length > 0){
|
||||
// There is locally modified code
|
||||
console.log("There is locally modified code, resetting...");
|
||||
await git.stash();
|
||||
await git.reset('hard', ['origin/master']);
|
||||
}
|
||||
|
||||
// Pull the latest changes from the remote repository
|
||||
await git.pull();
|
||||
|
||||
// Run the post-update script
|
||||
console.log('Running post-update script...');
|
||||
await launchProcess("bash", ['./post-update.sh'], true);
|
||||
|
||||
// Restart the application to apply the updates
|
||||
console.log('Update completed successfully. Restarting the application...');
|
||||
restartApplication();
|
||||
|
||||
return true
|
||||
} else {
|
||||
console.log('The application is up to date.');
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking for updates:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to restart the application
|
||||
const restartApplication = () => {
|
||||
console.log('Restarting the application...');
|
||||
restartService('discord-radio-bot');
|
||||
}
|
||||
58
modules/serviceHandler.mjs
Normal file
58
modules/serviceHandler.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
import { exec } from 'child_process';
|
||||
|
||||
/**
|
||||
* Executes a system command with error handling.
|
||||
* @param {string} command The command to execute.
|
||||
* @returns {Promise<{ stdout: string, stderr: string }>} A promise resolving to an object containing stdout and stderr.
|
||||
*/
|
||||
const executeCommand = (command) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Command failed with error: ${error.message}`);
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts the given service from the command line.
|
||||
* @param {string} serviceName The service name to be started.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const startService = async (serviceName) => {
|
||||
try {
|
||||
await executeCommand(`sudo systemctl start ${serviceName}.service`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to start service: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Restarts the given service from the command line.
|
||||
* @param {string} serviceName The service name to be restarted.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const restartService = async (serviceName) => {
|
||||
try {
|
||||
await executeCommand(`sudo systemctl restart ${serviceName}.service`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart service: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops the given service from the command line.
|
||||
* @param {string} serviceName The service name to be stopped.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const stopService = async (serviceName) => {
|
||||
try {
|
||||
await executeCommand(`sudo systemctl stop ${serviceName}.service`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to stop service: ${error.message}`);
|
||||
}
|
||||
};
|
||||
52
modules/socketClient.mjs
Normal file
52
modules/socketClient.mjs
Normal file
@@ -0,0 +1,52 @@
|
||||
import { io } from "socket.io-client";
|
||||
import { logIntoServerWrapper, nodeCheckStatus, nodeJoinServer, nodeLeaveServer, nodeGetUsername, nodeCheckDiscordClientStatus, nodeCheckCurrentSystem, nodeUpdate, nodeGetDiscordID } from "./socketClientWrappers.mjs";
|
||||
|
||||
/**
|
||||
* Initialize the socket connection with the server, this will handle disconnects within itself
|
||||
* @param {Object} localNodeConfig The local node config object
|
||||
* @returns {any}
|
||||
*/
|
||||
export const initSocketConnection = async (localNodeConfig) => {
|
||||
const serverEndpoint = `http://${localNodeConfig.serverIp}:${localNodeConfig.serverPort}` || 'http://localhost:3000'; // Adjust the server endpoint
|
||||
|
||||
const socket = io.connect(serverEndpoint);
|
||||
|
||||
// Socket Events ('system' events persay)
|
||||
// When the socket connects to the node server
|
||||
socket.on('connect', async () => {
|
||||
console.log('Connected to the server');
|
||||
await logIntoServerWrapper(socket, localNodeConfig);
|
||||
});
|
||||
|
||||
// When the socket disconnects from the node server
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Disconnected from the server');
|
||||
});
|
||||
|
||||
// Node events/commands
|
||||
// Requested the node update itself
|
||||
socket.on('node-update', nodeUpdate);
|
||||
|
||||
// Requested to join a discord guild and listen to a system
|
||||
socket.on('node-join', nodeJoinServer);
|
||||
|
||||
// Requested to leave a discord guild
|
||||
socket.on('node-leave', nodeLeaveServer);
|
||||
|
||||
// Requested to get the discord username in a given guild
|
||||
socket.on('node-get-discord-username', nodeGetUsername);
|
||||
|
||||
// Requested to get the ID of the active discord client
|
||||
socket.on('node-get-discord-id', nodeGetDiscordID);
|
||||
|
||||
// Requested to check if the node is connected to VC in a given guild
|
||||
socket.on('node-check-connected-status', nodeCheckStatus);
|
||||
|
||||
// Requested to check if the node has an open discord client
|
||||
socket.on('node-check-discord-open-client', nodeCheckDiscordClientStatus);
|
||||
|
||||
// Requested to get the current listening system
|
||||
socket.on('node-check-current-system', nodeCheckCurrentSystem);
|
||||
|
||||
return socket;
|
||||
}
|
||||
108
modules/socketClientWrappers.mjs
Normal file
108
modules/socketClientWrappers.mjs
Normal file
@@ -0,0 +1,108 @@
|
||||
import { checkIfDiscordVCConnected, joinDiscordVC, leaveDiscordVC, getDiscordUsername, checkIfClientIsOpen, getDiscordID } from '../discordAudioBot/pdabWrappers.mjs';
|
||||
import { getCurrentSystem } from '../op25Handler/op25Handler.mjs';
|
||||
import { checkForUpdates } from './selfUpdater.mjs';
|
||||
|
||||
|
||||
/**
|
||||
* Check if the bot has an update available
|
||||
* @param {any} socketCallback The callback function to return the result
|
||||
* @callback {boolean} If the node has an update available or not
|
||||
*/
|
||||
export const nodeUpdate = async (socketCallback) => {
|
||||
socketCallback(await checkForUpdates());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper to log into the server
|
||||
* @param {any} socket The socket connection with the server
|
||||
* @param {object} localNodeConfig The local node object
|
||||
* @returns {any}
|
||||
*/
|
||||
export const logIntoServerWrapper = async (socket, localNodeConfig) => {
|
||||
// Log into the server
|
||||
socket.emit("node-login", localNodeConfig.node);
|
||||
|
||||
// Send an update to the server
|
||||
sendNodeUpdateWrapper(socket, localNodeConfig);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send the server an update
|
||||
* @param {any} socket The socket connection with the server
|
||||
* @param {object} localNodeConfig The local node object
|
||||
*/
|
||||
export const sendNodeUpdateWrapper = async (socket, localNodeConfig) => {
|
||||
socket.emit('node-update', {
|
||||
'node': localNodeConfig.node,
|
||||
'nearbySystems': localNodeConfig.nearbySystems
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Join the requested server VC and listen to the requested system
|
||||
* @param {object} joinData The object containing all the information to join the server
|
||||
*/
|
||||
export const nodeJoinServer = async (joinData) => {
|
||||
await joinDiscordVC(joinData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave VC on the requested server
|
||||
* @param {string} guildId The guild ID to disconnect from VC
|
||||
*/
|
||||
export const nodeLeaveServer = async (guildId) => {
|
||||
await leaveDiscordVC(guildId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the bot is connected to a discord VC in the given server
|
||||
* @param {string} guildId The guild id to check the connection status in
|
||||
* @param {any} socketCallback The callback function to return the result to
|
||||
* @callback {boolean} If the node is connected to VC in the given guild
|
||||
*/
|
||||
export const nodeCheckStatus = async (guildId, socketCallback) => {
|
||||
socketCallback(await checkIfDiscordVCConnected(guildId));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the username of the bot in a given guild
|
||||
* (there may be a server nickname given to the bot in a certain guild)
|
||||
* @param {string} guildId The guild id to check the connection status in
|
||||
* @param {any} socketCallback The callback function to return the result to
|
||||
* @callback {any}
|
||||
*/
|
||||
export const nodeGetUsername = async (guildId, socketCallback) => {
|
||||
socketCallback(await getDiscordUsername(guildId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the active client
|
||||
* @param {any} socketCallback The callback function to return the result to
|
||||
* @callback {any}
|
||||
*/
|
||||
export const nodeGetDiscordID = async (socketCallback) => {
|
||||
socketCallback(await getDiscordID());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the local node has an open discord client in any server
|
||||
* @callback {boolean} If the node has an open discord client or not
|
||||
*/
|
||||
export const nodeCheckDiscordClientStatus = async (socketCallback) => {
|
||||
socketCallback(await checkIfClientIsOpen());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check what system the local node is currently listening to
|
||||
* @callback {boolean} If the node has an open discord client or not
|
||||
*/
|
||||
export const nodeCheckCurrentSystem = async (socketCallback) => {
|
||||
socketCallback(await getCurrentSystem());
|
||||
}
|
||||
100
modules/subprocessHandler.mjs
Normal file
100
modules/subprocessHandler.mjs
Normal file
@@ -0,0 +1,100 @@
|
||||
import { spawn } from "child_process";
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
/**
|
||||
* Object to store references to spawned processes.
|
||||
* @type {Object.<string, import('child_process').ChildProcess>}
|
||||
*/
|
||||
const runningProcesses = {};
|
||||
|
||||
/**
|
||||
* Launches a new process if it's not already running.
|
||||
* @param {string} processName - The name of the process to launch.
|
||||
* @param {string[]} args - The arguments to pass to the process.
|
||||
* @param {boolean} waitForClose - Set this to wait to return until the process exits
|
||||
*/
|
||||
export const launchProcess = (processName, args, waitForClose = false, pcwd = undefined) => {
|
||||
if (!runningProcesses[processName]) {
|
||||
let childProcess;
|
||||
if (pcwd) {
|
||||
childProcess = spawn(processName, args, { cwd: pcwd });
|
||||
}
|
||||
else {
|
||||
childProcess = spawn(processName, args);
|
||||
}
|
||||
|
||||
// Store reference to the spawned process
|
||||
runningProcesses[processName] = childProcess;
|
||||
|
||||
// Output the process output in development
|
||||
var scriptOutput = "";
|
||||
|
||||
// Get the stdout from the child process
|
||||
childProcess.stdout.setEncoding('utf8');
|
||||
childProcess.stdout.on('data', (data) => {
|
||||
if (process.env.NODE_ENV === "development") console.log(`Data from ${processName}:`, data);
|
||||
scriptOutput += data.toString();
|
||||
});
|
||||
|
||||
// Get the stderr from the child process
|
||||
childProcess.stderr.setEncoding('utf8');
|
||||
childProcess.stderr.on('data', (data) => {
|
||||
if (process.env.NODE_ENV === "development") console.log(`Data from ${processName}:`, data);
|
||||
scriptOutput += data.toString();
|
||||
})
|
||||
|
||||
let code = new Promise(res => {
|
||||
childProcess.on('exit', (code, signal) => {
|
||||
// Remove reference to the process when it exits
|
||||
delete runningProcesses[processName];
|
||||
console.log(`${processName} process exited with code ${code} and signal ${signal}`);
|
||||
console.log("Child process console output: ", scriptOutput);
|
||||
res(code);
|
||||
})
|
||||
});
|
||||
|
||||
if (waitForClose === true) {
|
||||
return code
|
||||
}
|
||||
|
||||
console.log(`${processName} process started.`);
|
||||
} else {
|
||||
console.log(`${processName} process is already running.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the status of a process.
|
||||
* @param {string} processName - The name of the process to check.
|
||||
* @returns {string} A message indicating whether the process is running or not.
|
||||
*/
|
||||
export const checkProcessStatus = (processName) => {
|
||||
const childProcess = runningProcesses[processName];
|
||||
if (childProcess) {
|
||||
// Check if the process is running
|
||||
if (!childProcess.killed) {
|
||||
return `${processName} process is running.`;
|
||||
} else {
|
||||
return `${processName} process is not running.`;
|
||||
}
|
||||
} else {
|
||||
return `${processName} process is not running.`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills a running process.
|
||||
* @param {string} processName - The name of the process to kill.
|
||||
*/
|
||||
export const killProcess = (processName) => {
|
||||
const childProcess = runningProcesses[processName];
|
||||
if (childProcess) {
|
||||
childProcess.kill();
|
||||
console.log(`${processName} process killed.`);
|
||||
} else {
|
||||
console.log(`${processName} process is not running.`);
|
||||
}
|
||||
}
|
||||
|
||||
export const getRunningProcesses = () => runningProcesses;
|
||||
109
modules/updateConfig.mjs
Normal file
109
modules/updateConfig.mjs
Normal file
@@ -0,0 +1,109 @@
|
||||
// Modules
|
||||
import replace from 'replace-in-file';
|
||||
|
||||
class Options {
|
||||
constructor(key, updatedValue) {
|
||||
this.files = ".env";
|
||||
// A regex of the line containing the key in the config file
|
||||
this.from = new RegExp(`${key}="?(.+)?"?`, "g");
|
||||
// Check to see if the value is a string and needs to be wrapped in double quotes
|
||||
if (Array(["string", "number"]).includes(typeof updatedValue)) this.to = `${key}="${updatedValue}",`;
|
||||
else this.to = `${key}=${updatedValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to update the client's saved ID
|
||||
* @param updatedId The updated ID assigned to the node
|
||||
*/
|
||||
export const updateId = async (updatedId) => {
|
||||
await updateConfig('CLIENT_NUID', updatedId);
|
||||
process.env.CLIENT_NUID = updatedId;
|
||||
console.log("Updated NUID to: ", updatedId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to update any or all keys in the client config
|
||||
*
|
||||
* @param {Object} runningConfig Running config object
|
||||
* @param {Object} newConfigObject Object with what keys you wish to update (node object format, will be converted)
|
||||
* @param {number} newConfigObject.nuid The ID given to the node to update
|
||||
* @param {string} newConfigObject.name The name of the node
|
||||
* @param {string} newConfigObject.ip The IP the server can contact the node on
|
||||
* @param {number} newConfigObject.port The port the server can contact the node on
|
||||
* @param {string} newConfigObject.location The physical location of the node
|
||||
* @returns
|
||||
*/
|
||||
export function updateClientConfig (runningConfig, newConfigObject) {
|
||||
var updatedKeys = []
|
||||
const configKeys = Object.keys(newConfigObject);
|
||||
|
||||
if (configKeys.includes("nuid")) {
|
||||
if (runningConfig.nuid != newConfigObject.nuid) {
|
||||
this.updateId(newConfigObject.nuid);
|
||||
updatedKeys.push({ 'CLIENT_NUID': newConfigObject.nuid });
|
||||
}
|
||||
}
|
||||
if (configKeys.includes("name")) {
|
||||
if (runningConfig.name != newConfigObject.name) {
|
||||
this.updateConfig('CLIENT_NAME', newConfigObject.name);
|
||||
updatedKeys.push({ 'CLIENT_NAME': newConfigObject.name });
|
||||
process.env.CLIENT_NAME = newConfigObject.name;
|
||||
console.log("Updated name to: ", newConfigObject.name);
|
||||
}
|
||||
}
|
||||
if (configKeys.includes("ip")) {
|
||||
if (runningConfig.ip != newConfigObject.ip) {
|
||||
this.updateConfig('CLIENT_IP', newConfigObject.ip);
|
||||
updatedKeys.push({ 'CLIENT_IP': newConfigObject.ip });
|
||||
process.env.CLIENT_IP = newConfigObject.ip;
|
||||
console.log("Updated ip to: ", newConfigObject.ip);
|
||||
}
|
||||
}
|
||||
if (configKeys.includes("port")) {
|
||||
if (runningConfig.port != newConfigObject.port) {
|
||||
this.updateConfig('CLIENT_PORT', newConfigObject.port);
|
||||
updatedKeys.push({ 'CLIENT_PORT': newConfigObject.port });
|
||||
process.env.CLIENT_PORT = newConfigObject.port;
|
||||
console.log("Updated port to: ", newConfigObject.port);
|
||||
}
|
||||
}
|
||||
if (configKeys.includes("location")) {
|
||||
if (runningConfig.location != newConfigObject.location) {
|
||||
this.updateConfig('CLIENT_LOCATION', newConfigObject.location);
|
||||
updatedKeys.push({ 'CLIENT_LOCATION': newConfigObject.location });
|
||||
process.env.CLIENT_LOCATION = newConfigObject.location;
|
||||
console.log("Updated location to: ", newConfigObject.location);
|
||||
}
|
||||
}
|
||||
|
||||
return updatedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key The config file key to update with the value
|
||||
* @param {string} value The value to update the key with
|
||||
*/
|
||||
export function updateConfig (key, value) {
|
||||
const options = new Options(key, value);
|
||||
|
||||
console.log("Options:", options);
|
||||
|
||||
updateConfigFile(options, (updatedFiles) => {
|
||||
// Do Something
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to write changes to the file
|
||||
* @param options An instance of the Objects class specified to the key being updated
|
||||
* @param callback Callback when the files have been modified
|
||||
*/
|
||||
function updateConfigFile(options, callback) {
|
||||
replace(options, (error, changedFiles) => {
|
||||
if (error) return console.error('Error occurred:', error);
|
||||
console.log('Updated config file: ', changedFiles);
|
||||
callback(changedFiles);
|
||||
});
|
||||
}
|
||||
183
op25Handler/modules/op25ConfigGenerators.mjs
Normal file
183
op25Handler/modules/op25ConfigGenerators.mjs
Normal file
@@ -0,0 +1,183 @@
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
class OP25ConfigObject {
|
||||
constructor() { }
|
||||
|
||||
async exportToFile(filename) {
|
||||
try {
|
||||
const jsonConfig = JSON.stringify(this, null, 2);
|
||||
await fs.writeFile(filename, jsonConfig);
|
||||
console.log(`Config exported to ${filename}`);
|
||||
} catch (error) {
|
||||
console.error(`Error exporting config to ${filename}: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class P25ConfigGenerator extends OP25ConfigObject {
|
||||
constructor({ systemName, controlChannels, tagsFile, whitelistFile = undefined }) {
|
||||
super();
|
||||
console.log("Generating P25 Config for:", systemName);
|
||||
const controlChannelsString = controlChannels.join(',');
|
||||
this.channels = [new channelConfig({
|
||||
"channelName": systemName,
|
||||
"systemName": systemName,
|
||||
"enableAnalog": "off",
|
||||
"demodType": "cqpsk",
|
||||
"cqpskTracking": true,
|
||||
"filterType": "rc"
|
||||
})];
|
||||
this.devices = [new deviceConfig({
|
||||
"gain": "LNA:36"
|
||||
})];
|
||||
this.trunking = new trunkingConfig({
|
||||
"module": "tk_p25.py",
|
||||
"systemName": systemName,
|
||||
"controlChannelsString": controlChannelsString,
|
||||
"tagsFile": tagsFile,
|
||||
"whitelist": whitelistFile
|
||||
});
|
||||
this.audio = new audioConfig({});
|
||||
this.terminal = new terminalConfig({});
|
||||
}
|
||||
}
|
||||
|
||||
export class NBFMConfigGenerator extends OP25ConfigObject {
|
||||
constructor({ systemName, frequency, nbfmSquelch = -70 }) {
|
||||
super();
|
||||
this.channels = new channelConfig({
|
||||
"channelName": systemName,
|
||||
"enableAnalog": "on",
|
||||
"nbfmSquelch": nbfmSquelch,
|
||||
"frequency": frequency,
|
||||
"demodType": "fsk4",
|
||||
"filterType": "widepulse"
|
||||
});
|
||||
this.devices = new deviceConfig({
|
||||
"gain": "LNA:32"
|
||||
});
|
||||
this.audio = new audioConfig({});
|
||||
this.terminal = new terminalConfig({});
|
||||
}
|
||||
}
|
||||
|
||||
class channelConfig {
|
||||
constructor({
|
||||
channelName = "Voice_ch1",
|
||||
device = "sdr0",
|
||||
systemName,
|
||||
metaStreamName,
|
||||
demodType, // cqpsk: P25; fsk4: everything else
|
||||
cqpskTracking,
|
||||
trackingThreshold = 120,
|
||||
trackingFeedback = 0.75,
|
||||
destination = "udp://127.0.0.1:23456",
|
||||
excess_bw = 0.2,
|
||||
filterType = "rc", // rc: P25; widepulse: analog
|
||||
ifRate = 24000,
|
||||
plot = "",
|
||||
symbolRate = 4800,
|
||||
enableAnalog, //[on, off, auto]
|
||||
nbfmDeviation = 4000, // only needed if analog is enabled
|
||||
nbfmSquelch = -50, // only needed if analog is enabled
|
||||
frequency, // only needed if analog is enabled
|
||||
blacklist,
|
||||
whitelist,
|
||||
cryptKeys
|
||||
}) {
|
||||
// Core Configs
|
||||
this.name = channelName;
|
||||
this.device = device;
|
||||
this.demod_type = demodType;
|
||||
this.destination = destination;
|
||||
this.excess_bw = excess_bw;
|
||||
this.filter_type = filterType;
|
||||
this.if_rate = ifRate;
|
||||
this.plot = plot;
|
||||
this.symbol_rate = symbolRate;
|
||||
this.enable_analog = enableAnalog;
|
||||
|
||||
// P25 config
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName) this.trunking_sysname = systemName;
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName && metaStreamName) this.meta_stream_name = metaStreamName ?? "";
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName) this.cqpsk_tracking = cqpskTracking;
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName) this.tracking_threshold = trackingThreshold;
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName) this.tracking_feedback = trackingFeedback;
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName && blacklist) this.blacklist = blacklist ?? "";
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName && whitelist) this.whitelist = whitelist ?? "";
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName && cryptKeys) this.crypt_keys = cryptKeys ?? "";
|
||||
|
||||
// Analog config
|
||||
if (enableAnalog === "on" || enableAnalog === "auto") this.nbfm_deviation = nbfmDeviation;
|
||||
if (enableAnalog === "on" || enableAnalog === "auto") this.nbfm_squelch = nbfmSquelch;
|
||||
if (enableAnalog === "on" || enableAnalog === "auto") this.frequency = frequency;
|
||||
}
|
||||
}
|
||||
|
||||
class deviceConfig {
|
||||
constructor({ args = "rtl", gain = "LNA:32", gainMode = false, name = "sdr0", offset = 0, ppm = 0.0, sampleRate = 1920000, tunable = true }) {
|
||||
this.args = args
|
||||
this.gains = gain
|
||||
this.gain_mode = gainMode
|
||||
this.name = name
|
||||
this.offset = offset
|
||||
this.ppm = ppm
|
||||
this.rate = sampleRate
|
||||
this.usable_bw_pct = 0.85
|
||||
this.tunable = tunable
|
||||
}
|
||||
}
|
||||
|
||||
class trunkingConfig {
|
||||
/**
|
||||
*
|
||||
* @param {object} *
|
||||
*/
|
||||
constructor({ module, systemName, controlChannelsString, tagsFile = "", nac = "0x0", wacn = "0x0", cryptBehavior = 2, whitelist = "", blacklist = "" }) {
|
||||
this.module = module;
|
||||
this.chans = [{
|
||||
"nac": nac,
|
||||
"wacn": wacn,
|
||||
"sysname": systemName,
|
||||
"control_channel_list": controlChannelsString,
|
||||
"whitelist": whitelist,
|
||||
"blacklist": blacklist,
|
||||
"tgid_tags_file": tagsFile,
|
||||
"tdma_cc": false,
|
||||
"crypt_behavior": cryptBehavior
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
class audioConfig {
|
||||
constructor({ module = "sockaudio.py", port = 23456, deviceName = "default" }) {
|
||||
this.module = module;
|
||||
this.instances = [{
|
||||
"instance_name": "audio0",
|
||||
"device_name": deviceName,
|
||||
"udp_port": port,
|
||||
"audio_gain": 2.0,
|
||||
"number_channels": 1
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
class metadataStreamConfig {
|
||||
constructor({ }) {
|
||||
this.module = "";
|
||||
this.streams = [];
|
||||
}
|
||||
}
|
||||
|
||||
class terminalConfig {
|
||||
constructor({ module = "terminal.py", terminalType = "http:0.0.0.0:8081" }) {
|
||||
this.module = module;
|
||||
this.terminal_type = terminalType;
|
||||
this.curses_plot_interval = 0.1;
|
||||
this.http_plot_interval = 1.0;
|
||||
this.http_plot_directory = "../www/images";
|
||||
this.tuning_step_large = 1200;
|
||||
this.tuning_step_small = 100;
|
||||
}
|
||||
}
|
||||
|
||||
84
op25Handler/op25Handler.mjs
Normal file
84
op25Handler/op25Handler.mjs
Normal file
@@ -0,0 +1,84 @@
|
||||
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') {
|
||||
console.log("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') {
|
||||
console.log("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];
|
||||
|
||||
console.log("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;
|
||||
};
|
||||
2657
package-lock.json
generated
Normal file
2657
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "drb-client",
|
||||
"version": "3.0.0",
|
||||
"description": "",
|
||||
"main": "client.js",
|
||||
"scripts": {
|
||||
"test": "mocha --timeout 10000"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@discordjs/voice": "^0.16.1",
|
||||
"convert-units": "^2.3.4",
|
||||
"discord.js": "^14.14.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.19.2",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"prism-media": "^1.3.5",
|
||||
"replace-in-file": "^7.1.0",
|
||||
"simple-git": "^3.22.0",
|
||||
"socket.io": "^4.7.5",
|
||||
"socket.io-client": "^4.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai-http": "^4.4.0",
|
||||
"chai": "^5.1.0",
|
||||
"mocha": "^10.4.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
12
post-update.sh
Normal file
12
post-update.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Install client package updates
|
||||
npm install
|
||||
|
||||
# Install OP25 Updates
|
||||
#cd ./op25
|
||||
#bash rebuild.sh
|
||||
|
||||
# Check for PDAB updates
|
||||
cd ../discordAudioBot/pdab
|
||||
git pull
|
||||
1
serviceStart.sh
Normal file
1
serviceStart.sh
Normal file
@@ -0,0 +1 @@
|
||||
node .
|
||||
314
setup.sh
Normal file
314
setup.sh
Normal file
@@ -0,0 +1,314 @@
|
||||
#!/bin/bash
|
||||
|
||||
####------------------- Pre-Flight Checks
|
||||
# Exit on error
|
||||
set -e
|
||||
|
||||
# Check if the script is run as root
|
||||
if [[ $(id -u) -ne 0 ]]; then
|
||||
echo "Please run this script as root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the working directory is 'client' and contains package.json
|
||||
if [[ ! -f "$(pwd)/package.json" ]]; then
|
||||
echo "Error: Please make sure the working directory is 'client' and contains package.json."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check to make sure the pi user exists
|
||||
if ! id "pi" &>/dev/null; then
|
||||
echo "Error: User pi does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
####------------------- Functions
|
||||
# Function to prompt user for input with a specific message and store the result in a variable
|
||||
prompt_user() {
|
||||
if [[ "$TEST_MODE" == "true" ]]; then
|
||||
echo "TESTING" # Use the pre-set value
|
||||
else
|
||||
read -p "$1: " input
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to prompt user for capabilities options and store the result in a variable
|
||||
prompt_capabilities() {
|
||||
if [[ "$TEST_MODE" == "true" ]]; then
|
||||
echo "radio" # Use the pre-set value
|
||||
else
|
||||
default_capabilities="radio" # Default value
|
||||
read -p "Select CLIENT_CAPABILITIES (comma-separated, default: $default_capabilities): " capabilities
|
||||
capabilities="${capabilities:-$default_capabilities}" # Use default value if input is empty
|
||||
echo "$capabilities"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to prompt user for nearby systems details
|
||||
prompt_nearby_system() {
|
||||
if [[ "$TEST_MODE" == "true" ]]; then
|
||||
echo "\"TESTING-Node\": {
|
||||
\"frequencies\": [\"$(echo "155750000,154750000,156555550" | sed 's/,/","/g')\"],
|
||||
\"mode\": \"p25\",
|
||||
\"trunkFile\": \"testing_trunk.tsv\",
|
||||
\"whitelistFile\": \"testing_whitelist.tsv\"
|
||||
}," # Use the pre-set value
|
||||
else
|
||||
local system_name=""
|
||||
local frequencies=""
|
||||
local mode=""
|
||||
local trunk_file=""
|
||||
local whitelist_file=""
|
||||
|
||||
read -p "Enter system name: " system_name
|
||||
read -p "Enter frequencies (comma-separated): " frequencies
|
||||
read -p "Enter mode (p25/nbfm): " mode
|
||||
|
||||
if [[ "$mode" == "p25" ]]; then
|
||||
read -p "Enter trunk file: " trunk_file
|
||||
read -p "Enter whitelist file: " whitelist_file
|
||||
fi
|
||||
|
||||
echo "\"$system_name\": {
|
||||
\"frequencies\": [\"$(echo "$frequencies" | sed 's/,/","/g')\"],
|
||||
\"mode\": \"$mode\",
|
||||
\"trunkFile\": \"$trunk_file\",
|
||||
\"whitelistFile\": \"$whitelist_file\"
|
||||
},"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if test mode is enabled
|
||||
if [[ "$1" == "--test" ]]; then
|
||||
TEST_MODE="true"
|
||||
else
|
||||
TEST_MODE="false"
|
||||
fi
|
||||
|
||||
# Install Node Repo
|
||||
# Get the CPU architecture
|
||||
cpu_arch=$(uname -m)
|
||||
|
||||
# Print the CPU architecture for verification
|
||||
echo "Detected CPU Architecture: $cpu_arch"
|
||||
|
||||
# Check if the architecture is ARMv6
|
||||
if [[ "$cpu_arch" == "armv6"* ]]; then
|
||||
echo "----- CPU Architecture is ARMv6 or compatible. -----"
|
||||
echo "----- CPU Architectre is not compatible with dependencies of this project, please use a newer CPU architecture -----"
|
||||
exit
|
||||
fi
|
||||
|
||||
curl -fsSL https://deb.nodesource.com/setup_current.x | sudo -E bash -
|
||||
|
||||
# Update the system
|
||||
apt update
|
||||
apt upgrade -y
|
||||
|
||||
# Install the necessary packages
|
||||
echo "Installing dependencies..."
|
||||
apt install -y \
|
||||
nodejs \
|
||||
libasound-dev \
|
||||
portaudio19-dev \
|
||||
libportaudio2 \
|
||||
libpulse-dev \
|
||||
pulseaudio \
|
||||
apulse \
|
||||
git \
|
||||
ffmpeg \
|
||||
python3 \
|
||||
python3-pip
|
||||
|
||||
echo "Setting up Pulse Audio"
|
||||
|
||||
# Ensure pulse audio is running as system so the service can see the audio device
|
||||
systemctl --global disable pulseaudio.service pulseaudio.socket
|
||||
|
||||
# Update the PulseAudio config to disable autospawning
|
||||
sed -i 's/autospawn = .*$/autospawn = no/' /etc/pulse/client.conf
|
||||
|
||||
# Add the system PulseAudio service
|
||||
echo "[Unit]
|
||||
Description=PulseAudio system server
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=pulseaudio --daemonize=no --system --realtime --log-target=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target" >> /etc/systemd/system/PulseAudio.service
|
||||
|
||||
# Add the root user to the pulse-access group
|
||||
usermod -aG pulse-access root
|
||||
usermod -aG pulse-access pi
|
||||
|
||||
# Enable the PulseAudio service
|
||||
systemctl enable PulseAudio.service
|
||||
|
||||
####------------------- Install and setup node
|
||||
# Run npm install to install dependencies listed in package.json
|
||||
echo "Installing npm dependencies..."
|
||||
npm install
|
||||
|
||||
# Get rid of PEP 668
|
||||
rm -rf /usr/lib/python3.11/EMTERNALL-MANAGED # Not sure if this was an attrocious fat finger or if this is needed, doesn't throw an error, so...
|
||||
rm -rf /usr/lib/python3.11/EXTERNALLY-MANAGED
|
||||
|
||||
# Getting the Python DAB
|
||||
echo "Installing PDAB and Dependencies"
|
||||
git clone -b DRBv3 https://git.vpn.cusano.net/logan/Python-Discord-Audio-Bot.git ./discordAudioBot/pdab
|
||||
pip3 install -r ./discordAudioBot/pdab/requirements.txt
|
||||
|
||||
# Generate .env file
|
||||
echo "Creating the config .env file..."
|
||||
echo "# Client Config" > .env
|
||||
echo "CLIENT_NUID=0" >> .env
|
||||
client_name=$(prompt_user "Enter the name for this node")
|
||||
echo "CLIENT_NAME=$client_name" >> .env
|
||||
client_location=$(prompt_user "Enter the location of this node")
|
||||
echo "CLIENT_LOCATION=$client_location" >> .env
|
||||
client_capabilities=$(prompt_capabilities)
|
||||
echo "CLIENT_CAPABILITIES=$client_capabilities" >> .env
|
||||
|
||||
# Server configuration (preset values)
|
||||
echo "" >> .env
|
||||
echo "# Configuration for the connection to the server" >> .env
|
||||
echo "SERVER_IP=vpn.cusano.net" >> .env
|
||||
echo "SERVER_PORT=3000" >> .env
|
||||
|
||||
# OP25 configuration (preset values)
|
||||
echo "" >> .env
|
||||
echo "# Configuration for OP25" >> .env
|
||||
op25_full_path="$(pwd)/op25/op25/gr-op25_repeater/apps" # Update this with the actual path
|
||||
echo "OP25_FULL_PATH=$op25_full_path" >> .env
|
||||
|
||||
# Core configuration (preset value)
|
||||
echo "" >> .env
|
||||
echo "# Core config, DO NOT TOUCH UNLESS YOU KNOW WHAT YOU ARE DOING" >> .env
|
||||
echo "CONFIG_PATH=./config/radioPresets.json" >> .env
|
||||
runuser -l pi -c 'python3 ./discordAudioBot/pdab/getDevices.py'
|
||||
audio_device_id=$(prompt_user "Enter the ID of the 'input' audio device you would like to use (most often 'default')")
|
||||
echo "AUDIO_DEVICE_ID=$audio_device_id" >> .env
|
||||
echo "PDAB_PORT=3110" >> .env
|
||||
echo "NODE_ENV=production" >> .env
|
||||
|
||||
echo ".env file generated successfully."
|
||||
|
||||
# Create a JSON object to store nearby systems
|
||||
systems_json="{"
|
||||
while true; do
|
||||
systems_json+="$(prompt_nearby_system)"
|
||||
read -p "Do you want to add another system? (yes/no): " choice
|
||||
if [[ "$choice" != "yes" ]]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
systems_json="${systems_json%,}" # Remove trailing comma
|
||||
systems_json+="}"
|
||||
|
||||
# Append the created systems to the presets file
|
||||
mkdir -p ./config
|
||||
echo "$systems_json" >> "./config/radioPresets.json"
|
||||
|
||||
echo "Systems added to radioPresets.json."
|
||||
|
||||
# Create a systemd service file
|
||||
echo "Adding DRB Node service..."
|
||||
service_content="[Unit]
|
||||
Description=Discord-Radio-Bot_v3
|
||||
After=syslog.target network.target nss-lookup.target network-online.target
|
||||
Requires=network-online.target
|
||||
|
||||
[Service]
|
||||
User=1000
|
||||
Group=1000
|
||||
WorkingDirectory=$(pwd)
|
||||
ExecStart=/bin/bash -- serviceStart.sh
|
||||
RestartSec=5
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target"
|
||||
|
||||
# Write the systemd service file
|
||||
echo "$service_content" > /etc/systemd/system/discord-radio-bot.service
|
||||
|
||||
# Reload systemd daemon
|
||||
systemctl daemon-reload
|
||||
systemctl enable discord-radio-bot.service
|
||||
|
||||
echo "\n\n\t\tDiscord Client Node install completed!\n\n"
|
||||
|
||||
####------------------- OP25 Installation
|
||||
# Clone OP25 from the git repository
|
||||
echo "Cloning OP25 from the git repository..."
|
||||
git clone -b gr310 https://github.com/boatbod/op25.git
|
||||
|
||||
# Navigate to the OP25 directory
|
||||
ogPwd=$(pwd)
|
||||
cd op25
|
||||
|
||||
# Edit the startup script to use the active.cfg.json config file generated by the app
|
||||
echo "Editing startup script..."
|
||||
sed -i 's/p25_rtl_example.json/active.cfg.json/g' op25-multi_rx.sh
|
||||
|
||||
# Move the startup script to the apps dir
|
||||
mv op25-multi_rx.sh op25/gr-op25_repeater/apps/
|
||||
|
||||
# Install the OP25 service
|
||||
echo "Adding OP25 service..."
|
||||
service_content="[Unit]
|
||||
Description=op25-multi_rx
|
||||
After=syslog.target network.target nss-lookup.target network-online.target
|
||||
Requires=network-online.target
|
||||
|
||||
[Service]
|
||||
User=1000
|
||||
Group=1000
|
||||
WorkingDirectory=$(pwd)/op25/gr-op25_repeater/apps
|
||||
ExecStart=/bin/bash -- op25-multi_rx.sh
|
||||
RestartSec=5
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target"
|
||||
|
||||
# Write the systemd service file
|
||||
echo "$service_content" > /etc/systemd/system/op25-multi_rx.service
|
||||
|
||||
# Reload systemd daemon
|
||||
systemctl daemon-reload
|
||||
|
||||
# Install OP25 using the provided installation script
|
||||
echo "Installing OP25..."
|
||||
./install.sh
|
||||
|
||||
echo "\n\n\t\tOP25 installation completed!\n\n"
|
||||
|
||||
# Setting permissions on the directories created
|
||||
cd $ogPwd
|
||||
chown -R 1000:1000 ./*
|
||||
chown 1000:1000 .env
|
||||
echo "Permissions set on the client directory!"
|
||||
|
||||
echo "\n\n\t\tNode installation Complete!"
|
||||
|
||||
# Prompt the user for reboot confirmation
|
||||
read -p "This script has installed all required components for the DRB client. Are you okay with rebooting? If not, you will have to reboot later before running the applications to finish the installation. (Reboot?: y/n): " confirm
|
||||
|
||||
# Convert user input to lowercase for case-insensitive comparison
|
||||
confirm="${confirm,,}"
|
||||
|
||||
#echo "To configure the app, please go to http://$nodeIP:$nodePort" # TODO - uncomment when webapp is built
|
||||
echo "Thank you for joining the network!"
|
||||
|
||||
if [[ "$confirm" == "y" && "$confirm" == "yes" ]]; then
|
||||
# Prompt user to press any key before rebooting
|
||||
read -rsp $'System will now reboot, press any key to continue or Ctrl+C to cancel...\n' -n1 key
|
||||
echo "Rebooting..."
|
||||
reboot
|
||||
else
|
||||
echo "Please restart your device to complete the installation"
|
||||
fi
|
||||
Reference in New Issue
Block a user