43 Commits

Author SHA1 Message Date
Logan Cusano
ca2815ab8f Improve the update script to use the owner of the git repo 2023-08-06 16:49:31 -04:00
Logan Cusano
556697725a Much improved install script for clients #6 2023-08-06 16:40:10 -04:00
Logan Cusano
b448f04aec Commenting out variables not set by the initial config 2023-08-06 16:27:04 -04:00
Logan Cusano
fae8417b2f Add log file location to the example ENV 2023-08-06 00:46:00 -04:00
Logan Cusano
e06cc4762d Using absolute file path for pdab binaries 2023-08-06 00:45:24 -04:00
Logan Cusano
6deba2bad2 Fix bug that used wrong env var for client port 2023-08-05 03:11:59 -04:00
Logan Cusano
e0d1a4a2fe Update client setup
- get user input for client config
- update pulseaudio setup to run for all users
- add op25 installer
2023-08-05 03:11:32 -04:00
1078faa766 Merge pull request '#37 Implement v1 Web Apps' (#41) from #37-implement-webapps into master
Reviewed-on: #41
2023-08-04 23:46:46 -04:00
Logan Cusano
75580c0547 Fix bug in httprequests, hostname was overwritten 2023-08-04 23:44:28 -04:00
Logan Cusano
880f1ccb01 Implemented v1 admin dashboard 2023-08-04 23:21:35 -04:00
Logan Cusano
76c4d002a0 Update to filter presets function in utils 2023-08-04 22:10:28 -04:00
Logan Cusano
2260deee01 Added filter presets function to utils and code formatted 2023-08-04 22:10:09 -04:00
Logan Cusano
8a0baa5bc9 Small tweaks
- Update local webUI to refresh when joining server to clear modal
- Updated client webUI page title
- Re-added join modal to node page (client & server)
- updated join modal to use node.nearbySystems
2023-07-22 15:01:48 -04:00
Logan Cusano
ec091c0017 Only concat the stored toasts with new toast when there are stored toasts 2023-07-22 14:46:51 -04:00
Logan Cusano
a5996ccfc0 Added page titles to server webUI 2023-07-22 04:21:06 -04:00
Logan Cusano
3b248e36ec Merge branches '#37-implement-webapps' and '#37-implement-webapps' of https://git.vpn.cusano.net/logan/DRB-CnC into #37-implement-webapps 2023-07-22 03:59:20 -04:00
Logan Cusano
abdb725964 Semi functional client webUI
- can update client info for sure
- Working on notifications now
2023-07-22 03:57:50 -04:00
Logan Cusano
167f87128e Check if the presets exist when going to get them
- Return empty object if no preset file is found
2023-07-22 03:57:50 -04:00
Logan Cusano
bc09840dda Ignore local radio presets 2023-07-22 03:57:50 -04:00
Logan Cusano
c680c8fb2c Fixed bug when editing node systems
- Name defaulted to new system name
2023-07-22 03:57:50 -04:00
Logan Cusano
4ceb71bd84 Remove specific presets and left an example file if needed for clients 2023-07-22 03:57:50 -04:00
Logan Cusano
4b86621626 Finalizing Server webUI
Still needed:
- Way to update clients' versions
- Way to delete nodes
- working dashboard
- working search function
2023-07-22 03:57:50 -04:00
Logan Cusano
d847aa4fc7 Add new node if node tries to check in with an ID not in the DB 2023-07-22 03:57:50 -04:00
Logan Cusano
9ff87403b2 Update request node checkin in update node 2023-07-22 03:57:50 -04:00
Logan Cusano
cf9deb4841 Remove unnecessary online param from webUI update node 2023-07-22 03:57:50 -04:00
Logan Cusano
58b4b7ff40 Update design on navbar and sidebar 2023-07-22 03:57:50 -04:00
Logan Cusano
6b4ffc88b3 Implemented functional method to add a new system to a new through web app 2023-07-22 03:57:50 -04:00
Logan Cusano
0f114066a6 Implement functioning method to update systems on web app 2023-07-22 03:57:50 -04:00
Logan Cusano
648782658c Update API and add webapp saving 2023-07-22 03:57:50 -04:00
Logan Cusano
d7ea6bbbd4 Updated NodeCard Design 2023-07-22 03:57:50 -04:00
Logan Cusano
6ffa12911a Update toast creator to display proper date string 2023-07-22 03:57:50 -04:00
Logan Cusano
61d7b69c10 Only show heartbeat toast once HTTP request is complete 2023-07-22 03:57:50 -04:00
Logan Cusano
c14316933b Update heartbeat function location and name 2023-07-22 03:57:50 -04:00
Logan Cusano
f55361575e #37 Working Joining and Leaving 2023-07-22 03:57:50 -04:00
Logan Cusano
c5f7cc1da6 Additional changes for #37
- Updating side bar
- Updating nav bar
- Adding node details page
- Adding controller page
- Updating routes
2023-07-22 03:57:50 -04:00
Logan Cusano
02854fb783 Initial bones for #37 2023-07-22 03:57:50 -04:00
Logan Cusano
4a54be7e51 Update jsDoc in utils 2023-07-22 03:57:50 -04:00
Logan Cusano
cfeea57744 Update jsDoc in nodeController 2023-07-22 03:57:50 -04:00
Logan Cusano
0a8dc75a93 Update adminController to use join/leave command wrappers 2023-07-22 03:57:50 -04:00
Logan Cusano
0426f5eb27 Add jsDoc to leaveServerWrapper 2023-07-22 03:57:50 -04:00
Logan Cusano
d4b974f81b Update join command to accept a specific node ID 2023-07-22 03:57:50 -04:00
Logan Cusano
d05c266f75 Added basic info to readmes 2023-07-22 03:51:46 -04:00
Logan Cusano
57fa6be110 Update Readmes (sort of) 2023-07-22 03:34:37 -04:00
24 changed files with 418 additions and 262 deletions

View File

@@ -17,4 +17,7 @@ SERVER_HOSTNAME=""
SERVER_PORT=3000
# Configuration of the local OP25 application
OP25_BIN_PATH=""
#OP25_BIN_PATH=""
# Logfile location config
#LOG_LOCATION=""

View File

@@ -17,7 +17,7 @@ var { attachRadioSessionToRequest } = require('./controllers/radioController');
const log = new DebugBuilder("client", "app");
var app = express();
var port = process.env.HTTP_PORT || '3010';
var port = process.env.CLIENT_PORT || '3010';
// view engine setup
app.set('views', path.join(__dirname, 'views'));

View File

@@ -8,20 +8,21 @@ async def load_opus():
# Check the system type and load the correct library
# Linux ARM AARCH64 running 32bit OS
processor = platform.machine()
script_dir = os.path.dirname(os.path.abspath(__file__))
print("Processor: ", processor)
if os.name == 'nt':
if processor == "AMD64":
opus.load_opus(os.path.join(script_dir, './opus/libopus_amd64.dll'))
print(f"Loaded OPUS library for AMD64")
opus.load_opus('./opus/libopus_amd64.dll')
return "AMD64"
else:
if processor == "aarch64":
opus.load_opus(os.path.join(script_dir, './opus/libopus_aarcch64.so'))
print(f"Loaded OPUS library for aarch64")
opus.load_opus('./opus/libopus_aarcch64.so')
return "aarch64"
elif processor == "armv7l":
opus.load_opus(os.path.join(script_dir, './opus/libopus_armv7l.so'))
print(f"Loaded OPUS library for armv7l")
opus.load_opus('./opus/libopus_armv7l.so')
return "armv7l"

View File

@@ -30,6 +30,7 @@ function addToastToStorage(time, message) {
var toasts = [{ 'time': time, 'message': message }]
var storedToasts = getStoredToasts();
console.log("Adding new notification to storage: ", toasts);
if (storedToasts) {
toasts = toasts.concat(storedToasts);
console.log("Combined new and stored notifications: ", toasts);
toasts = toasts.filter((value, index, self) =>
@@ -37,6 +38,7 @@ function addToastToStorage(time, message) {
t.time === value.time && t.message === value.message
))
)
}
console.log("Deduped stored notifications: ", toasts);
localStorage.setItem("toasts", JSON.stringify(toasts));
navbarUpdateNotificationBellCount(toasts);
@@ -251,8 +253,8 @@ function joinServer() {
const responseObject = JSON.parse(Http.responseText)
console.log(Http.status);
console.log(responseObject);
createToast(`${responseObject.name} will join shortly`, { showNow: true });
$("#joinModal").modal('toggle');
createToast(`${responseObject.name} will join shortly`);
location.reload();
}
}

View File

@@ -2,13 +2,13 @@
---
Explanation here
The client application communicates with the server through the provided API. Each client instance waits for join requests sent by users through Discord. Once a join request is received, the client uses the SDR application to tune into the specified radio preset. It then establishes a connection to Discord, allowing users to listen to the selected radio preset in real-time.
In addition to its interaction with the server, the client also has its own API and web application. This enables users to directly interface with the client, perform actions specific to the client application, and access relevant information about the connected SDR and radio presets.
## Requirements
---
Requirements here (not modules, that will be installed with npm)
### Hardware
- SBC

View File

@@ -5,21 +5,81 @@ if [ "$EUID" -ne 0 ]
then echo "Please run as root"
exit
fi
# Prompt the user for reboot confirmation
read -p "This script will install all required components for the DRB client. Are you okay with rebooting afterward? 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,,}"
if [[ "$confirm" != "y" && "$confirm" != "yes" ]]; then
echo "Script will not reboot."
should_reboot=false
else
echo "Script will reboot after completion."
should_reboot=true
fi
echo "----- Starting Radio Node Client Install Script -----"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
ls -ld $SCRIPT_DIR | awk '{print $3}' >> ./config/installerName
# Copy the example env file
cp .env.example .env
# Setup user for service
useradd -M RadioNode
usermod -s -L RadioNode
# Copy the radio config example file
cp config/radioPresets.json.EXAMPLE config/radioPresets.json
# Change the ownership of the directory to the service user
chown RadioNode -R $SCRIPT_DIR
echo "----- Collecting Setup Information -----"
# Check for updates
apt-get update
# Ask the user for input and store in variables
echo " \\Client Config"
read -p " Enter Node Name: " nodeName
read -p " Enter Node IP: " nodeIP
read -p " Enter Node Port: " nodePort
read -p " Enter Node Location: " nodeLocation
read -p " Enter Audio Device ID: " audioDeviceID
echo " \\Server Config"
read -p " Enter Server IP (leave blank if using hostname): " serverIP
if [ -z "$serverIP" ]; then
read -p " Enter Server Hostname: " serverHostname
fi
read -p " Enter Server Port: " serverPort
# Update the values in the env file using sed
sed -i "s/^AUDIO_DEVICE_ID=\".*\"$/AUDIO_DEVICE_ID=\"$audioDeviceID\"/" .env
sed -i "s/^CLIENT_NAME=\".*\"$/CLIENT_NAME=\"$nodeName\"/" .env
sed -i "s/^CLIENT_IP=\".*\"$/CLIENT_IP=\"$nodeIP\"/" .env
sed -i "s/^CLIENT_PORT=.*$/CLIENT_PORT=$nodePort/" .env
sed -i "s/^CLIENT_LOCATION=\".*\"$/CLIENT_LOCATION=\"$nodeLocation\"/" .env
if [ -z "$serverIP" ]; then
sed -i "s/^SERVER_HOSTNAME=\".*\"$/SERVER_HOSTNAME=\"$serverHostname\"/" .env
else
sed -i "s/^SERVER_IP=\".*\"$/SERVER_IP=\"$serverIP\"/" .env
fi
sed -i "s/^SERVER_PORT=\".*\"$/SERVER_PORT=\"$serverPort\"/" .env
echo "----- Config file has been updated -----"
# Display the updated values
echo "----- Start of Config File -----"
cat .env
echo "----- End of Config File -----"
echo "----- Getting Dependencies -----"
# 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
curl -fsSL https://deb.nodesource.com/setup_current.x | sudo -E bash -
# Update the system
@@ -27,16 +87,41 @@ apt-get update
apt-get upgrade -y
# Install the necessary packages
apt-get install -y nodejs portaudio19-dev libportaudio2 libpulse-dev pulseaudio apulse python3 python3-pip
# Ensure pulse audio is running
pulseaudio
apt-get install -y nodejs portaudio19-dev libportaudio2 libpulse-dev pulseaudio apulse python3 python3-pip git
# Install the node packages from the project
npm i
# Install the python packages needed for the bot
pip install -r
pip install -r ./pdab/requirements.txt
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
echo "----- Setting up Radio Node Service -----"
# Setup bot service
echo "[Unit]
@@ -44,32 +129,80 @@ Description=Radio Node Service
After=network.target
[Service]
WorkingDirectory="$SCRIPT_DIR"
WorkingDirectory=$SCRIPT_DIR/
ExecStart=/usr/bin/node .
Restart=always
RestartDelay=10
User=RadioNode
Environment=DEBUG='client:*'
Environment=\"DEBUG='client:*'\"
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNode.service
# Enable the Radio Node service
systemctl enable RadioNode.service
echo "----- Setting up Radio Node Update Service -----"
# Setup bot update service
echo "[Unit]
Description=Radio Node Updater Service
After=network.target
[Service]
WorkingDirectory="$SCRIPT_DIR"
WorkingDirectory=$SCRIPT_DIR/
ExecStart=/usr/bin/bash update.sh
Restart=on-failure
User=RadioNode
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNodeUpdater.service
# Enable the service
systemctl enable RadioNode.service
# Install OP25
echo "----- Installing OP25 from Source -----"
# Clone the OP25 Git
cd /opt/
git clone https://github.com/boatbod/op25.git
cd op25
# Start the service
systemctl start RadioNode.service
# Run the OP25 install script
bash ./install.sh
# Create the config file for the client or user to update later
cp /opt/op25/op25/gr-op25_repeater/apps/p25_rtl_example.json /opt/op25/op25/gr-op25_repeater/apps/radioNodeOP25Config.json
# Create the OP25 service
echo "[Unit]
Description=OP25 Service
After=network.target
[Service]
WorkingDirectory=/opt/op25/op25/gr-op25_repeater/apps
ExecStart=./multi_rx.py -c radioNodeOP25Config.json
Restart=always
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/OP25.service
echo "----- OP25 Setup Complete -----"
# Enable the OP25 service, don't start it though as the user needs to config
systemctl enable OP25.service
echo "----- OP25 Enabled; Please ensure to update the configuration and start the service -----"
# Move back to the directory that the user started in (might not be needed?)
cd $SCRIPT_DIR
echo "----- Setup Complete! -----"
# Reboot if the user confirmed earlier
if [ "$should_reboot" = true ]; then
echo "To configure the app, please go to http://$nodeIP:$nodePort"
echo "Thank you for joining the network!"
# 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 "To configure the app, please go to http://$nodeIP:$nodePort"
echo "Thank you for joining the network!"
echo "Please restart your device to complete the installation"
fi

View File

@@ -17,10 +17,12 @@ echo "<!-- UPDATING ---!>"
# Stop any running service
systemctl stop RadioNode
# Update the git Repo
installUser=$(cat ./config/installerName)
sudo su -l $installUser -c 'git fetch -a -p'
sudo su -l $installUser -c 'git pull'
# Get the owner of the current working directory
cwd_owner=$(stat -c '%U' .)
# Update the git Repo as the owner of the current working directory
sudo su -l $cwd_owner -c 'git fetch'
sudo su -l $cwd_owner -c 'git pull'
# Install any new libraries
npm i

View File

@@ -12,8 +12,8 @@ exports.requestOptions = class requestOptions {
if (["POST", "PUT"].includes(method)){
log.VERBOSE("Hostname Vars: ", hostname, process.env.SERVER_HOSTNAME, process.env.SERVER_IP);
if (hostname) this.hostname = hostname;
if (process.env.SERVER_HOSTNAME) this.hostname = process.env.SERVER_HOSTNAME;
if (process.env.SERVER_IP) this.hostname = process.env.SERVER_IP;
if (!this.hostname && process.env.SERVER_HOSTNAME) this.hostname = process.env.SERVER_HOSTNAME;
if (!this.hostname && process.env.SERVER_IP) this.hostname = process.env.SERVER_IP;
if (!this.hostname) throw new Error("No server hostname / IP was given when creating a request");
this.path = path;
this.port = port ?? process.env.SERVER_PORT;

View File

@@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<title>'<%=node.name%>' - Configuration</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
@@ -154,6 +154,8 @@
<% // new System Modal %>
<%- include("partials/modifySystemModal.ejs", {'system': "New System" , 'frequencies' : [], 'mode' : '' }) %>
<% // Join Server Modal %>
<%- include("partials/joinModal.ejs", {'node': node}) %>
</body>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>

View File

@@ -24,7 +24,7 @@
<div class="col-md-6">
<label class="small mb-1" for="selectRadioPreset">Selected Preset:</label>
<select class="custom-select" id="selectRadioPreset">
<% for(const system in nearbySystems) { %>
<% for(const system in node.nearbySystems) { %>
<option value="<%=system%>"><%=system%></option>
<% } %>
</select>

View File

@@ -1,7 +1,7 @@
// Modules
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { filterAutocompleteValues } = require("../utilities/utils");
const { filterAutocompleteValues, filterPresetsAvailable } = require("../utilities/utils");
const { getOnlineNodes, getAllConnections } = require("../utilities/mysqlHandler");
const { joinServerWrapper } = require("../controllers/adminController");
@@ -28,18 +28,7 @@ module.exports = {
recordResolve(nodeRows);
});
});
log.DEBUG("Node objects: ", nodeObjects);
var presetsAvailable = [];
for (const nodeObject of nodeObjects) {
log.DEBUG("Node object: ", nodeObject);
presetsAvailable.push.apply(presetsAvailable, nodeObject.presets);
}
log.DEBUG("All Presets available: ", presetsAvailable);
// Remove duplicates
options = [...new Set(presetsAvailable)];
log.DEBUG("DeDuped Presets available: ", options);
const options = await filterPresetsAvailable(nodeObjects);
// Filter the results to what the user is entering
filterAutocompleteValues(interaction, options);

View File

@@ -82,6 +82,7 @@ a {
position: relative;
margin-bottom: 30px;
box-shadow: 0 0.46875rem 2.1875rem rgba(90, 97, 105, 0.1), 0 0.9375rem 1.40625rem rgba(90, 97, 105, 0.1), 0 0.25rem 0.53125rem rgba(90, 97, 105, 0.12), 0 0.125rem 0.1875rem rgba(90, 97, 105, 0.1);
min-height: 85%;
}
.info-card .card-statistic .card-icon-large .bi {

View File

@@ -30,6 +30,7 @@ function addToastToStorage(time, message) {
var toasts = [{ 'time': time, 'message': message }]
var storedToasts = getStoredToasts();
console.log("Adding new notification to storage: ", toasts);
if (storedToasts) {
toasts = toasts.concat(storedToasts);
console.log("Combined new and stored notifications: ", toasts);
toasts = toasts.filter((value, index, self) =>
@@ -37,6 +38,7 @@ function addToastToStorage(time, message) {
t.time === value.time && t.message === value.message
))
)
}
console.log("Deduped stored notifications: ", toasts);
localStorage.setItem("toasts", JSON.stringify(toasts));
navbarUpdateNotificationBellCount(toasts);
@@ -254,7 +256,7 @@ function joinServer() {
console.log(Http.status);
console.log(responseObject);
createToast(`${responseObject.name} will join shortly`, { showNow: true });
$("#joinModal").modal('toggle');
location.reload();
}
}

View File

@@ -2,7 +2,11 @@
---
Overview here
The server application acts as the central hub within Discord, providing various functionalities and serving as the main point of communication for the clients. Some of the key features and responsibilities of the server include:
- **RSS Feed Updates**: The server periodically updates text channels with RSS feed updates, keeping users informed about the latest news or information.
- **Server Management Functions / User Requests**: The server includes management functions that allow administrators to control and configure various aspects of the server environment. Users can interact with the server through Discord commands, which range from requesting specific radio presets to updating RSS feeds.
- **API and Web Front End**: The server exposes an API and web front end, providing an interface to view and control all the online clients. This allows users to monitor and manage the available radio presets, as well as perform various administrative tasks.
## Requirements

View File

@@ -1,32 +1,25 @@
var express = require('express');
var router = express.Router();
const { getAllNodes, getNodeInfoFromId } = require("../utilities/mysqlHandler");
const { getAllNodes, getNodeInfoFromId, getAllConnections } = require("../utilities/mysqlHandler");
const { filterPresetsAvailable } = require("../utilities/utils");
/* GET home page. */
router.get('/', (req, res) => {
//var sources = libCore.getSources();
return res.render('index');
var htmlOutput = "";
sources.forEach(source => {
htmlOutput += `
<div style='margin-bottom:15px;'>
<div> Title: ${source.title} </div>
<div> Link: ${source.link} </div>
<div> category: ${source.category} </div>
</div>
<div>
<hr />
</div>
`
router.get('/', async (req, res) => {
var nodes = await new Promise((recordResolve, recordReject) => {
getAllNodes((nodeRows) => {
recordResolve(nodeRows);
});
res.send(htmlOutput);
});
var connections = await getAllConnections();
var presets = await new Promise((recordResolve, recordReject) => {
getAllNodes((nodeRows) => {
recordResolve(filterPresetsAvailable(nodeRows));
});
});
//var sources = libCore.getSources();
return res.render('index', { 'page': 'index', 'nodes': nodes, 'connections': connections, 'presets': presets });
});
/* GET node controller page. */
@@ -38,7 +31,7 @@ router.get('/controller', async (req, res) => {
});
//var sources = libCore.getSources();
return res.render('controller', {'nodes' : nodes});
return res.render('controller', { 'nodes': nodes, 'page': 'controller' });
});
/* GET individual node page. */
@@ -46,7 +39,7 @@ router.get('/node/:id', async (req, res) => {
var node = await getNodeInfoFromId(req.params.id);
//var sources = libCore.getSources();
return res.render('node', {'node' : node});
return res.render('node', { 'node': node, 'page': 'node' });
});
module.exports = router;

View File

@@ -139,3 +139,25 @@ exports.filterAutocompleteValues = async (interaction, options) => {
filtered.map(option => ({ name: option, value: option })),
);
}
/**
* Filter an array of nodeObjects to get all unique presets within
*
* @param {Array} nodeObjects An array of nodeObjects to get the presets from
* @returns {Array} Presets available from given nodeObjects
*/
exports.filterPresetsAvailable = async (nodeObjects) => {
log.DEBUG("Node objects: ", nodeObjects);
var presetsAvailable = [];
for (const nodeObject of nodeObjects) {
log.DEBUG("Node object: ", nodeObject);
presetsAvailable.push.apply(presetsAvailable, nodeObject.presets);
}
log.DEBUG("All Presets available: ", presetsAvailable);
// Remove duplicates
presetsAvailable = [...new Set(presetsAvailable)];
log.DEBUG("DeDuped Presets available: ", presetsAvailable);
return presetsAvailable;
}

View File

@@ -1,133 +1,64 @@
<%- include('partials/htmlHead.ejs') %>
<%- include('partials/htmlHead.ejs', {'page': page}) %>
<div class="container">
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4 mt-2">
<%- include('partials/valueChip.ejs', {
'title': 'Nodes in the Network',
'bgColor': "orange-dark",
'value': nodes.length,
'progressPercent': false,
'icon': 'server'
}) %>
<%- include('partials/valueChip.ejs', {
'title': 'Nodes Online',
'bgColor': "green",
'value': nodes.filter(node => node.online).length,
'progressPercent': false,
'icon': 'cpu-fill'
}) %>
<%- include('partials/valueChip.ejs', {
'title': 'Nodes with Discord Connections',
'bgColor': "blue-dark",
'value': connections.length,
'progressPercent': false,
'icon': 'gear-wide-connected'
}) %>
</div>
<div class="my-4">
<p><h3><b>Current Connections</b></h3></p>
</div>
<hr>
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4">
<div class="col-xl-3 col-lg-6">
<div class="info-card l-bg-cherry">
<div class="card-statistic p-4">
<div class="card-icon card-icon-large"><i class="bi bi-cart"></i></div>
<div class="mb-4">
<h5 class="card-title mb-0">New Orders</h5>
</div>
<div class="row align-items-center mb-2 d-flex">
<div class="col-8">
<h2 class="d-flex align-items-center mb-0">
3,243
</h2>
</div>
<div class="col-4 text-right">
<span>12.5% <i class="fa fa-arrow-up"></i></span>
</div>
</div>
<div class="progress mt-1 " data-height="8" style="height: 8px;">
<div class="progress-bar l-bg-cyan" role="progressbar" data-width="25%" aria-valuenow="25"
aria-valuemin="0" aria-valuemax="100" style="width: 25%;"></div>
</div>
</div>
</div>
<% for(const conn of connections) { %>
<%- include('partials/connectionCard.ejs', {'connection': conn}) %>
<%}%>
</div>
<div class="col-xl-3 col-lg-6">
<div class="info-card l-bg-blue-dark">
<div class="card-statistic p-4">
<div class="card-icon card-icon-large"><i class="bi bi-cart"></i></div>
<div class="mb-4">
<h5 class="card-title mb-0">New Orders</h5>
</div>
<div class="row align-items-center mb-2 d-flex">
<div class="col-8">
<h2 class="d-flex align-items-center mb-0">
3,243
</h2>
</div>
<div class="col-4 text-right">
<span>12.5% <i class="fa fa-arrow-up"></i></span>
</div>
</div>
<div class="progress mt-1 " data-height="8" style="height: 8px;">
<div class="progress-bar l-bg-cyan" role="progressbar" data-width="25%" aria-valuenow="25"
aria-valuemin="0" aria-valuemax="100" style="width: 25%;"></div>
</div>
</div>
</div>
<div class="my-4">
<p><h3><b>Online Nodes</b></h3></p>
</div>
<div class="col-xl-3 col-sm-6">
<div class="card node-card">
<div class="card-body">
<div class="dropdown float-end">
<a class="text-muted dropdown-toggle font-size-16" href="#" role="button"
data-bs-toggle="dropdown" aria-haspopup="true"><i
class="bx bx-dots-horizontal-rounded"></i></a>
<div class="dropdown-menu dropdown-menu-end"><a class="dropdown-item" href="#">Edit</a><a
class="dropdown-item" href="#">Action</a><a class="dropdown-item"
href="#">Remove</a></div>
</div>
<div class="d-flex align-items-center">
<div><img src="https://bootdey.com/img/Content/avatar/avatar1.png" alt=""
class="avatar-md rounded-circle img-thumbnail" /></div>
<div class="flex-1 ms-3">
<h5 class="font-size-16 mb-1"><a href="#" class="text-dark">Phyllis Gatlin</a></h5>
<span class="badge badge-soft-success mb-0">Full Stack Developer</span>
</div>
</div>
<div class="mt-3 pt-1">
<p class="text-muted mb-0"><i
class="mdi mdi-phone font-size-15 align-middle pe-2 text-primary"></i> 070 2860 5375
</p>
<p class="text-muted mb-0 mt-2"><i
class="mdi mdi-email font-size-15 align-middle pe-2 text-primary"></i>
PhyllisGatlin@spy.com</p>
<p class="text-muted mb-0 mt-2"><i
class="mdi mdi-google-maps font-size-15 align-middle pe-2 text-primary"></i> 52
Ilchester MYBSTER 9WX</p>
</div>
</div>
</div>
</div>
<hr>
<div class="col-xl-3 col-sm-6">
<div class="card node-card">
<div class="card-body">
<div class="dropdown float-end">
<a class="text-muted dropdown-toggle font-size-16" href="#" role="button"
data-bs-toggle="dropdown" aria-haspopup="true"><i
class="bx bx-dots-horizontal-rounded"></i></a>
<div class="dropdown-menu dropdown-menu-end"><a class="dropdown-item" href="#">Edit</a><a
class="dropdown-item" href="#">Action</a><a class="dropdown-item"
href="#">Remove</a></div>
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4">
<% for(const node of nodes.filter(node => node.online)) { %>
<%- include('partials/nodeCard.ejs', {'node': node}) %>
<%}%>
</div>
<div class="d-flex align-items-center">
<div><img src="https://bootdey.com/img/Content/avatar/avatar5.png" alt=""
class="avatar-md rounded-circle img-thumbnail" /></div>
<div class="flex-1 ms-3">
<h5 class="font-size-16 mb-1"><a href="#" class="text-dark">Diana Owens</a></h5>
<span class="badge badge-soft-danger mb-0">UI/UX Designer</span>
<div class="my-4">
<p><h3><b>Offline Nodes</b></h3></p>
</div>
</div>
<div class="mt-3 pt-1">
<p class="text-muted mb-0"><i
class="mdi mdi-phone font-size-15 align-middle pe-2 text-primary"></i> 087 6321 3235
</p>
<p class="text-muted mb-0 mt-2"><i
class="mdi mdi-email font-size-15 align-middle pe-2 text-primary"></i>
DianaOwens@spy.com</p>
<p class="text-muted mb-0 mt-2"><i
class="mdi mdi-google-maps font-size-15 align-middle pe-2 text-primary"></i> 52
Ilchester MYBSTER 9WX</p>
</div>
<div class="d-flex gap-2 pt-4">
<button type="button" class="btn btn-soft-primary btn-sm w-50"><i
class="bx bx-user me-1"></i> Profile</button>
<button type="button" class="btn btn-primary btn-sm w-50"><i
class="bx bx-message-square-dots me-1"></i> Contact</button>
</div>
</div>
</div>
</div>
<hr>
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4">
<% for(const node of nodes.filter(node => node.online == false)) { %>
<%- include('partials/nodeCard.ejs', {'node': node}) %>
<%}%>
</div>
</div>
<%- include('partials/bodyEnd.ejs') %>
<%- include('partials/htmlFooter.ejs') %>

View File

@@ -134,5 +134,7 @@
<% // new System Modal %>
<%- include("partials/modifySystemModal.ejs", {'system': "New System", 'frequencies': [], 'mode': ''}) %>
<% // Join Server Modal %>
<%- include("partials/joinModal.ejs", {'node': node}) %>
<%- include('partials/bodyEnd.ejs') %>
<%- include('partials/htmlFooter.ejs') %>

View File

@@ -0,0 +1,29 @@
<div class="col-xl-3 col-sm-6">
<div class="card node-card">
<div class="card-body">
<div class="dropdown float-end">
<a class="text-muted dropdown-toggle font-size-16" href="#" role="button" data-bs-toggle="dropdown"
aria-haspopup="true">
<i class="bx bx-dots-horizontal-rounded"></i>
</a>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item node-action">Edit</a>
<a class="dropdown-item node-action" onclick="">Send Heartbeat</a>
</div>
</div>
<div class="d-flex align-items-center">
<div class="flex-1 ms-3">
<h5 class="font-size-16 mb-1"><a class="text-dark">
<%= connection.clientObject.name%>
</a></h5>
</div>
</div>
<div class="mt-3 pt-1">
<p class="text-muted mb-0">
<i class="bi bi-cpu-fill font-size-15 align-middle pe-2 text-primary"></i>
Node ID: <a href="/node/<%= connection.node.id %>"><%= connection.node.id %></a>
</p>
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,20 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<% switch (page) {
case "index":%>
<title>Node Dashboard</title>
<% break;
case "controller":%>
<title>Node Controller</title>
<% break;
case "node":%>
<title>Node Configuration</title>
<% break;
default:%>
<title>DRB_CnC Server</title>
<%break;
} %>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">

View File

@@ -1,6 +1,6 @@
<!doctype html>
<html lang="en" data-bs-theme="auto">
<%- include('head.ejs') %>
<%- include('head.ejs', {'page': page}) %>
<body>
<%- include('navbar.ejs') %>
<%- include('sidebar.ejs') %>

View File

@@ -24,7 +24,7 @@
<div class="col-md-6">
<label class="small mb-1" for="selectRadioPreset">Selected Preset:</label>
<select class="custom-select" id="selectRadioPreset">
<% for(const system in nearbySystems) { %>
<% for(const system in node.nearbySystems) { %>
<option value="<%=system%>"><%=system%></option>
<% } %>
</select>

View File

@@ -0,0 +1,32 @@
<div class="col-xl-3 col-lg-6">
<div class="info-card l-bg-<%=bgColor%>">
<div class="card-statistic p-4">
<div class="card-icon card-icon-large"><i class="bi bi-<%=icon%>"></i></div>
<div class="mb-4">
<h5 class="card-title mb-0">
<%=title%>
</h5>
</div>
<div class="row align-items-center mb-2 d-flex">
<div class="col-8">
<h2 class="d-flex align-items-center mb-0">
<%=value%>
</h2>
</div>
<% if (progressPercent) {%>
<div class="col-4 text-right">
<span>
<%=progressPercent%>%<i class="fa fa-arrow-up"></i>
</span>
</div>
<%}%>
</div>
<% if (progressPercent) {%>
<div class="progress mt-1 " data-height="8" style="height: 8px;">
<div class="progress-bar l-bg-cyan" role="progressbar" data-width="<%=progressPercent%>%" aria-valuenow="<%=progressPercent%>"
aria-valuemin="0" aria-valuemax="100" style="width: <%=progressPercent%>%;"></div>
</div>
<%}%>
</div>
</div>
</div>

View File

@@ -1,32 +1,27 @@
# Discord Radio Bot: Command & Control
# Project Overview
This project is a multi-layered application consisting of client and server applications. Its main purpose is to enable the use of Software-Defined Radios (SDRs) and Raspberry Pi (or similar Single Board Computers) to listen to radio frequencies in Discord voice channels. The project is designed to provide a seamless integration between the SDR hardware and the server with Discord commands.
## Server Application
The server application acts as the central hub within Discord, providing various functionalities and serving as the main point of communication for the clients. Some of the key features and responsibilities of the server include:
- **RSS Feed Updates**: The server periodically updates text channels with RSS feed updates, keeping users informed about the latest news or information.
- **Server Management Functions / User Requests**: The server includes management functions that allow administrators to control and configure various aspects of the server environment. Users can interact with the server through Discord commands, which range from requesting specific radio presets to updating RSS feeds.
- **API and Web Front End**: The server exposes an API and web front end, providing an interface to view and control all the online clients. This allows users to monitor and manage the available radio presets, as well as perform various administrative tasks.
#### [Read more about the Server](https://git.vpn.cusano.net/logan/DRB-CnC/src/branch/master/Server)
---
## Client Application
The client application communicates with the server through the provided API. Each client instance waits for join requests sent by users through Discord. Once a join request is received, the client uses the SDR application to tune into the specified radio preset. It then establishes a connection to Discord, allowing users to listen to the selected radio preset in real-time.
In addition to its interaction with the server, the client also has its own API and web application. This enables users to directly interface with the client, perform actions specific to the client application, and access relevant information about the connected SDR and radio presets.
#### [Read more about the Client](https://git.vpn.cusano.net/logan/DRB-CnC/src/branch/master/Client)
---
Project overview here
## Requirements Overview
---
### Server Requirements
#### Server: Discord Bot Requirements
### Client Requirements
#### Client: Discord Bot Requirements
## Server
---
Explanation and overview here
## Client
---
Explanation and overview here
## Discord Bot
---
Explanation and overview here
## Troubleshooting
Check the [wiki](https://git.vpn.cusano.net/logan/DRB-CnC/wiki)