From 97822a08a5f0da6b84dc263844b3c0b2b4f69377 Mon Sep 17 00:00:00 2001 From: Konrad Kosmatka Date: Sat, 17 May 2025 01:36:09 +0200 Subject: [PATCH] RDS bugfixes & improvements --- server/datahandler.js | 62 +++++++++++---------- server/endpoints.js | 3 +- server/index.js | 119 ++++++++++++++++++++-------------------- server/server_config.js | 3 +- web/js/init.js | 2 +- web/setup.ejs | 7 ++- 6 files changed, 103 insertions(+), 93 deletions(-) diff --git a/server/datahandler.js b/server/datahandler.js index 7d54f1a..864833f 100644 --- a/server/datahandler.js +++ b/server/datahandler.js @@ -264,7 +264,27 @@ const resetToDefault = dataToSend => Object.assign(dataToSend, initialData); const ServerStartTime = process.hrtime(); var serialportUpdateTime = process.hrtime(); let checkSerialport = false; +let rdsTimeoutTimer = null; +function rdsReceived() { + if (rdsTimeoutTimer) { + clearTimeout(rdsTimeoutTimer); + rdsTimeoutTimer = null; + } + if (serverConfig.webserver.rdsTimeout && serverConfig.webserver.rdsTimeout != 0) { + rdsTimeoutTimer = setInterval(rdsReset, serverConfig.webserver.rdsTimeout * 1000); + } +} + +function rdsReset() { + resetToDefault(dataToSend); + dataToSend.af.length = 0; + rdsparser.clear(rds); + if (rdsTimeoutTimer) { + clearTimeout(rdsTimeoutTimer); + rdsTimeoutTimer = null; + } +} function handleData(wss, receivedData, rdsWss) { // Retrieve the last update time for this client @@ -280,6 +300,7 @@ function handleData(wss, receivedData, rdsWss) { dataToSend.bw = receivedLine.substring(1); break; case receivedLine.startsWith('P'): // PI Code + rdsReceived(); modifiedData = receivedLine.slice(1); legacyRdsPiBuffer = modifiedData; if (dataToSend.pi.length >= modifiedData.length || dataToSend.pi == '?') { @@ -289,16 +310,11 @@ function handleData(wss, receivedData, rdsWss) { case receivedLine.startsWith('T'): // Frequency modifiedData = receivedLine.substring(1).split(",")[0]; - if((modifiedData / 1000).toFixed(3) == dataToSend.freq) { - resetToDefault(dataToSend); - rdsparser.clear(rds); - dataToSend.af = []; + rdsReset(); + if((modifiedData / 1000).toFixed(3) == dataToSend.freq) { return; // Prevent tune spamming using scrollwheel } - resetToDefault(dataToSend); - dataToSend.af.length = 0; - rdsparser.clear(rds); parsedValue = parseFloat(modifiedData); if (!isNaN(parsedValue)) { @@ -315,6 +331,7 @@ function handleData(wss, receivedData, rdsWss) { case receivedLine.startsWith('Z'): // Antenna dataToSend.ant = receivedLine.substring(1); initialData.ant = receivedLine.substring(1); + rdsReset(); break; case receivedLine.startsWith('G'): // EQ / iMS (RF+/IF+) const mapping = filterMappings[receivedLine]; @@ -342,6 +359,7 @@ function handleData(wss, receivedData, rdsWss) { processSignal(receivedLine, false, true); break; case receivedLine.startsWith('R'): // RDS HEX + rdsReceived(); modifiedData = receivedLine.slice(1); dataToSend.rds = true; @@ -357,7 +375,7 @@ function handleData(wss, receivedData, rdsWss) { // error correction, but this is a good substitute. errorsNew = (legacyRdsPiBuffer.length - 4) << 6; } else { - pi = '----'; + pi = '0000'; errorsNew = (0x03 << 6); } @@ -371,28 +389,14 @@ function handleData(wss, receivedData, rdsWss) { } rdsWss.clients.forEach((client) => { - let dataString = modifiedData.toString(); - let lastTwoChars = dataString.slice(-2); - let lastByteValue = parseInt(lastTwoChars, 16); - - let truncatedString = dataString.slice(0, -2); - - if ((lastByteValue & 0x03) !== 0) { - truncatedString = truncatedString.slice(0, 4) + '----' + truncatedString.slice(8); - } - - if ((lastByteValue & 0x30) !== 0) { - truncatedString = truncatedString.slice(0, 8) + '----' + truncatedString.slice(12); - } - - if ((lastByteValue & 0x0C) !== 0) { - truncatedString = truncatedString.slice(0, 12) + '----'; - } - - let newDataString = "G:\r\n" + truncatedString + "\r\n\r\n"; - - let finalBuffer = Buffer.from(newDataString, 'utf-8'); + const errors = parseInt(modifiedData.slice(-2), 16); + let data = (((errors & 0xC0) == 0) ? modifiedData.slice(0, 4) : '----'); + data += (((errors & 0x30) == 0) ? modifiedData.slice(4, 8) : '----'); + data += (((errors & 0x0C) == 0) ? modifiedData.slice(8, 12) : '----'); + data += (((errors & 0x03) == 0) ? modifiedData.slice(12, 16) : '----'); + const newDataString = "G:\r\n" + data + "\r\n\r\n"; + const finalBuffer = Buffer.from(newDataString, 'utf-8'); client.send(finalBuffer); }); diff --git a/server/endpoints.js b/server/endpoints.js index af3f429..cddcd50 100644 --- a/server/endpoints.js +++ b/server/endpoints.js @@ -162,7 +162,7 @@ router.get('/rdsspy', (req, res) => { }); router.get('/rds', (req, res) => { - res.send('Please c onnect using a WebSocket compatible app to obtain RDS stream.'); + res.send('Please connect using a WebSocket compatible app to obtain RDS stream.'); }); router.get('/rdsspy', (req, res) => { @@ -344,6 +344,7 @@ router.get('/static_data', (req, res) => { defaultTheme: serverConfig.webserver.defaultTheme || 'theme1', bgImage: serverConfig.webserver.bgImage || '', rdsMode: serverConfig.webserver.rdsMode || false, + rdsTimeout: serverConfig.webserver.rdsTimeout || 0, tunerName: serverConfig.identification.tunerName || '', tunerDesc: serverConfig.identification.tunerDesc || '', ant: serverConfig.antennas || {} diff --git a/server/index.js b/server/index.js index a1db2c0..d9ea23e 100644 --- a/server/index.js +++ b/server/index.js @@ -214,7 +214,10 @@ if (serverConfig.xdrd.wirelessConnection === false) { return serialport; } } + // xdrd connection +let authFlags = {}; + function connectToXdrd() { const { xdrd } = serverConfig; @@ -222,76 +225,72 @@ function connectToXdrd() { client.connect(xdrd.xdrdPort, xdrd.xdrdIp, () => { logInfo('Connection to xdrd established successfully.'); - let authFlags = { + authFlags = { authMsg: false, firstClient: false, receivedSalt: '', receivedPassword: false, messageCount: 0, }; - - const authDataHandler = (data) => { - authFlags.messageCount++ - const receivedData = data.toString(); - const lines = receivedData.split('\n'); - - for (const line of lines) { - if (authFlags.receivedPassword === false) { - authFlags.receivedSalt = line.trim(); - authFlags.receivedPassword = true; - helpers.authenticateWithXdrd(client, authFlags.receivedSalt, xdrd.xdrdPassword); - } 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.'); - } else if (line.startsWith('G')) { - const value = line.substring(1); - dataHandler.initialData.eq = value.charAt(0); - dataHandler.dataToSend.eq = value.charAt(0); - dataHandler.initialData.ims = value.charAt(1); - dataHandler.dataToSend.ims = value.charAt(1); - } else if (line.startsWith('Z')) { - let modifiedLine = line.slice(1); - dataHandler.initialData.ant = modifiedLine; - dataHandler.dataToSend.ant = modifiedLine; - } - - if (authFlags.authMsg === true && authFlags.firstClient === true) { - client.write('x\n'); - client.write(serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true ? 'T' + Math.round(serverConfig.defaultFreq * 1000) + '\n' : 'T87500\n'); - dataHandler.initialData.freq = serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true ? Number(serverConfig.defaultFreq).toFixed(3) : (87.5).toFixed(3); - dataHandler.dataToSend.freq = serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true ? Number(serverConfig.defaultFreq).toFixed(3) : (87.5).toFixed(3); - client.write('A0\n'); - client.write(serverConfig.audio.startupVolume ? 'Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n' : 'Y100\n'); - serverConfig.webserver.rdsMode ? client.write('D1\n') : client.write('D0\n'); - client.off('data', authDataHandler); - return; - } - } - } - }; - - client.on('data', (data) => { - helpers.resolveDataBuffer(data, wss, rdsWss); - if (authFlags.authMsg == true && authFlags.messageCount > 1) { - // If the limit is reached, remove the 'data' event listener - client.off('data', authDataHandler); - return; - } - authDataHandler(data); - }); }); } } +client.on('data', (data) => { + const { xdrd } = serverConfig; + + helpers.resolveDataBuffer(data, wss, rdsWss); + if (authFlags.authMsg == true && authFlags.messageCount > 1) { + return; + } + + authFlags.messageCount++; + const receivedData = data.toString(); + const lines = receivedData.split('\n'); + + for (const line of lines) { + if (authFlags.receivedPassword === false) { + authFlags.receivedSalt = line.trim(); + authFlags.receivedPassword = true; + helpers.authenticateWithXdrd(client, authFlags.receivedSalt, xdrd.xdrdPassword); + } 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.'); + } else if (line.startsWith('G')) { + const value = line.substring(1); + dataHandler.initialData.eq = value.charAt(0); + dataHandler.dataToSend.eq = value.charAt(0); + dataHandler.initialData.ims = value.charAt(1); + dataHandler.dataToSend.ims = value.charAt(1); + } else if (line.startsWith('Z')) { + let modifiedLine = line.slice(1); + dataHandler.initialData.ant = modifiedLine; + dataHandler.dataToSend.ant = modifiedLine; + } + + if (authFlags.authMsg === true && authFlags.firstClient === true) { + client.write('x\n'); + client.write(serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true ? 'T' + Math.round(serverConfig.defaultFreq * 1000) + '\n' : 'T87500\n'); + dataHandler.initialData.freq = serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true ? Number(serverConfig.defaultFreq).toFixed(3) : (87.5).toFixed(3); + dataHandler.dataToSend.freq = serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true ? Number(serverConfig.defaultFreq).toFixed(3) : (87.5).toFixed(3); + client.write('A0\n'); + client.write(serverConfig.audio.startupVolume ? 'Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n' : 'Y100\n'); + serverConfig.webserver.rdsMode ? client.write('D1\n') : client.write('D0\n'); + return; + } + } + } +}); + client.on('close', () => { if(serverConfig.autoShutdown === false) { logWarn('Disconnected from xdrd. Attempting to reconnect.'); diff --git a/server/server_config.js b/server/server_config.js index ff5ad2e..fcf4290 100644 --- a/server/server_config.js +++ b/server/server_config.js @@ -31,6 +31,7 @@ let serverConfig = { defaultTheme: "theme1", bgImage: "", rdsMode: false, + rdsTimeout: 0, txIdAlgorithm: 0 }, xdrd: { @@ -181,4 +182,4 @@ if (configExists()) { module.exports = { configName, serverConfig, configUpdate, configSave, configExists, configPath -}; \ No newline at end of file +}; diff --git a/web/js/init.js b/web/js/init.js index 6b970da..720cf5d 100644 --- a/web/js/init.js +++ b/web/js/init.js @@ -14,7 +14,7 @@ function getInitialSettings() { dataType: 'json', success: function (data) { - ['qthLatitude', 'qthLongitude', 'defaultTheme', 'bgImage', 'rdsMode'].forEach(key => { + ['qthLatitude', 'qthLongitude', 'defaultTheme', 'bgImage', 'rdsMode', 'rdsTimeout'].forEach(key => { if (data[key] !== undefined) { localStorage.setItem(key, data[key]); } diff --git a/web/setup.ejs b/web/setup.ejs index 59ae2f0..96242cd 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -323,7 +323,12 @@

RDS Mode

You can switch between American (RBDS) / Global (RDS) mode here.

- <%- include('_components', {component: 'checkbox', cssClass: 'bottom-20', iconClass: '', label: 'American RDS mode (RBDS)', id: 'webserver-rdsMode'}) %>
+ <%- include('_components', {component: 'checkbox', cssClass: 'bottom-20', iconClass: '', label: 'American RDS mode (RBDS)', id: 'webserver-rdsMode'}) %> +

RDS Timeout

+

If no data is received, RDS will be automatically cleared after a timeout.
+ Enter timeout in seconds or 0 to disable.

+ + <%- include('_components', {component: 'text', cssClass: 'w-100', placeholder: '0', label: 'RDS Timeout', id: 'webserver-rdsTimeout'}) %>

Transmitter Search Algorithm