From f3a99e6adc27c63385255509adc7caafeeba79f9 Mon Sep 17 00:00:00 2001 From: NoobishSVK Date: Mon, 5 Feb 2024 21:16:32 +0100 Subject: [PATCH] TX ID, UI fixes --- datahandler.js | 25 +++++++ index.js | 145 ++++++++++++++++++++-------------------- stream/index.js | 6 +- tx_search.js | 127 +++++++++++++++++++++++++++++++++++ web/css/breadcrumbs.css | 10 +++ web/css/helpers.css | 8 +++ web/index.ejs | 58 +++++++--------- web/js/main.js | 45 +++++++++++-- web/setup.ejs | 6 +- 9 files changed, 312 insertions(+), 118 deletions(-) create mode 100644 tx_search.js diff --git a/datahandler.js b/datahandler.js index 4095237..5d9880a 100644 --- a/datahandler.js +++ b/datahandler.js @@ -7,6 +7,7 @@ const os = require('os'); const win32 = (os.platform() == "win32"); const unicode_type = (win32 ? 'int16_t' : 'int32_t'); const lib = koffi.load(path.join(__dirname, "librdsparser." + (win32 ? "dll" : "so"))); +const { fetchTx } = require('./tx_search.js'); koffi.proto('void callback_pi(void *rds, void *user_data)'); koffi.proto('void callback_pty(void *rds, void *user_data)'); @@ -206,6 +207,15 @@ var dataToSend = { ims: 0, eq: 0, ant: 0, + txInfo: { + station: '', + pol: '', + erp: '', + city: '', + itu: '', + distance: '', + azimuth: '' + }, country_name: '', country_iso: 'UN', users: '', @@ -215,6 +225,7 @@ var legacyRdsPiBuffer = null; const initialData = { ...dataToSend }; const resetToDefault = dataToSend => Object.assign(dataToSend, initialData); + function handleData(ws, receivedData) { // Retrieve the last update time for this client let lastUpdateTime = clientUpdateIntervals.get(ws) || 0; @@ -329,6 +340,20 @@ function handleData(ws, receivedData) { } } + // Get the received TX info + const currentTx = fetchTx(dataToSend.freq, dataToSend.pi, dataToSend.ps); + if(currentTx.station !== undefined) { + dataToSend.txInfo = { + station: currentTx.station, + pol: currentTx.pol, + erp: currentTx.erp, + city: currentTx.city, + itu: currentTx.itu, + distance: currentTx.distance, + azimuth: currentTx.azimuth + } + } + // Send the updated data to the client const dataToSendJSON = JSON.stringify(dataToSend); if (currentTime - lastUpdateTime >= updateInterval) { diff --git a/index.js b/index.js index df8e2d5..5707fe2 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,6 @@ const http = require('http'); const https = require('https'); const app = express(); const httpServer = http.createServer(app); -const ejs = require('ejs'); // Websocket handling const WebSocket = require('ws'); @@ -43,13 +42,13 @@ let serverConfig = { xdrd: { xdrdIp: "127.0.0.1", xdrdPort: "7373", - xdrdPassword: "password" + xdrdPassword: "" }, identification: { tunerName: "", tunerDesc: "", - lat: "", - lon: "" + lat: "0", + lon: "0" }, password: { tunePass: "", @@ -102,78 +101,80 @@ function authenticateWithXdrd(client, salt, password) { } // xdrd connection -client.connect(serverConfig.xdrd.xdrdPort, serverConfig.xdrd.xdrdIp, () => { - logInfo('Connection to xdrd established successfully.'); - - const authFlags = { - authMsg: false, - firstClient: false, - receivedPassword: false - }; - - const authDataHandler = (data) => { - const receivedData = data.toString(); - const lines = receivedData.split('\n'); - - for (const line of lines) { - - if (!authFlags.receivedPassword) { - authFlags.receivedSalt = line.trim(); - authenticateWithXdrd(client, authFlags.receivedSalt, serverConfig.xdrd.xdrdPassword); - authFlags.receivedPassword = true; - } else { - if (line.startsWith('a')) { - authFlags.authMsg = true; - logWarn('Authentication with xdrd failed. Is your password set correctly?'); - } else if (line.startsWith('o1,')) { - authFlags.firstClient = true; - } else if (line.startsWith('T') && line.length <= 7) { - const freq = line.slice(1) / 1000; - dataHandler.dataToSend.freq = freq.toFixed(3); - } else if (line.startsWith('OK')) { - authFlags.authMsg = true; - logInfo('Authentication with xdrd successful.'); - } - - if (authFlags.authMsg && authFlags.firstClient) { - client.write('T87500\n'); - client.write('A0\n'); - client.write('G11\n'); - client.off('data', authDataHandler); - return; +if (serverConfig.xdrd.xdrdPassword.length > 1) { + client.connect(serverConfig.xdrd.xdrdPort, serverConfig.xdrd.xdrdIp, () => { + logInfo('Connection to xdrd established successfully.'); + + const authFlags = { + authMsg: false, + firstClient: false, + receivedPassword: false + }; + + const authDataHandler = (data) => { + const receivedData = data.toString(); + const lines = receivedData.split('\n'); + + for (const line of lines) { + + if (!authFlags.receivedPassword) { + authFlags.receivedSalt = line.trim(); + authenticateWithXdrd(client, authFlags.receivedSalt, serverConfig.xdrd.xdrdPassword); + authFlags.receivedPassword = true; + } else { + if (line.startsWith('a')) { + authFlags.authMsg = true; + logWarn('Authentication with xdrd failed. Is your password set correctly?'); + } else if (line.startsWith('o1,')) { + authFlags.firstClient = true; + } else if (line.startsWith('T') && line.length <= 7) { + const freq = line.slice(1) / 1000; + dataHandler.dataToSend.freq = freq.toFixed(3); + } else if (line.startsWith('OK')) { + authFlags.authMsg = true; + logInfo('Authentication with xdrd successful.'); + } + + if (authFlags.authMsg && authFlags.firstClient) { + client.write('T87500\n'); + client.write('A0\n'); + client.write('G11\n'); + client.off('data', authDataHandler); + return; + } } } - } - }; - - client.on('data', (data) => { - var receivedData = incompleteDataBuffer + data.toString(); - const isIncomplete = (receivedData.slice(-1) != '\n'); - - if (isIncomplete) { - const position = receivedData.lastIndexOf('\n'); - if (position < 0) { - incompleteDataBuffer = receivedData; - receivedData = ''; - } else { - incompleteDataBuffer = receivedData.slice(position + 1); - receivedData = receivedData.slice(0, position + 1); - } - } else { - incompleteDataBuffer = ''; - } - - if (receivedData.length) { - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - dataHandler.handleData(client, receivedData); + }; + + client.on('data', (data) => { + var receivedData = incompleteDataBuffer + data.toString(); + const isIncomplete = (receivedData.slice(-1) != '\n'); + + if (isIncomplete) { + const position = receivedData.lastIndexOf('\n'); + if (position < 0) { + incompleteDataBuffer = receivedData; + receivedData = ''; + } else { + incompleteDataBuffer = receivedData.slice(position + 1); + receivedData = receivedData.slice(0, position + 1); } - }); - } + } else { + incompleteDataBuffer = ''; + } + + if (receivedData.length) { + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + dataHandler.handleData(client, receivedData); + } + }); + } + }); + + client.on('data', authDataHandler); }); - - client.on('data', authDataHandler); -}); +} client.on('close', () => { logWarn('Disconnected from xdrd.'); diff --git a/stream/index.js b/stream/index.js index 6032a4c..bd796e2 100644 --- a/stream/index.js +++ b/stream/index.js @@ -23,15 +23,15 @@ function enableAudioStream() { // Specify the command and its arguments const command = 'ffmpeg'; const flags = `-fflags +nobuffer+flush_packets -flags low_delay -rtbufsize 6192 -probesize 32`; - const codec = `-acodec pcm_s16le -ar 32000 -ac ${serverConfig.audio.audioChannels}`; + const codec = `-acodec pcm_s16le -ar 48000 -ac ${serverConfig.audio.audioChannels}`; const output = `-f s16le -fflags +nobuffer+flush_packets -packetsize 384 -flush_packets 1 -bufsize 960`; // Combine all the settings for the ffmpeg command if (process.platform === 'win32') { // Windows - ffmpegCommand = `${flags} -f dshow -i audio="${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node stream/3las.server.js -port ${serverConfig.webserver.audioPort} -samplerate 32000 -channels ${serverConfig.audio.audioChannels}`; + ffmpegCommand = `${flags} -f dshow -i audio="${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node stream/3las.server.js -port ${serverConfig.webserver.audioPort} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; } else { // Linux - ffmpegCommand = `${flags} -f alsa -i "${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node stream/3las.server.js -port ${serverConfig.webserver.audioPort} -samplerate 32000 -channels ${serverConfig.audio.audioChannels}`; + ffmpegCommand = `${flags} -f alsa -i "${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node stream/3las.server.js -port ${serverConfig.webserver.audioPort} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; } consoleCmd.logInfo("Using audio device: " + serverConfig.audio.audioDevice); diff --git a/tx_search.js b/tx_search.js new file mode 100644 index 0000000..42143cf --- /dev/null +++ b/tx_search.js @@ -0,0 +1,127 @@ +const fetch = require('node-fetch'); +var fs = require('fs'); + +let cachedData = {}; + +let serverConfig = { + identification: { + lat: 0, + lon: 0 + }, + }; + + +if(fs.existsSync('config.json')) { + const configFileContents = fs.readFileSync('config.json', 'utf8'); + serverConfig = JSON.parse(configFileContents); + } + +let lastFetchTime = 0; +const fetchInterval = 3000; + +// Fetch data from maps +function fetchTx(freq, piCode, rdsPs) { + const now = Date.now(); + + // Check if it's been at least 3 seconds since the last fetch and if the QTH is correct + if (now - lastFetchTime < fetchInterval || serverConfig.identification.lat.length < 2) { + return Promise.resolve(); + } + + lastFetchTime = now; + + // Check if data for the given frequency is already cached + if (cachedData[freq]) { + return processData(cachedData[freq], piCode, rdsPs); + } + + const url = "https://maps.fmdx.pl/controller.php?freq=" + freq; + + return fetch(url) + .then(response => response.json()) + .then(data => { + // Cache the fetched data for the specific frequency + cachedData[freq] = data; + return processData(data, piCode, rdsPs); + }) + .catch(error => { + console.error("Error fetching data:", error); + }); +} + +function processData(data, piCode, rdsPs) { + let matchingStation = null; + let matchingCity = null; + let minDistance = Infinity; + let txAzimuth; + + for (const cityId in data.locations) { + const city = data.locations[cityId]; + if (city.stations) { + for (const station of city.stations) { + if (station.pi === piCode && station.ps.includes(rdsPs.replace(/ /g, '_'))) { + const distance = haversine(serverConfig.identification.lat, serverConfig.identification.lon, city.lat, city.lon); + if (distance.distanceKm < minDistance) { + minDistance = distance.distanceKm; + txAzimuth = distance.azimuth; + matchingStation = station; + matchingCity = city; + } + } + } + } + } + + if (matchingStation) { + return { + station: matchingStation.station.replace("R.", "Radio "), + pol: matchingStation.pol.toUpperCase(), + erp: matchingStation.erp, + city: matchingCity.name, + itu: matchingCity.itu, + distance: minDistance.toFixed(0), + azimuth: txAzimuth.toFixed(0), + foundStation: true + }; + } else { + return; + } +} + +function haversine(lat1, lon1, lat2, lon2) { + const R = 6371; // Earth radius in kilometers + const dLat = deg2rad(lat2 - lat1); + const dLon = deg2rad(lon2 - lon1); + + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + // Distance in kilometers + const distance = R * c; + + // Azimuth calculation + const y = Math.sin(dLon) * Math.cos(deg2rad(lat2)); + const x = Math.cos(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) - + Math.sin(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(dLon); + const azimuth = Math.atan2(y, x); + + // Convert azimuth from radians to degrees + const azimuthDegrees = (azimuth * 180 / Math.PI + 360) % 360; + + return { + distanceKm: distance, + azimuth: azimuthDegrees + }; +} + + +function deg2rad(deg) { + return deg * (Math.PI / 180); +} + +module.exports = { + fetchTx +}; diff --git a/web/css/breadcrumbs.css b/web/css/breadcrumbs.css index af2405e..396ede7 100644 --- a/web/css/breadcrumbs.css +++ b/web/css/breadcrumbs.css @@ -20,6 +20,12 @@ h3 { font-size: 22px; } +h4 { + margin: 0; + font-weight: 400; + font-size: 20px; +} + p#tuner-desc { margin: 0; } @@ -54,6 +60,10 @@ label { font-weight: 500; } +#data-station-container { + display: none; +} + .form-group { float: left; margin-bottom: 10px; diff --git a/web/css/helpers.css b/web/css/helpers.css index 2afe077..19b82ae 100644 --- a/web/css/helpers.css +++ b/web/css/helpers.css @@ -58,6 +58,14 @@ background-color: transparent; } +.opacity-full { + opacity: 1; +} + +.opacity-half { + opacity: 0.5; +} + .flex-container { display: flex; } diff --git a/web/index.ejs b/web/index.ejs index 49dcbed..0f61624 100644 --- a/web/index.ejs +++ b/web/index.ejs @@ -9,10 +9,10 @@ - + - + @@ -27,18 +27,6 @@