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("python3.12", ["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","653"])) // conn.send(JSON.stringify(["CAL","655"])) // conn.send(JSON.stringify(["CAL","634"])) } 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}`); } }); }) }); // TwoTone breaks on my server. Will fix later (I think i need py 3.12 not 3.13) // 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); });