rdio-to-discord/index.js

222 lines
7 KiB
JavaScript

require("dotenv").config({quiet:true})
const ws = require("ws");
// const conn = new ws("wss://saubeo.solutions/demo/rdio-scanner/");
const conn = new ws("wss://rosebud-mt.ko4wal.radio")
const fs = require("fs");
const Discord = require("discord.js");
const hook = new Discord.WebhookClient({ url: process.env.DISCORD_WEBHOOK_URL });
const knownTones = {
"634.5:706.8": "<@&1531872533119504464>",
"601.1:706.8": "<@&1531872550852755596>",
}
// create or empty the temp folder
if (!fs.existsSync("./temp")) {
fs.mkdirSync("./temp");
} else {
fs.readdirSync("./temp").forEach((file) => {
fs.unlinkSync(`./temp/${file}`);
});
}
// Build an LFM filter map that enables every talkgroup, in every group,
// on every system found in the CFG payload.
var globalSystems = {};
function buildFullLfmFilter(config) {
const filter = {};
const systems = (config && config.systems) || {};
for (const sysId of Object.keys(systems)) {
const system = systems[sysId];
const talkgroups = (system && system.talkgroups) || [];
filter[system.id] = filter[system.id] || {};
globalSystems[system.id] = {};
globalSystems[system.id].name = system.label;
for (const tg of talkgroups) {
const groupId = tg.groupId ?? tg.group ?? tg.tagId ?? 0;
filter[system.id][tg.id] = true;
globalSystems[system.id][tg.id] = tg;
// console.log(tg)
}
// Print out number of talkgroups for system, and sys name
console.log(`System ${system.label} (${system.id}) has ${talkgroups.length} talkgroups.`);
}
// console.log(JSON.stringify(filter, null, 2));
// console.log("Global systems:", JSON.stringify(globalSystems, null, 2));
return filter;
}
// Wrapper function around twotonedec.py to call it from Node.js and return the result.
const { spawn } = require("child_process");
const decodeTwoTone = (filePath) => {
return new Promise((resolve, reject) => {
// FFMPEG Convert file to wav
const wavFilePath = filePath.replace(/\.\w+$/, ".wav");
console.log(`Converting ${filePath} to ${wavFilePath} using FFMPEG...`);
// unlink the wav file if it already exists
const path = require("path");
console.log("Original:", filePath);
console.log("WAV:", wavFilePath);
console.log("Absolute:", path.resolve(wavFilePath));
console.log("Exists:", fs.existsSync(wavFilePath));
if (fs.existsSync(wavFilePath)) {
fs.unlinkSync(wavFilePath);
}
const ffmpegProcess = spawn("ffmpeg", [
"-y",
"-i", filePath,
"-acodec", "pcm_s16le",
"-ar", "8000", // or whatever your decoder expects
"-ac", "1", // mono if required
wavFilePath
]);
ffmpegProcess.on("close", (code) => {
if (code !== 0) {
reject(new Error(`FFMPEG process exited with code ${code}`));
return;
}
// Proceed with Python process only after FFMPEG conversion is successful
const pythonProcess = spawn("python", ["twotonedec.py", wavFilePath]);
let result = "";
pythonProcess.stdout.on("data", (data) => {
result += data.toString();
});
pythonProcess.stderr.on("data", (data) => {
console.error(`stderr: ${data}`);
});
pythonProcess.on("close", (code) => {
if (code === 0) {
try {
result = JSON.parse(result.replace(/'/g, '"'));
// Delete the wav file after processing
fs.unlink(wavFilePath, (err) => {
if (err) {
console.error(`Error deleting wav file: ${err}`);
}
});
resolve(result);
} catch (e) {
reject(e);
}
} else {
reject(new Error(`Python process exited with code ${code}`));
}
});
});
});
};
// decodeTwoTone("test2.m4a").then((result) => {
// console.log("Two-tone detection result:", result);
// }) // Test code
conn.on("message", (data) => {
let msg = data.toString()
let parsed = JSON.parse(msg)
// console.log("Received message:", parsed);
switch(parsed[0]) {
case "CFG": // Config RX
const config = parsed[1];
const lfmFilter = buildFullLfmFilter(config);
console.log("Got config from server, sending LFM filter");
conn.send(JSON.stringify(["LFM", lfmFilter]));
break;
case "LFM": // LFM Response
if (parsed[1] == true) {
console.log("LFM filter accepted by server.");
// TEST
// conn.send(JSON.stringify(["CAL","616","p"]))
} else {
console.error("LFM filter rejected by server:", parsed[1]);
process.exit(1);
}
break;
case "LSC": // Listener Count
console.log("Listener count:", parsed[1]);
break;
case "VER": // Ver info
console.log(`${parsed[1].branding} RDIO v${parsed[1].version}`)
conn.send(JSON.stringify(["CFG"]));
break;
case "CAL": // New call
console.log(`New Call ${globalSystems[parsed[1].system][parsed[1].talkgroup].name} call #${parsed[1].id}`);
const tgName = globalSystems[parsed[1].system][parsed[1].talkgroup].name;
fs.writeFileSync(`./temp/${parsed[1].audioName}`, Buffer.from(parsed[1].audio.data));
decodeTwoTone(`./temp/${parsed[1].audioName}`).then((result) => {
console.log(`Two-tone detection result for ${tgName}:`, result);
// Two tone result will be array of objects for each detected twotone activation. .detected is array of two frequencies, one for each tone. Create a counter for each pair of frequencies detected. Make a message to send to discord with the tone names, so if 2 copies of RC EMS are sent, send "RC EMS (2x)" instead. For multiple, send "RC EMS (2x), RC Fire (1x)" etc.
const toneCounts = {};
for (const activation of result) {
const detected = activation.detected;
const key = `${detected[0]}:${detected[1]}`;
if (toneCounts[key]) {
toneCounts[key]++;
} else {
toneCounts[key] = 1;
}
}
const toneMessages = [];
for (const key in toneCounts) {
const name = knownTones[key] || key;
const count = toneCounts[key];
toneMessages.push(count > 1 ? `${name} (${count}x)` : name);
}
const message = toneMessages.join(", ");
console.log(`Two-tone summary for ${tgName}:`, message);
hook.send({
// Username is tg name
username: globalSystems[parsed[1].system][parsed[1].talkgroup].name,
content: `${message}`,
// Upload audio file
files: [{
attachment: `./temp/${parsed[1].audioName}`,
name: parsed[1].audioName
}]
}).then(() => {
// Delete the audio file after sending to Discord
fs.unlink(`./temp/${parsed[1].audioName}`, (err) => {
if (err) {
console.error(`Error deleting audio file: ${err}`);
}
});
})
});
break;
default:
console.log("Unknown message:", JSON.stringify(parsed));
break;
}
});
conn.on("open", () => {
console.log("Connected to the server.");
conn.send(JSON.stringify(["VER"]))
});
conn.on("close", () => {
// Set 3 second timeout to reconnect.
setTimeout(() => {
console.log("Connection closed. Reconnecting...");
conn = new ws("wss://rosebud-mt.ko4wal.radio");
}, 3000);
});
conn.on("error", (err) => {
console.error("Connection error:", err);
// Close conn, and reconnect after 3 seconds
conn.close();
setTimeout(() => {
console.log("Reconnecting...");
conn = new ws("wss://rosebud-mt.ko4wal.radio");
}, 3000);
});