Merge pull request 'Implement Python Discord Bot to Handle Voice Connection' (#5) from feature/implement-discordpy into master
Reviewed-on: #5
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,3 +6,5 @@ node_modules/
|
||||
*.log
|
||||
*.txt
|
||||
*.env
|
||||
!requirements.txt
|
||||
*testOP25Dir/
|
||||
@@ -19,7 +19,6 @@ CLIENT_NAME=""
|
||||
CLIENT_IP=""
|
||||
CLIENT_PORT=3010
|
||||
CLIENT_LOCATION=""
|
||||
CLIENT_NEARBY_SYSTEMS=""
|
||||
CLIENT_ONLINE=true
|
||||
|
||||
# Configuration for the connection to the server
|
||||
|
||||
@@ -5,39 +5,20 @@ var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
var http = require('http');
|
||||
require('dotenv').config();
|
||||
const fs = require('fs');
|
||||
const { DebugBuilder } = require("./utilities/debugBuilder");
|
||||
const deployCommands = require('./utilities/deployCommands');
|
||||
const { checkIn } = require("./controllers/clientController");
|
||||
|
||||
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");
|
||||
const {
|
||||
Client,
|
||||
Events,
|
||||
Collection,
|
||||
GatewayIntentBits,
|
||||
MessageActionRow,
|
||||
MessageButton
|
||||
} = require('discord.js');
|
||||
|
||||
var app = express();
|
||||
var discordToken = process.env.TOKEN;
|
||||
var port = process.env.HTTP_PORT || '3010';
|
||||
|
||||
const discordClient = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.GuildVoiceStates
|
||||
]
|
||||
});
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
@@ -53,16 +34,13 @@ app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use('/', indexRouter);
|
||||
|
||||
// Discord bot control route
|
||||
app.use('/bot', (req, res, next) => {
|
||||
req.discordClient = discordClient; // Add the discord client to bot requests to be used downstream
|
||||
next();
|
||||
}, 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) => {
|
||||
@@ -116,54 +94,8 @@ async function runHTTPServer() {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Discord bot config
|
||||
|
||||
// Setup commands for the Discord bot
|
||||
discordClient.commands = new Collection();
|
||||
const commandsPath = path.join(__dirname, 'commands');
|
||||
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||
//const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
|
||||
for (const file of commandFiles) {
|
||||
const filePath = path.join(commandsPath, file);
|
||||
const command = require(filePath);
|
||||
log.DEBUG("Importing command: ", command.data.name);
|
||||
// Set a new item in the Collection
|
||||
// With the key as the command name and the value as the exported module
|
||||
discordClient.commands.set(command.data.name, command);
|
||||
}
|
||||
|
||||
// Run when the bot is ready
|
||||
discordClient.on('ready', () => {
|
||||
log.DEBUG(`Discord server up and running with client: ${discordClient.user.tag}`);
|
||||
log.INFO(`Logged in as ${discordClient.user.tag}!`);
|
||||
|
||||
// Deploy slash commands
|
||||
log.DEBUG("Deploying slash commands");
|
||||
deployCommands.deploy(discordClient.user.id, discordClient.guilds.cache.map(guild => guild.id));
|
||||
|
||||
log.DEBUG(`Starting HTTP Server`);
|
||||
log.DEBUG(`Starting HTTP Server`);
|
||||
runHTTPServer();
|
||||
|
||||
log.DEBUG("Checking in with the master server")
|
||||
checkIn();
|
||||
});
|
||||
|
||||
// Setup any additional event handlers
|
||||
const eventsPath = path.join(__dirname, 'events');
|
||||
if (fs.existsSync(eventsPath)) {
|
||||
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
|
||||
if (eventFiles.length > 0) {
|
||||
for (const file of eventFiles) {
|
||||
const filePath = path.join(eventsPath, file);
|
||||
const event = require(filePath);
|
||||
if (event.once) {
|
||||
discordClient.once(event.name, (...args) => event.execute(...args));
|
||||
} else {
|
||||
discordClient.on(event.name, (...args) => event.execute(...args));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
discordClient.login(discordToken); //Load Client Discord Token
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const app = require('../app');
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("client", "www");
|
||||
const http = require('http');
|
||||
const config = require('../config/clientConfig');
|
||||
const clientController = require('../controllers/clientController');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
app.set('port', config.clientConfig.port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
const server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(config.clientConfig.port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
const addr = server.address();
|
||||
const bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
log.DEBUG('Listening on ' + bind);
|
||||
|
||||
// check in with the server to add this node or come back online
|
||||
clientController.checkIn();
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("client", "ping");
|
||||
// Modules
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { join } = require("../controllers/commandController")
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('join')
|
||||
.setDescription('Join a voice channel'),
|
||||
example: "join",
|
||||
isPrivileged: false,
|
||||
async execute(interaction) {
|
||||
await join({ interaction: interaction });
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("client", "leave");
|
||||
// Modules
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { leave } = require("../controllers/commandController")
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('leave')
|
||||
.setDescription('Leave a voice channel'),
|
||||
example: "leave",
|
||||
isPrivileged: false,
|
||||
async execute(interaction) {
|
||||
await leave({ interaction: interaction })
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Utilities
|
||||
const { replyToInteraction } = require('../utilities/messageHandler.js');
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("client", "ping");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ping')
|
||||
.setDescription('Replies with your input!'),
|
||||
/*
|
||||
.addStringOption(option =>
|
||||
option.setName('input')
|
||||
.setDescription('The input to echo back')
|
||||
.setRequired(false)
|
||||
.addChoices()),
|
||||
*/
|
||||
example: "ping",
|
||||
isPrivileged: false,
|
||||
async execute(interaction) {
|
||||
try{
|
||||
await replyToInteraction(interaction, "Pong! I have Aids and now you do too!"); // TODO - Add insults as the response to this command
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
//await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("client", "status");
|
||||
// Modules
|
||||
const { status } = require('../controllers/commandController');
|
||||
// Utilities
|
||||
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('status')
|
||||
.setDescription('Check the status of the bot'),
|
||||
example: "status",
|
||||
isPrivileged: false,
|
||||
async execute(interaction) {
|
||||
await status({ interaction: interaction });
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Core config settings for the node, these are the settings that are checked with the server
|
||||
exports.nodeConfig = {
|
||||
"id": 0,
|
||||
"name": "",
|
||||
"ip": "",
|
||||
"port": 0,
|
||||
"location": "",
|
||||
"nearbySystems": {
|
||||
"System Name": {
|
||||
"frequencies": [],
|
||||
"mode": "",
|
||||
"trunkFile": ""
|
||||
}
|
||||
},
|
||||
"online": false
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"},"coppies":{"frequencies":[154875000],"mode":"nbfm","trunkFile":"none"}}
|
||||
{"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"},"coppies":{"frequencies":[154690000],"mode":"nbfm","trunkFile":"none"},"poopoo":{"frequencies":[479135500],"mode":"nbfm","trunkFile":"none"},"ppeeeeeeeeee":{"frequencies":[479135500,133990000,133000000,555999000],"mode":"p25","trunkFile":"none"}}
|
||||
@@ -1,88 +0,0 @@
|
||||
// Config
|
||||
const { getDeviceID } = require('../utilities/configHandler.js');
|
||||
// Modules
|
||||
const portAudio = require('naudiodon');
|
||||
const { returnAlsaDeviceObject } = require("../utilities/executeConsoleCommands.js");
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
// Global Vars
|
||||
const log = new DebugBuilder("client", "audioController");
|
||||
|
||||
/**
|
||||
* Checks to make sure the selected audio device is available and returns the device object (PortAudio Device Info)
|
||||
* At least one option must be supplied, it will prefer ID to device name
|
||||
*
|
||||
* @param deviceName The name of the device being queried
|
||||
* @param deviceId The ID of the device being queried
|
||||
* @returns {unknown}
|
||||
*/
|
||||
async function confirmAudioDevice({deviceName = undefined, deviceId = undefined}){
|
||||
const deviceList = await getAudioDevices();
|
||||
if (!deviceName && !deviceId) throw new Error("No device given");
|
||||
let confirmedDevice;
|
||||
if (deviceId) confirmedDevice = deviceList.find(device => device.id === deviceId);
|
||||
if (deviceName) confirmedDevice = deviceList.find(device => device.name === deviceName);
|
||||
|
||||
log.DEBUG("Confirmed Audio Device: ", confirmedDevice);
|
||||
|
||||
return confirmedDevice;
|
||||
}
|
||||
exports.confirmAudioDevice = confirmAudioDevice;
|
||||
|
||||
/**
|
||||
* Return a list of the audio devices connected with input channels
|
||||
*
|
||||
* @returns {unknown[]}
|
||||
*/
|
||||
async function getAudioDevices(){
|
||||
// Exec output contains both stderr and stdout outputs
|
||||
//const deviceList = await returnAlsaDeviceObject();
|
||||
const deviceList = portAudio.getDevices().map((device) => {
|
||||
if (device.maxInputChannels > 2) {
|
||||
return device;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}).filter(Boolean);
|
||||
log.VERBOSE("Device List: ", deviceList);
|
||||
return deviceList;
|
||||
}
|
||||
exports.getAudioDevices = getAudioDevices;
|
||||
|
||||
/**
|
||||
* Create and return the audio instance from the saved settings
|
||||
* TODO Allow the client to save and load these settings dynamically
|
||||
*
|
||||
* @returns new portAudio.AudioIO
|
||||
*/
|
||||
async function createAudioInstance() {
|
||||
const selectedDevice = await confirmAudioDevice({deviceId: getDeviceID()});//{deviceName: "VoiceMeeter VAIO3 Output (VB-Au"});
|
||||
log.DEBUG("Device selected from config: ", selectedDevice);
|
||||
// Create an instance of AudioIO with outOptions (defaults are as below), which will return a WritableStream
|
||||
|
||||
return new portAudio.AudioIO({
|
||||
inOptions: {
|
||||
channelCount: 2,
|
||||
sampleFormat: portAudio.SampleFormat16Bit,
|
||||
sampleRate: 48000,
|
||||
deviceId: selectedDevice.id, // Use -1 or omit the deviceId to select the default device
|
||||
closeOnError: false, // Close the stream if an audio error is detected, if set false then just log the error
|
||||
framesPerBuffer: 20, //(48000 / 1000) * 20, //(48000 * 16 * 2) / 1000 * 20 // (48000 * (16 / 8) * 2) / 60 / 1000 * 20 //0.025 * 48000 / 2
|
||||
highwaterMark: 3840,
|
||||
},
|
||||
});
|
||||
/*
|
||||
return new alsaInstance({
|
||||
channels: 2,
|
||||
format: "U8",
|
||||
rate: 48000,
|
||||
device: selectedDevice.name ?? "default", // Omit the deviceId to select the default device
|
||||
periodSize: 100, //(48000 / 1000) * 20, //(48000 * 16 * 2) / 1000 * 20 // (48000 * (16 / 8) * 2) / 60 / 1000 * 20 //0.025 * 48000 / 2
|
||||
periodTime: undefined,
|
||||
// highwaterMark: 3840
|
||||
});
|
||||
*/
|
||||
|
||||
}
|
||||
exports.createAudioInstance = createAudioInstance;
|
||||
@@ -1,98 +1,82 @@
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("client", "clientController");
|
||||
// Modules
|
||||
const { status, join, leave } = require("./commandController")
|
||||
|
||||
/**
|
||||
* Get an object of client guilds
|
||||
* @param req The express request which includes the discord client
|
||||
* @returns
|
||||
*/
|
||||
function getGuilds(req) {
|
||||
return req.discordClient.guilds.cache.map(guild => guild.id)
|
||||
}
|
||||
const spawn = require('child_process').spawn;
|
||||
const { resolve } = require("path");
|
||||
const { closeProcessWrapper } = require("../utilities/utilities");
|
||||
|
||||
/**
|
||||
* Get an object of the channels in a guild
|
||||
* @param {*} guildId The Guild ID to check the channels of
|
||||
* @param {*} req The request object to use to check the discord client
|
||||
*/
|
||||
function getChannels(guildId, req) {
|
||||
const guild = req.discordClient.guilds.find(guildId);
|
||||
log.DEBUG("Found Guild channels with guild", guild.channels, guild);
|
||||
return guild.channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if a given guild has a given channel
|
||||
* @param {*} guildId The guild ID to check if the channel exists
|
||||
* @param {*} channelId The channel ID to check if exists in the guild
|
||||
* @param {*} req The express request param to use the discord client
|
||||
* @returns {true|false}
|
||||
*/
|
||||
function checkIfGuildHasChannel(guildId, channelId, req){
|
||||
const guildChannels = getChannels(guildId, req)
|
||||
const checkedChannel = guildChannels.find(c => c.id === channelId);
|
||||
if (!checkedChannel) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getGuildFromChannel(channelId, req){
|
||||
const channel = req.discordClient.channels.cache.get(channelId);
|
||||
|
||||
if (!channel) return new Error("Error getting channel from client");
|
||||
|
||||
if (channel.guild) return channel.guild;
|
||||
|
||||
return new Error("No Guild found with the given ID");
|
||||
}
|
||||
// Global vars
|
||||
let pythonProcess;
|
||||
|
||||
/**
|
||||
* Get Status of the discord process
|
||||
*/
|
||||
exports.getStatus = (req, res) => {
|
||||
log.INFO("Getting the status of the bot");
|
||||
guildIds = getGuilds(req);
|
||||
log.DEBUG("Guild IDs: ", guildIds);
|
||||
var guildStatuses = []
|
||||
for (const guildId of guildIds){
|
||||
status({guildID: guildId, callback: (statusObj) => {
|
||||
log.DEBUG("Status Object string: ", statusObj);
|
||||
guildStatuses.push(statusObj);
|
||||
}});
|
||||
}
|
||||
return res.status(200).json(guildStatuses);
|
||||
if (pythonProcess) return res.sendStatus(200);
|
||||
return res.sendStatus(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the bot and join the server and preset specified
|
||||
*/
|
||||
exports.joinServer = (req, res) => {
|
||||
const channelId = req.body.channelID;
|
||||
exports.joinServer = async (req, res) => {
|
||||
if (!req.body.clientId || !req.body.deviceId || !req.body.channelId) return res.send("500").json({"message": "You must include the client ID (discord token), device ID (ID of the audio device to use), channel ID (The discord ID of the channel to connect to)"});
|
||||
const deviceId = req.body.deviceId;
|
||||
const channelId = req.body.channelId;
|
||||
const clientId = req.body.clientId;
|
||||
const presetName = req.body.presetName;
|
||||
const guildObj = getGuildFromChannel(channelId, req);
|
||||
const NGThreshold = req.body.NGThreshold ?? 50
|
||||
|
||||
if (!channelId || !presetName || !guildObj) return res.status(400).json({'message': "Request does not have all components to proceed"});
|
||||
|
||||
// join the sever
|
||||
join({guildID: guildObj.id, guildObj: guildObj, channelID: channelId, callback: () => {
|
||||
return res.sendStatus(202);
|
||||
}});
|
||||
// Joining the discord server
|
||||
log.INFO("Join requested to: ", deviceId, channelId, clientId);
|
||||
if (process.platform === "win32") {
|
||||
log.DEBUG("Starting Windows Python");
|
||||
pythonProcess = await spawn('H:\\Logan\\Projects\\Python-Discord-Audio-Bot\\venv\\Scripts\\python.exe', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId, '-n', NGThreshold], { cwd: resolve(__dirname, "../pdab/").toString() });
|
||||
//pythonProcess = await spawn('C:\\Python310\\python.exe', [resolve(__dirname, "../PDAB/main.py"), deviceId, channelId, clientId, NGThreshold ]);
|
||||
}
|
||||
else {
|
||||
log.DEBUG("Starting Linux Python");
|
||||
pythonProcess = await spawn('python3', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId, NGThreshold ], { cwd: resolve(__dirname, "../pdab/") });
|
||||
}
|
||||
|
||||
log.VERBOSE("Python Process: ", pythonProcess);
|
||||
|
||||
let fullOutput;
|
||||
pythonProcess.stdout.setEncoding('utf8');
|
||||
pythonProcess.stdout.on("data", (data) => {
|
||||
log.VERBOSE("From Process: ", data);
|
||||
fullOutput += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.stderr.on('data', (data) => {
|
||||
log.VERBOSE(`stderr: ${data}`);
|
||||
fullOutput += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
log.DEBUG(`child process exited with code ${code}`);
|
||||
log.VERBOSE("Full output from bot: ", fullOutput);
|
||||
});
|
||||
|
||||
pythonProcess.on("error", (code, signal) => {
|
||||
log.ERROR("Error from the discord bot process: ", code, signal);
|
||||
});
|
||||
|
||||
// Starting the radio application
|
||||
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaves the server if it's in one
|
||||
*/
|
||||
exports.leaveServer = (req, res) => {
|
||||
exports.leaveServer = async (req, res) => {
|
||||
log.INFO("Leaving the server");
|
||||
const guildIds = getGuilds(req);
|
||||
log.DEBUG("Guild IDs: ", guildIds);
|
||||
for (const guildId of guildIds){
|
||||
leave({guildID: guildId, callback: (response) => {
|
||||
log.DEBUG("Response from leaving server on guild ID", guildId, response);
|
||||
}});
|
||||
}
|
||||
if (!pythonProcess) return res.sendStatus(200)
|
||||
|
||||
pythonProcess = await closeProcessWrapper(pythonProcess);
|
||||
|
||||
return res.sendStatus(202);
|
||||
}
|
||||
@@ -5,14 +5,13 @@ const log = new DebugBuilder("client", "clientController");
|
||||
require('dotenv').config();
|
||||
const modes = require("../config/modes");
|
||||
// Modules
|
||||
const { executeAsyncConsoleCommand } = require("../utilities/executeConsoleCommands.js");
|
||||
const { executeAsyncConsoleCommand, nodeObject } = require("../utilities/utilities");
|
||||
// Utilities
|
||||
const { updateId, updateConfig } = require("../utilities/updateConfig");
|
||||
const updatePreset = require("../utilities/updatePresets");
|
||||
const requests = require("../utilities/httpRequests");
|
||||
const { nodeObject } = require("../utilities/recordHelper.js");
|
||||
const { updatePreset, addNewPreset, getPresets, removePreset } = require("../utilities/updatePresets");
|
||||
const { onHttpError, requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
|
||||
|
||||
var runningClientConfig = new nodeObject({_id: process.env.CLIENT_ID, _ip: process.env.CLIENT_IP, _name: process.env.CLIENT_NAME, _port: process.env.CLIENT_PORT, _location: process.env.CLIENT_LOCATION, _nearbySystems: process.env.CLIENT_NEARBY_SYSTEMS, _online: process.env.CLIENT_ONLINE});
|
||||
var runningClientConfig = new nodeObject({_id: process.env.CLIENT_ID, _ip: process.env.CLIENT_IP, _name: process.env.CLIENT_NAME, _port: process.env.CLIENT_PORT, _location: process.env.CLIENT_LOCATION, _nearbySystems: getPresets(), _online: process.env.CLIENT_ONLINE});
|
||||
|
||||
/**
|
||||
* Check the body for the required fields to update or add a preset
|
||||
@@ -34,8 +33,7 @@ function checkBodyForPresetFields(req, res, callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
async function checkLocalIP() {
|
||||
let ipAddr;
|
||||
async function checkLocalIP() {
|
||||
if (process.platform === "win32") {
|
||||
// Windows
|
||||
var networkConfig = await executeAsyncConsoleCommand("ipconfig");
|
||||
@@ -98,28 +96,43 @@ exports.checkIn = async () => {
|
||||
let reqOptions;
|
||||
await this.checkConfig();
|
||||
// Check if there is an ID found, if not add the node to the server. If there was an ID, check in with the server to make sure it has the correct information
|
||||
if (runningClientConfig.id === 0) {
|
||||
// ID was not found in the config, creating a new node
|
||||
reqOptions = new requests.requestOptions("/nodes/newNode", "POST");
|
||||
requests.sendHttpRequest(reqOptions, JSON.stringify(), (responseObject) => {
|
||||
// Update the client's ID if the server accepted it
|
||||
if (responseObject.statusCode === 202) {
|
||||
runningClientConfig.id = responseObject.body.nodeId;
|
||||
updateId(responseObject.body.nodeId);
|
||||
}
|
||||
});
|
||||
try {
|
||||
if (runningClientConfig.id === 0) {
|
||||
// ID was not found in the config, creating a new node
|
||||
reqOptions = new requestOptions("/nodes/newNode", "POST");
|
||||
sendHttpRequest(reqOptions, JSON.stringify(), (responseObject) => {
|
||||
// Update the client's ID if the server accepted it
|
||||
if (responseObject.statusCode === 202) {
|
||||
runningClientConfig.id = responseObject.body.nodeId;
|
||||
updateId(responseObject.body.nodeId);
|
||||
}
|
||||
|
||||
if (responseObject.statusCode >= 300) {
|
||||
// Server threw an error
|
||||
onHttpError(responseObject.statusCode);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
else {
|
||||
// ID is in the config, checking in with the server
|
||||
reqOptions = new requestOptions("/nodes/nodeCheckIn", "POST");
|
||||
sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), (responseObject) => {
|
||||
if (responseObject.statusCode === 202) {
|
||||
// Server accepted an update
|
||||
}
|
||||
if (responseObject.statusCode === 200) {
|
||||
// Server accepted the response but there were no keys to be updated
|
||||
}
|
||||
if (responseObject.statusCode >= 300) {
|
||||
// Server threw an error
|
||||
onHttpError(responseObject.statusCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
// ID is in the config, checking in with the server
|
||||
reqOptions = new requests.requestOptions("/nodes/nodeCheckIn", "POST");
|
||||
requests.sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), (responseObject) => {
|
||||
if (responseObject.statusCode === 202) {
|
||||
// Server accepted an update
|
||||
}
|
||||
if (responseObject.statusCode === 200) {
|
||||
// Server accepted the response but there were no keys to be updated
|
||||
}
|
||||
});
|
||||
catch (err) {
|
||||
log.ERROR("Error checking in: ", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +148,7 @@ exports.requestCheckIn = async (req, res) => {
|
||||
* This is the endpoint wrapper to get the presets object
|
||||
*/
|
||||
exports.getPresets = async (req, res) => {
|
||||
return res.status(200).json(updatePreset.getPresets());
|
||||
return res.status(200).json(getPresets());
|
||||
}
|
||||
|
||||
/** Controller for the /client/updatePreset endpoint
|
||||
@@ -143,7 +156,7 @@ exports.getPresets = async (req, res) => {
|
||||
*/
|
||||
exports.updatePreset = async (req, res) => {
|
||||
checkBodyForPresetFields(req, res, () => {
|
||||
updatePreset.updatePreset(req.body.systemName, () => {
|
||||
updatePreset(req.body.systemName, () => {
|
||||
return res.sendStatus(200);
|
||||
}, {frequencies: req.body.frequencies, mode: req.body.mode, trunkFile: req.body.trunkFile});
|
||||
})
|
||||
@@ -154,10 +167,21 @@ exports.updatePreset = async (req, res) => {
|
||||
*/
|
||||
exports.addNewPreset = async (req, res) => {
|
||||
checkBodyForPresetFields(req, res, () => {
|
||||
updatePreset.addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => {
|
||||
addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => {
|
||||
return res.sendStatus(200);
|
||||
}, req.body.trunkFile);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a preset from the client
|
||||
*/
|
||||
exports.removePreset = async (req, res) => {
|
||||
checkBodyForPresetFields(req, res, () => {
|
||||
if (!req.body.systemName) return res.status("500").json({"message": "You must specify a system name to delete, this must match exactly to how the system name is saved."})
|
||||
removePreset(req.body.systemName, () => {
|
||||
return res.sendStatus(200);
|
||||
}, req.body.trunkFile);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
require('dotenv').config();
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("client", "commandController");
|
||||
// Modules
|
||||
const { joinVoiceChannel, VoiceConnectionStatus, getVoiceConnection, createAudioPlayer, createAudioResource, AudioPlayerStatus } = require("@discordjs/voice");
|
||||
const { OpusEncoder } = require("@discordjs/opus");
|
||||
const { calcRmsSync } = require("../utilities/rmsCalculator.js");
|
||||
// Utilities
|
||||
const {replyToInteraction} = require("../utilities/messageHandler.js");
|
||||
const {createAudioInstance} = require("../controllers/audioController.js");
|
||||
const { all } = require('../routes/index.js');
|
||||
|
||||
// Declare the encoder
|
||||
const encoder = new OpusEncoder(48000, 2);
|
||||
const noiseGateOpen = process.env.AUDIO_NOISE_GATE_OPEN ?? 100;
|
||||
|
||||
/**
|
||||
* Join the specified voice channel
|
||||
*
|
||||
* @param interaction Message interaction from discord
|
||||
* @param {string||any} guildID The specified Guild ID if this function is run from the client instead of from an interaction in Discord
|
||||
* @param {string||any} channelID The channel ID to join
|
||||
* @param guild The guild object to be used to create a voice adapter
|
||||
* @param {function} callback The callback that will be needed if this function is run with a Guild ID instead of an interaction
|
||||
*/
|
||||
exports.join = async function join({interaction= undefined, guildID= undefined, channelID = undefined, guildObj = undefined, callback = undefined}){
|
||||
if (interaction){
|
||||
const voiceChannel = interaction.options.getChannel('voicechannel');
|
||||
channelID = voiceChannel.id;
|
||||
guildID = interaction.guildId;
|
||||
guildObj = interaction.guild;
|
||||
if (interaction) replyToInteraction(interaction, `Ok, Joining ${voiceChannel.name}`);
|
||||
}
|
||||
log.DEBUG("Channel ID: ", channelID)
|
||||
log.DEBUG("Guild ID: ", guildID)
|
||||
|
||||
const vcConnectionObj = {
|
||||
channelId: channelID,
|
||||
guildId: guildID,
|
||||
adapterCreator: guildObj.voiceAdapterCreator,
|
||||
selfMute: false,
|
||||
selfDeaf: false,
|
||||
};
|
||||
|
||||
// Join the voice channel
|
||||
const voiceConnection = await joinVoiceChannel(vcConnectionObj);
|
||||
|
||||
// Create the audio stream instance
|
||||
const audioInstance = await createAudioInstance();
|
||||
log.VERBOSE("Audio Instance: ", audioInstance);
|
||||
|
||||
// Play audio data when it's received from the stream
|
||||
audioInstance.on('data', buffer => {
|
||||
buffer = Buffer.from(buffer);
|
||||
//log.VERBOSE("Audio buffer: ", buffer);
|
||||
// Check intensity of audio and only play when audio is present (no white noise/static)
|
||||
volume = Math.trunc(calcRmsSync(buffer, buffer.length));
|
||||
|
||||
if (parseInt(volume) > parseInt(noiseGateOpen)) {
|
||||
//log.VERBOSE("Noisegate and buffer volume: ", (parseInt(volume) > parseInt(noiseGateOpen)), noiseGateOpen, volume);
|
||||
const encodedBuffer = encoder.encode(buffer);
|
||||
voiceConnection.playOpusPacket(encodedBuffer);
|
||||
}
|
||||
});
|
||||
|
||||
audioInstance.start();
|
||||
|
||||
// Handle state changes in the voice connection
|
||||
voiceConnection.on('stateChange', async (oldState, newState) => {
|
||||
//log.VERBOSE("VoiceConnection state Changed from state: ", oldState, "\n\nto state: ", newState);
|
||||
log.DEBUG("VoiceConnection state Changed from: ", oldState.status, "to: ", newState.status);
|
||||
|
||||
// Ready -> Connecting
|
||||
if (oldState.status === VoiceConnectionStatus.Ready && newState.status === VoiceConnectionStatus.Connecting) {
|
||||
log.DEBUG("Configuring Network");
|
||||
voiceConnection.configureNetworking();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ready -> Disconnected
|
||||
if (oldState.status === VoiceConnectionStatus.Ready && newState.status === VoiceConnectionStatus.Disconnected) {
|
||||
log.DEBUG("Attempting to reconnect the voice connection");
|
||||
voiceConnection = joinVoiceChannel(vcConnectionObj);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ready
|
||||
if (newState.status === VoiceConnectionStatus.Ready){
|
||||
log.DEBUG("Bot connected to voice channel");
|
||||
return;
|
||||
}
|
||||
|
||||
// Destroyed
|
||||
if (newState.status === VoiceConnectionStatus.Destroyed){
|
||||
log.DEBUG("Quitting audio instance");
|
||||
audioInstance.quit();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
voiceConnection.on('error', async (error) => {
|
||||
log.ERROR("Voice Connection Error: ", error);
|
||||
})
|
||||
|
||||
if (guildID && callback) return callback();
|
||||
else return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If in a voice channel for the specified guild, leave
|
||||
*
|
||||
* @param interaction Message interaction from discord
|
||||
* @param guildID
|
||||
* @param callback
|
||||
*/
|
||||
exports.leave = async function leave({interaction = undefined, guildID= undefined, callback = undefined}) {
|
||||
if(interaction) {
|
||||
guildID = interaction.guild.id;
|
||||
}
|
||||
const voiceConnection = getVoiceConnection(guildID);
|
||||
|
||||
let response;
|
||||
if (!voiceConnection){
|
||||
response = "Not in a voice channel."
|
||||
if (interaction) return replyToInteraction(interaction, response);
|
||||
else callback(response);
|
||||
}
|
||||
voiceConnection.destroy();
|
||||
|
||||
response = "Goodbye"
|
||||
if (interaction) return replyToInteraction(interaction, response);
|
||||
else callback(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the voice status of the bots
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
exports.status = async function status({interaction= undefined, guildID= undefined, callback = undefined}) {
|
||||
//if (!interaction && !guildID) // Need error of sorts
|
||||
if (interaction){
|
||||
guildID = interaction.guild.id;
|
||||
}
|
||||
const voiceConnection = getVoiceConnection(guildID);
|
||||
|
||||
const statusObj = {
|
||||
"guildID": guildID,
|
||||
"voiceConnection": typeof g !== 'undefined' ? true : false // True if there is a voice connection, false if undefined
|
||||
}
|
||||
|
||||
log.DEBUG('Status Object: ', statusObj);
|
||||
|
||||
// get the status and return it accordingly (message reply / module)
|
||||
|
||||
if (interaction) {
|
||||
return replyToInteraction(interaction, "Pong! I have Aids and now you do too!");
|
||||
}
|
||||
else {
|
||||
callback(statusObj);
|
||||
}
|
||||
}
|
||||
@@ -2,147 +2,72 @@
|
||||
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 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 = (req, res) => {
|
||||
if (!radioChildProcess) return res.sendStatus(200)
|
||||
tempRes = res;
|
||||
radioChildProcess.kill();
|
||||
radioChildProcess = undefined;
|
||||
exports.closeRadioSession = async (req, res) => {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current 'cfg.json' file to the preset specified
|
||||
* @param {string} presetName
|
||||
*/
|
||||
exports.changeCurrentConfig = (req, res) => {
|
||||
// Check if the given config is saved
|
||||
log.DEBUG("[/radio/changeCurrentConfig] - Checking if provided preset is in the config");
|
||||
if (!checkIfPresetExists(req.body.presetName)) return res.status(500).JSON("No preset with given name found in config"); // No preset with the given name is in the config
|
||||
exports.changeCurrentConfig = async (req, res) => {
|
||||
const presetName = req.body.presetName;
|
||||
if (!presetName) return res.status(500).json("You must include the preset name")
|
||||
|
||||
// Check if the current config is the same as the preset given
|
||||
const currentConfig = readOP25Config();
|
||||
if (currentConfig.channels && currentConfig.channels.name === req.body.presetName) {
|
||||
log.DEBUG("[/radio/changeCurrentConfig] - Current config is the same as the preset given");
|
||||
return res.sendStatus(202);
|
||||
const updatedConfigObject = await changeCurrentConfigWrapper(presetName);
|
||||
|
||||
// No change was made to the config
|
||||
if (!updatedConfigObject) return res.sendStatus(200);
|
||||
|
||||
// Error was encountered
|
||||
if (typeof updatedConfigObject === "string") return res.status(500).json(updatedConfigObject);
|
||||
|
||||
// There was a change made to the config, reopening the radio session if it was open
|
||||
if (radioChildProcess) {
|
||||
log.DEBUG("Radio session open, restarting to accept the new config");
|
||||
const radioSessionResult = await 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);
|
||||
}
|
||||
|
||||
// Convert radioPreset to OP25 'cfg.json. file
|
||||
log.DEBUG("[/radio/changeCurrentConfig] - Converting radioPreset to OP25 config");
|
||||
const updatedConfigObject = convertRadioPresetsToOP25Config(req.body.presetName);
|
||||
|
||||
// Replace current JSON file with the updated file
|
||||
writeOP25Config(updatedConfigObject, () => {
|
||||
res.sendStatus(200);
|
||||
})
|
||||
|
||||
return res.sendStatus(202);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new OP25 process tuned to the specified system
|
||||
*/
|
||||
exports.openRadioSession = () => {
|
||||
if (radioChildProcess) closeRadioSession();
|
||||
radioChildProcess = spawn(getRadioBinPath());
|
||||
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 openRadioSessionWrapper(radioChildProcess, presetName);
|
||||
|
||||
// throw an error to the client if the wrapper ran into an error
|
||||
if (typeof radioSessionResult === "string") return res.status(500).json(updatedConfigObject);
|
||||
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the location of the 'multi_rx.py' binary from the config
|
||||
* Attach the radio session to the request to be used elsewhere
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
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}'`);
|
||||
return JSON.parse(fs.readFileSync(configPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
5
Client/pdab/.gitignore
vendored
Normal file
5
Client/pdab/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
*venv/
|
||||
*__pycache__/
|
||||
*.html
|
||||
*.exe
|
||||
LICENSE
|
||||
215
Client/pdab/NoiseGatev2.py
Normal file
215
Client/pdab/NoiseGatev2.py
Normal file
@@ -0,0 +1,215 @@
|
||||
import audioop
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
|
||||
import pyaudio
|
||||
import discord
|
||||
import numpy
|
||||
|
||||
voice_connection = None
|
||||
|
||||
LOGGER = logging.getLogger("Discord_Radio_Bot.NoiseGateV2")
|
||||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
class AudioStream:
|
||||
def __init__(self, _channels: int = 2, _sample_rate: int = 48000, _frames_per_buffer: int = 1024,
|
||||
_input_device_index: int = None, _output_device_index: int = None, _input: bool = True,
|
||||
_output: bool = True, _init_on_startup: bool = True):
|
||||
self.paInstance_kwargs = {
|
||||
'format': pyaudio.paInt16,
|
||||
'channels': _channels,
|
||||
'rate': _sample_rate,
|
||||
'input': _input,
|
||||
'output': _output,
|
||||
'frames_per_buffer': _frames_per_buffer
|
||||
}
|
||||
|
||||
if _input_device_index:
|
||||
if _input:
|
||||
self.paInstance_kwargs['input_device_index'] = _input_device_index
|
||||
else:
|
||||
LOGGER.warning(f"[AudioStream.__init__]:\tInput was not enabled."
|
||||
f" Reinitialize with '_input=True'")
|
||||
|
||||
if _output_device_index:
|
||||
if _output:
|
||||
self.paInstance_kwargs['output_device_index'] = _output_device_index
|
||||
else:
|
||||
LOGGER.warning(f"[AudioStream.__init__]:\tOutput was not enabled."
|
||||
f" Reinitialize with '_output=True'")
|
||||
|
||||
if _init_on_startup:
|
||||
# Init PyAudio instance
|
||||
LOGGER.info("Creating PyAudio instance")
|
||||
self.paInstance = pyaudio.PyAudio()
|
||||
|
||||
# Define and initialize stream object if we have been passed a device ID (pyaudio.open)
|
||||
self.stream = None
|
||||
|
||||
if _output_device_index or _input_device_index:
|
||||
if _init_on_startup:
|
||||
LOGGER.info("Init stream")
|
||||
self.init_stream()
|
||||
|
||||
def init_stream(self, _new_output_device_index: int = None, _new_input_device_index: int = None):
|
||||
# Check what device was asked to be changed (or set)
|
||||
if _new_input_device_index:
|
||||
if self.paInstance_kwargs['input']:
|
||||
self.paInstance_kwargs['input_device_index'] = _new_input_device_index
|
||||
else:
|
||||
LOGGER.warning(f"[AudioStream.init_stream]:\tInput was not enabled when initialized."
|
||||
f" Reinitialize with '_input=True'")
|
||||
|
||||
if _new_output_device_index:
|
||||
if self.paInstance_kwargs['output']:
|
||||
self.paInstance_kwargs['output_device_index'] = _new_output_device_index
|
||||
else:
|
||||
LOGGER.warning(f"[AudioStream.init_stream]:\tOutput was not enabled when initialized."
|
||||
f" Reinitialize with '_output=True'")
|
||||
|
||||
self.close_if_open()
|
||||
|
||||
# Open the stream
|
||||
self.stream = self.paInstance.open(**self.paInstance_kwargs)
|
||||
|
||||
def close_if_open(self):
|
||||
# Stop the stream if it is started
|
||||
if self.stream:
|
||||
if self.stream.is_active():
|
||||
self.stream.stop_stream()
|
||||
self.stream.close()
|
||||
LOGGER.debug(f"[ReopenStream.close_if_open]:\t Stream was open; It was closed.")
|
||||
|
||||
def list_devices(self, _display_input_devices: bool = True, _display_output_devices: bool = True):
|
||||
LOGGER.info('Getting a list of the devices connected')
|
||||
info = self.paInstance.get_host_api_info_by_index(0)
|
||||
numdevices = info.get('deviceCount')
|
||||
|
||||
devices = {
|
||||
'Input': {},
|
||||
'Output': {}
|
||||
}
|
||||
for i in range(0, numdevices):
|
||||
if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels')) > 0:
|
||||
input_device = self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name')
|
||||
devices['Input'][i] = input_device
|
||||
if _display_input_devices:
|
||||
LOGGER.debug(f"Input Device id {i} - {input_device}")
|
||||
|
||||
if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxOutputChannels')) > 0:
|
||||
output_device = self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name')
|
||||
devices['Output'][i] = output_device
|
||||
if _display_output_devices:
|
||||
LOGGER.debug(f"Output Device id {i} - {output_device}")
|
||||
|
||||
return devices
|
||||
|
||||
async def stop(self):
|
||||
await voice_connection.disconnect()
|
||||
self.close_if_open()
|
||||
self.stream.close()
|
||||
self.paInstance.terminate()
|
||||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
class NoiseGate(AudioStream):
|
||||
def __init__(self, _voice_connection, _noise_gate_threshold: int, **kwargs):
|
||||
super(NoiseGate, self).__init__(_init_on_startup=True, **kwargs)
|
||||
global voice_connection
|
||||
voice_connection = _voice_connection
|
||||
self.THRESHOLD = _noise_gate_threshold
|
||||
self.NGStream = NoiseGateStream(self)
|
||||
self.Voice_Connection_Thread = None
|
||||
|
||||
def run(self) -> None:
|
||||
global voice_connection
|
||||
# Start the audio stream
|
||||
LOGGER.debug(f"Starting stream")
|
||||
self.stream.start_stream()
|
||||
# Start the stream to discord
|
||||
self.core()
|
||||
|
||||
def core(self, error=None):
|
||||
if error:
|
||||
LOGGER.warning(error)
|
||||
|
||||
while not voice_connection.is_connected():
|
||||
time.sleep(.2)
|
||||
|
||||
if not voice_connection.is_playing():
|
||||
LOGGER.debug(f"Playing stream to discord")
|
||||
voice_connection.play(self.NGStream, after=self.core)
|
||||
|
||||
async def close(self):
|
||||
LOGGER.debug(f"Closing")
|
||||
await voice_connection.disconnect()
|
||||
if self.stream.is_active:
|
||||
self.stream.stop_stream()
|
||||
LOGGER.debug(f"Stopping stream")
|
||||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
class NoiseGateStream(discord.AudioSource):
|
||||
def __init__(self, _stream):
|
||||
super(NoiseGateStream, self).__init__()
|
||||
self.stream = _stream # The actual audio stream object
|
||||
self.NG_fadeout = 240/20 # Fadeout value used to hold the noisegate after de-triggering
|
||||
self.NG_fadeout_count = 0 # A count set when the noisegate is triggered and was de-triggered
|
||||
self.process_set_count = 0 # Counts how many processes have been made
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
while voice_connection.is_connected():
|
||||
curr_buffer = bytearray(self.stream.stream.read(960))
|
||||
buffer_rms = audioop.rms(curr_buffer, 2)
|
||||
if buffer_rms > 0:
|
||||
buffer_decibel = 20 * math.log10(buffer_rms)
|
||||
|
||||
if self.process_set_count % 10 == 0:
|
||||
if buffer_decibel >= self.stream.THRESHOLD:
|
||||
LOGGER.debug(f"[Noisegate Open] {buffer_decibel} db")
|
||||
else:
|
||||
LOGGER.debug(f"[Noisegate Closed] {buffer_decibel} db")
|
||||
|
||||
if buffer_decibel >= self.stream.THRESHOLD:
|
||||
self.NG_fadeout_count = self.NG_fadeout
|
||||
self.process_set_count += 1
|
||||
if curr_buffer:
|
||||
return bytes(curr_buffer)
|
||||
|
||||
else:
|
||||
if self.NG_fadeout_count > 0:
|
||||
self.NG_fadeout_count -= 1
|
||||
LOGGER.debug(f"Frames in fadeout remaining: {self.NG_fadeout_count}")
|
||||
self.process_set_count += 1
|
||||
if curr_buffer:
|
||||
return bytes(curr_buffer)
|
||||
|
||||
except OSError as e:
|
||||
LOGGER.warning(e)
|
||||
pass
|
||||
|
||||
def audio_datalist_set_volume(self, datalist, volume):
|
||||
""" Change value of list of audio chunks """
|
||||
sound_level = (volume / 100.)
|
||||
|
||||
for i in range(len(datalist)):
|
||||
chunk = numpy.fromstring(datalist[i], numpy.int16)
|
||||
|
||||
chunk = chunk * sound_level
|
||||
|
||||
datalist[i] = chunk.astype(numpy.int16)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
input_index = int(input("Input:\t"))
|
||||
output_index = int(input("Output:\t"))
|
||||
|
||||
ng = NoiseGate(_input_device_index=input_index, _output_device_index=output_index)
|
||||
|
||||
ng.list_devices()
|
||||
|
||||
ng.start()
|
||||
11
Client/pdab/getDevices.py
Normal file
11
Client/pdab/getDevices.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from NoiseGatev2 import AudioStream
|
||||
|
||||
print('Getting a list of devices')
|
||||
list_of_devices = AudioStream().list_devices()
|
||||
print("----- INPUT DEVICES -----")
|
||||
for inputDevice in list_of_devices['Input']:
|
||||
print(f"{inputDevice}\t-\t{list_of_devices['Input'][inputDevice]}")
|
||||
|
||||
print("----- OUTPUT DEVICES -----")
|
||||
for outputDevice in list_of_devices['Output']:
|
||||
print(f"{outputDevice}\t-\t{list_of_devices['Output'][outputDevice]}")
|
||||
75
Client/pdab/main.py
Normal file
75
Client/pdab/main.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import argparse, platform, os
|
||||
from discord import Intents, Client, Member, opus
|
||||
from discord.ext import commands
|
||||
from NoiseGatev2 import NoiseGate
|
||||
|
||||
# Load the proper OPUS library for the device being used
|
||||
async def load_opus():
|
||||
# Check the system type and load the correct library
|
||||
# Linux ARM AARCH64 running 32bit OS
|
||||
processor = platform.machine()
|
||||
print("Processor: ", processor)
|
||||
if os.name == 'nt':
|
||||
if processor == "AMD64":
|
||||
print(f"Loaded OPUS library for AMD64")
|
||||
opus.load_opus('./opus/libopus_amd64.dll')
|
||||
return "AMD64"
|
||||
else:
|
||||
if processor == "aarch64":
|
||||
print(f"Loaded OPUS library for aarch64")
|
||||
opus.load_opus('./opus/libopus_aarcch64.so')
|
||||
return "aarch64"
|
||||
elif processor == "armv7l":
|
||||
print(f"Loaded OPUS library for armv7l")
|
||||
opus.load_opus('./opus/libopus_armv7l.so')
|
||||
return "armv7l"
|
||||
|
||||
|
||||
def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY', channelId=367396189529833476, NGThreshold=50, deviceId=1):
|
||||
intents = Intents.default()
|
||||
|
||||
client = commands.Bot(command_prefix='!', intents=intents)
|
||||
|
||||
@client.event
|
||||
async def on_ready():
|
||||
print(f'We have logged in as {client.user}')
|
||||
|
||||
channelIdToJoin = client.get_channel(channelId)
|
||||
print("Channel", channelIdToJoin)
|
||||
|
||||
print("Loading opus")
|
||||
await load_opus()
|
||||
|
||||
if opus.is_loaded():
|
||||
print("Joining voice")
|
||||
channelConnection = await channelIdToJoin.connect(timeout=60.0, reconnect=True)
|
||||
print("Voice Connected")
|
||||
streamHandler = NoiseGate(
|
||||
_input_device_index=deviceId,
|
||||
_voice_connection=channelConnection,
|
||||
_noise_gate_threshold=NGThreshold)
|
||||
# Start the audio stream
|
||||
streamHandler.run()
|
||||
print("stream running")
|
||||
|
||||
|
||||
client.run(clientId)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("deviceId", type=int, help="The ID of the audio device to use")
|
||||
parser.add_argument("channelId", type=int, help="The ID of the voice channel to use")
|
||||
parser.add_argument("clientId", type=str, help="The discord client ID")
|
||||
parser.add_argument("-n", "--NGThreshold", type=int, help="Change the noisegate threshold. This defaults to 50")
|
||||
args = parser.parse_args()
|
||||
|
||||
if (not args.NGThreshold):
|
||||
args.NGThreshold = 50
|
||||
|
||||
print("Arguments:", args)
|
||||
|
||||
main(
|
||||
clientId=args.clientId,
|
||||
channelId=args.channelId,
|
||||
NGThreshold=args.NGThreshold,
|
||||
deviceId=args.deviceId
|
||||
)
|
||||
BIN
Client/pdab/opus/libopus_aarcch64.so
Normal file
BIN
Client/pdab/opus/libopus_aarcch64.so
Normal file
Binary file not shown.
BIN
Client/pdab/opus/libopus_amd64.dll
Normal file
BIN
Client/pdab/opus/libopus_amd64.dll
Normal file
Binary file not shown.
BIN
Client/pdab/opus/libopus_armv7l.so
Normal file
BIN
Client/pdab/opus/libopus_armv7l.so
Normal file
Binary file not shown.
5
Client/pdab/requirements.txt
Normal file
5
Client/pdab/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
discord^=2.2.3
|
||||
PyNaCl^=1.5.0
|
||||
pyaudio^=0.2.13
|
||||
numpy^=1.24.3
|
||||
argparse
|
||||
@@ -5,7 +5,7 @@ const botController = require("../controllers/botController");
|
||||
/** GET bot status
|
||||
* Check to see if the bot is online and if so, if it is currently connected to anything
|
||||
*
|
||||
* The status of the bot: 200 = client is online but not connected to discord, 201 = online on discord, 202 = connected to a channel, 500 + JSON = encountered error
|
||||
* The status of the bot: 200 = connected to discord, 201 = not connected to discord, 500 + JSON = encountered error
|
||||
* @returns status
|
||||
*/
|
||||
router.get('/status', botController.getStatus);
|
||||
|
||||
@@ -2,22 +2,21 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
// Controllers
|
||||
const clientController = require("../controllers/clientController");
|
||||
const {requestCheckIn, getPresets, updatePreset, addNewPreset, removePreset} = require("../controllers/clientController");
|
||||
|
||||
/** GET Request a check in from the client
|
||||
* Queue the client to check in with the server
|
||||
*
|
||||
* The status of the checkin request: 200 = Queued
|
||||
*/
|
||||
router.get('/requestCheckIn', clientController.requestCheckIn);
|
||||
router.get('/requestCheckIn', requestCheckIn);
|
||||
|
||||
/** GET Object of all known presets
|
||||
* Query the client to get all the known presets
|
||||
*/
|
||||
router.get('/presets', clientController.getPresets);
|
||||
router.get('/presets', getPresets);
|
||||
|
||||
/** POST Update to preset
|
||||
* Join the channel specified listening to the specified freq/mode
|
||||
*
|
||||
* @param req The request sent from the master
|
||||
* @param {string} req.body.systemName The name of the system to be updated
|
||||
@@ -25,7 +24,7 @@ router.get('/presets', clientController.getPresets);
|
||||
* @param {string} req.body.mode The listening mode for the SDR
|
||||
* @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications
|
||||
*/
|
||||
router.post('/updatePreset', clientController.updatePreset);
|
||||
router.post('/updatePreset', updatePreset);
|
||||
|
||||
/** POST Add new preset
|
||||
* Join the channel specified listening to the specified freq/mode
|
||||
@@ -36,6 +35,14 @@ router.post('/updatePreset', clientController.updatePreset);
|
||||
* @param {string} req.body.mode The listening mode for the SDR
|
||||
* @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications
|
||||
*/
|
||||
router.post('/addPreset', clientController.addNewPreset);
|
||||
router.post('/addPreset', addNewPreset);
|
||||
|
||||
/** POST Remove a preset
|
||||
*
|
||||
* @param req The request sent from the master
|
||||
* @param {string} req.body.systemName The name of the system to be updated
|
||||
*/
|
||||
router.post('/removePreset', removePreset);
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -15,6 +15,7 @@ router.post('/start', radioController.openRadioSession);
|
||||
|
||||
/**
|
||||
* POST Close the current radio session
|
||||
* Response from the radio: 200: closed; 204: not connected
|
||||
*/
|
||||
router.post('/stop', radioController.closeRadioSession);
|
||||
|
||||
|
||||
@@ -17,10 +17,13 @@ apt-get update
|
||||
apt-get upgrade -y
|
||||
|
||||
# Install the necessary packages
|
||||
apt-get install -y nodejs npm libopus-dev gcc make alsa-utils libasound2 libasound2-dev libpulse-dev pulseaudio apulse
|
||||
apt-get install -y nodejs npm libopus-dev gcc make alsa-utils libasound2 libasound2-dev libpulse-dev pulseaudio apulse python3.9
|
||||
|
||||
# Ensure pulse audio is running
|
||||
pulseaudio
|
||||
|
||||
# Install the node packages from the project
|
||||
npm i
|
||||
npm i
|
||||
|
||||
# Install the python packages needed for the bot
|
||||
pip install -r
|
||||
@@ -1,49 +0,0 @@
|
||||
const { REST, Routes } = require('discord.js');
|
||||
|
||||
require('dotenv').config();
|
||||
const token = process.env.TOKEN;
|
||||
//const clientId = process.env.clientId;
|
||||
//const guildId = process.env.guildId;
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { DebugBuilder } = require("./debugBuilder");
|
||||
const log = new DebugBuilder("client", "deployCommands");
|
||||
|
||||
const commands = [];
|
||||
// Grab all the command files from the commands directory you created earlier
|
||||
const commandsPath = path.resolve(__dirname, '../commands');
|
||||
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||
|
||||
exports.deploy = (clientId, guildIDs) => {
|
||||
log.DEBUG("Deploying commands for: ", guildIDs);
|
||||
if (Array.isArray(guildIDs)) guildIDs = [guildIDs];
|
||||
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
|
||||
for (const file of commandFiles) {
|
||||
const command = require(`${path.resolve(commandsPath, file)}`);
|
||||
commands.push(command.data.toJSON());
|
||||
}
|
||||
|
||||
// Construct and prepare an instance of the REST module
|
||||
const rest = new REST({ version: '10' }).setToken(token);
|
||||
|
||||
// and deploy your commands!
|
||||
for (const guildId of guildIDs){
|
||||
(async () => {
|
||||
try {
|
||||
log.DEBUG(`Started refreshing ${commands.length} application (/) commands for guild ID: ${guildId}.`);
|
||||
// The put method is used to fully refresh all commands in the guild with the current set
|
||||
const data = await rest.put(
|
||||
Routes.applicationGuildCommands(clientId, guildId),
|
||||
{ body: commands },
|
||||
);
|
||||
|
||||
log.DEBUG(`Successfully reloaded ${data.length} application (/) commands for guild ID: ${guildId}.`);
|
||||
} catch (error) {
|
||||
// And of course, make sure you catch and log any errors!
|
||||
log.ERROR("ERROR Deploying commands: ", error, "Body from error: ", commands);
|
||||
}
|
||||
})()
|
||||
}
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
// Modules
|
||||
const { promisify } = require('util');
|
||||
const { exec } = require("child_process");
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
// Global Vars
|
||||
const log = new DebugBuilder("client", "executeConsoleCommands");
|
||||
const execCommand = promisify(exec);
|
||||
|
||||
|
||||
async function executeAsyncConsoleCommand(consoleCommand) {
|
||||
// Check to see if the command is a real command
|
||||
// TODO needs to be improved
|
||||
const acceptableCommands = [ "arecord -L", 'ipconfig', 'ip addr' ];
|
||||
if (!acceptableCommands.includes(consoleCommand)) {
|
||||
log.WARN("Console command is not acceptable: ", consoleCommand);
|
||||
return undefined;
|
||||
}
|
||||
log.DEBUG("Running console command: ", consoleCommand);
|
||||
|
||||
const tempOutput = await execCommand(consoleCommand);
|
||||
const output = tempOutput.stdout.trim();
|
||||
|
||||
log.DEBUG("Executed Console Command Response: ", output)
|
||||
|
||||
// TODO add some error checking
|
||||
return output;
|
||||
}
|
||||
exports.executeAsyncConsoleCommand = executeAsyncConsoleCommand;
|
||||
|
||||
async function returnAlsaDeviceObject() {
|
||||
const listAlsaDevicesCommand = "arecord -L";
|
||||
const commandResponse = await executeAsyncConsoleCommand(listAlsaDevicesCommand);
|
||||
const brokenCommand = String(commandResponse).split('\n');
|
||||
var devices = [];
|
||||
var i = 0;
|
||||
|
||||
for (const responseLine of brokenCommand) {
|
||||
if (String(responseLine) && !String(responseLine).match(/^\s/g)) {
|
||||
const tempDevice = {
|
||||
id: i,
|
||||
name: responseLine
|
||||
}
|
||||
devices.push(tempDevice);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
exports.returnAlsaDeviceObject = returnAlsaDeviceObject;
|
||||
@@ -5,10 +5,9 @@ const log = new DebugBuilder("client", "httpRequests");
|
||||
require('dotenv').config();
|
||||
// Modules
|
||||
const http = require("http");
|
||||
const { nodeObject } = require("./recordHelper.js");
|
||||
|
||||
exports.requestOptions = class requestOptions {
|
||||
constructor(path, method, hostname = undefined, headers = undefined, port = undefined) {
|
||||
constructor(path, method, hostname = undefined, headers = undefined, port = undefined) {
|
||||
if (method === "POST"){
|
||||
this.hostname = hostname ?? process.env.SERVER_HOSTNAME ?? process.env.SERVER_IP;
|
||||
this.path = path;
|
||||
@@ -32,12 +31,22 @@ exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callbac
|
||||
// Create the request
|
||||
const req = http.request(requestOptions, res => {
|
||||
res.on('data', (data) => {
|
||||
const responseObject = {
|
||||
"statusCode": res.statusCode,
|
||||
"body": JSON.parse(data)
|
||||
};
|
||||
log.DEBUG("Response Object: ", responseObject);
|
||||
callback(responseObject);
|
||||
if (res.statusCode >= 200 && res.statusCode <= 299) {
|
||||
const responseObject = {
|
||||
"statusCode": res.statusCode,
|
||||
"body": JSON.parse(data)
|
||||
};
|
||||
log.DEBUG("Response Object: ", responseObject);
|
||||
callback(responseObject);
|
||||
}
|
||||
if (res.statusCode >= 300) {
|
||||
const responseObject = {
|
||||
"statusCode": res.statusCode,
|
||||
"body": data
|
||||
};
|
||||
log.DEBUG("Response Object: ", responseObject);
|
||||
callback(responseObject);
|
||||
}
|
||||
})
|
||||
}).on('error', err => {
|
||||
log.ERROR('Error: ', err.message)
|
||||
@@ -47,4 +56,18 @@ exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callbac
|
||||
// Write the data to the request and send it
|
||||
req.write(data)
|
||||
req.end()
|
||||
}
|
||||
|
||||
exports.onHttpError = function onHttpError(httpStatusCode) {
|
||||
switch(httpStatusCode){
|
||||
case 404:
|
||||
// Endpoint not found
|
||||
log.WARN("404 received");
|
||||
break;
|
||||
default:
|
||||
// Unhandled HTTP error code
|
||||
log.ERROR("HTTP request returned with status: ", httpStatusCode)
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("client", "messageHandler");
|
||||
|
||||
exports.replyToInteraction = async function replyToInteraction(interaction, message){
|
||||
interaction.reply({ content: message, fetchReply: true })
|
||||
.then((message) => log.DEBUG(`Reply sent with content ${message.content}`))
|
||||
.catch((err) => log.ERROR(err));
|
||||
}
|
||||
@@ -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,26 +0,0 @@
|
||||
/**
|
||||
*
|
||||
*/
|
||||
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.nodeObject = nodeObject;
|
||||
@@ -1,21 +0,0 @@
|
||||
exports.calcRmsSync = (arr , n) => {
|
||||
var square = 0;
|
||||
var mean = 0;
|
||||
var root = 0;
|
||||
|
||||
// Calculate square.
|
||||
for (i = 0; i < n; i++) {
|
||||
square += Math.pow(arr[i], 2);
|
||||
}
|
||||
|
||||
// Calculate Mean.
|
||||
mean = (square / (n));
|
||||
|
||||
// Calculate Root.
|
||||
root = Math.sqrt(mean);
|
||||
|
||||
// Normalize the output
|
||||
root = root / 10
|
||||
|
||||
return root;
|
||||
}
|
||||
@@ -48,12 +48,14 @@ function convertFrequencyToHertz(frequency){
|
||||
if (Number.isInteger(frequency)) {
|
||||
log.DEBUG(`${frequency} is an integer.`);
|
||||
// Check to see if the frequency has the correct length
|
||||
if (frequency.toString().length >= 7 && frequency.toString().length <= 9) return frequency
|
||||
if (frequency >= 1000000) return frequency
|
||||
if (frequency >= 100 && frequency <= 999) return frequency * 1000000
|
||||
log.WARN("Frequency hasn't matched filters: ", frequency);
|
||||
}
|
||||
else {
|
||||
log.DEBUG(`${frequency} is a float value.`);
|
||||
// Convert to a string to remove the decimal in place and then correct the length
|
||||
return converter(frequency).from("MHz").to("Hz");
|
||||
return parseInt(converter(frequency).from("MHz").to("Hz"));
|
||||
}
|
||||
} else {
|
||||
log.DEBUG(`${frequency} is not a number`);
|
||||
@@ -116,5 +118,20 @@ exports.updatePreset = (systemName, callback, { frequencies = undefined, mode =
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
exports.removePreset = (systemName, callback) => {
|
||||
const presets = this.getPresets();
|
||||
// Check if a system name was passed
|
||||
if (systemName in presets) {
|
||||
delete presets[systemName];
|
||||
writePresets(presets, callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
264
Client/utilities/utilities.js
Normal file
264
Client/utilities/utilities.js
Normal file
@@ -0,0 +1,264 @@
|
||||
// Modules
|
||||
const { promisify } = require('util');
|
||||
const { exec, spawn } = require("child_process");
|
||||
const { resolve, dirname } = require('path');
|
||||
const radioConfigHelper = require("../utilities/radioConfigHelper");
|
||||
const presetWrappers = require("../utilities/updatePresets");
|
||||
const converter = require("convert-units");
|
||||
const fs = require('fs');
|
||||
require('dotenv').config();
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
// Global Vars
|
||||
const log = new DebugBuilder("client", "executeConsoleCommands");
|
||||
const execCommand = promisify(exec);
|
||||
const radioBinPath = process.env.OP25_BIN_PATH;
|
||||
|
||||
/**
|
||||
* An object containing the variables needed to run the local node
|
||||
*/
|
||||
exports.nodeObject = class nodeObject {
|
||||
/**
|
||||
*
|
||||
* @param {*} param0._id The ID of the node
|
||||
* @param {*} param0._name The name of the node
|
||||
* @param {*} param0._ip The IP that the master can contact the node at
|
||||
* @param {*} param0._port The port that the client is listening on
|
||||
* @param {*} param0._location The physical location of the node
|
||||
* @param {*} param0._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
|
||||
* @returns
|
||||
*/
|
||||
exports.executeAsyncConsoleCommand = async function executeAsyncConsoleCommand(consoleCommand) {
|
||||
// Check to see if the command is a real command
|
||||
// TODO needs to be improved
|
||||
const acceptableCommands = [ "arecord -L", 'ipconfig', 'ip addr' ];
|
||||
if (!acceptableCommands.includes(consoleCommand)) {
|
||||
log.WARN("Console command is not acceptable: ", consoleCommand);
|
||||
return undefined;
|
||||
}
|
||||
log.DEBUG("Running console command: ", consoleCommand);
|
||||
|
||||
const tempOutput = await execCommand(consoleCommand);
|
||||
const output = tempOutput.stdout.trim();
|
||||
|
||||
log.DEBUG("Executed Console Command Response: ", output)
|
||||
|
||||
// TODO add some error checking
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} process The process to close
|
||||
* @returns {undefined} Undefined to replace the existing process in the parent
|
||||
*/
|
||||
exports.closeProcessWrapper = async (process) => {
|
||||
log.INFO("Leaving the server");
|
||||
if (!process) return undefined;
|
||||
|
||||
// Try to close the process gracefully
|
||||
await process.kill(2);
|
||||
|
||||
// Wait 25 seconds and see if the process is still open, if it is force it close
|
||||
await setTimeout(async () => {
|
||||
if (process) await process.kill(9);
|
||||
}, 25000)
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This wrapper closes any open radio sessions and the opens a new one
|
||||
*
|
||||
* @returns {radioChildProcess} The process of the radio session for use
|
||||
*/
|
||||
exports.openRadioSessionWrapper = async (radioChildProcess, presetName) => {
|
||||
if (radioChildProcess) radioChildProcess = await this.closeProcessWrapper(radioChildProcess);
|
||||
|
||||
const configChangeResult = await this.changeCurrentConfigWrapper(presetName);
|
||||
|
||||
// Throw an error to the client if the config change ran into an error
|
||||
if (typeof configChangeResult === "string") return configChangeResult;
|
||||
|
||||
if (process.platform === "win32") {
|
||||
log.DEBUG("Starting Windows OP25");
|
||||
radioChildProcess = await spawn("C:\\Python310\\python.exe", [getRadioBinPath(), "-c", getRadioConfigPath()], { cwd: dirname(getRadioBinPath()) });
|
||||
}
|
||||
else {
|
||||
log.DEBUG("Starting Linux OP25");
|
||||
radioChildProcess = await spawn(getRadioBinPath(), ["-c", getRadioConfigPath()], { cwd: dirname(getRadioBinPath()) });
|
||||
}
|
||||
|
||||
log.VERBOSE("Radio Process: ", radioChildProcess);
|
||||
|
||||
let fullOutput;
|
||||
radioChildProcess.stdout.setEncoding('utf8');
|
||||
radioChildProcess.stdout.on("data", (data) => {
|
||||
log.VERBOSE("From Process: ", data);
|
||||
fullOutput += data.toString();
|
||||
});
|
||||
|
||||
radioChildProcess.stderr.on('data', (data) => {
|
||||
log.VERBOSE(`stderr: ${data}`);
|
||||
fullOutput += data.toString();
|
||||
});
|
||||
|
||||
radioChildProcess.on('close', (code) => {
|
||||
log.DEBUG(`child process exited with code ${code}`);
|
||||
log.VERBOSE("Full output from radio: ", fullOutput);
|
||||
});
|
||||
|
||||
radioChildProcess.on("error", (code, signal) => {
|
||||
log.ERROR("Error from the radio process: ", code, signal);
|
||||
});
|
||||
|
||||
// Starting the radio application
|
||||
|
||||
return radioChildProcess
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the OP25 config with a preset
|
||||
*
|
||||
* @param {*} presetName The preset name to update the OP25 config file with
|
||||
* @returns
|
||||
*/
|
||||
exports.changeCurrentConfigWrapper = async (presetName) => {
|
||||
// Check if the given config is saved
|
||||
log.DEBUG("Checking if provided preset is in the config");
|
||||
const presetIsPresent = await checkIfPresetExists(presetName);
|
||||
if (!presetIsPresent) return "No preset with given name found in config"; // No preset with the given name is in the config
|
||||
|
||||
// Check if the current config is the same as the preset given
|
||||
try {
|
||||
const currentConfig = readOP25Config();
|
||||
if (currentConfig.channels && currentConfig.channels.name === presetName) {
|
||||
log.DEBUG("Current config is the same as the preset given");
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
log.WARN("Problem reading the config file, overwriting with the new config", err);
|
||||
}
|
||||
|
||||
// Convert radioPreset to OP25 'cfg.json. file
|
||||
log.DEBUG("Converting radioPreset to OP25 config");
|
||||
const updatedConfigObject = convertRadioPresetsToOP25Config(presetName);
|
||||
|
||||
// Replace current JSON file with the updated file
|
||||
writeOP25Config(updatedConfigObject, () => {
|
||||
return updatedConfigObject;
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the location of the 'multi_rx.py' binary from the config
|
||||
*/
|
||||
function getRadioBinPath(){
|
||||
return resolve(radioBinPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of the config for the radio app (OP25) and set the global variable
|
||||
*/
|
||||
function getRadioConfigPath(){
|
||||
let radioConfigDirPath = dirname(getRadioBinPath());
|
||||
return resolve(`${radioConfigDirPath}/cfg.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the given config to the JSON file in OP25 the bin dir
|
||||
* @param config The full config to be written to the file
|
||||
* @param {function} callback The function to be called when this wrapper completes
|
||||
*/
|
||||
function writeOP25Config(config, callback = undefined) {
|
||||
log.DEBUG("Updating OP25 config with: ", config);
|
||||
fs.writeFile(getRadioConfigPath(), JSON.stringify(config), (err) => {
|
||||
// Error checking
|
||||
if (err) {
|
||||
log.ERROR(err);
|
||||
throw err;
|
||||
}
|
||||
log.DEBUG("Write Complete");
|
||||
if (callback) callback()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current config file in use by OP25
|
||||
* @returns {object|*} The parsed config object currently set in OP25
|
||||
*/
|
||||
function readOP25Config() {
|
||||
const configPath = getRadioConfigPath();
|
||||
log.DEBUG(`Reading from config path: '${configPath}'`);
|
||||
const readFile = fs.readFileSync(configPath);
|
||||
log.VERBOSE("File Contents: ", readFile.toString());
|
||||
return JSON.parse(readFile);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check to see if the preset name exists in the config
|
||||
* @param {string} presetName The system name as saved in the preset
|
||||
* @returns {true||false}
|
||||
*/
|
||||
function checkIfPresetExists(presetName) {
|
||||
const savedPresets = presetWrappers.getPresets();
|
||||
log.DEBUG("Found presets: ", savedPresets, presetName, Object.keys(savedPresets).includes(presetName));
|
||||
if (!Object.keys(savedPresets).includes(presetName)) return false;
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a radioPreset to OP25's cfg.json file
|
||||
*/
|
||||
function convertRadioPresetsToOP25Config(presetName){
|
||||
const savedPresets = presetWrappers.getPresets();
|
||||
let frequencyString = "";
|
||||
for (const frequency of savedPresets[presetName].frequencies){
|
||||
frequencyString += `${converter(frequency).from("Hz").to("MHz")},`
|
||||
}
|
||||
frequencyString = frequencyString.slice(0, -1);
|
||||
|
||||
let updatedOP25Config;
|
||||
switch (savedPresets[presetName].mode){
|
||||
case "p25":
|
||||
updatedOP25Config = new radioConfigHelper.P25({
|
||||
"systemName": presetName,
|
||||
"controlChannelsString": frequencyString,
|
||||
"tagsFile": savedPresets[presetName].trunkFile
|
||||
});
|
||||
break;
|
||||
case "nbfm":
|
||||
//code for nbfm here
|
||||
updatedOP25Config = new radioConfigHelper.NBFM({
|
||||
"frequency": frequencyString,
|
||||
"systemName": presetName
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error("Radio mode of selected preset not recognized");
|
||||
}
|
||||
|
||||
log.DEBUG(updatedOP25Config);
|
||||
return updatedOP25Config;
|
||||
}
|
||||
Reference in New Issue
Block a user