diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index c9c43933..ce2aa67b 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -4,7 +4,7 @@ an open-source SDR receiver software with a web UI. Copyright (c) 2013-2015 by Andras Retzler Copyright (c) 2019-2021 by Jakob Ketterl - Copyright (c) 2022-2023 by Marat Fayzullin + Copyright (c) 2022-2024 by Marat Fayzullin This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -1648,43 +1648,6 @@ img.openwebrx-mirror-img height: 27px; } -@keyframes openwebrx-record-animation { - 0% { background: #ff0000; color: white; } - 50% { background: #800000; color: white; } - 100% { background: #ff0000; color: white; } -} - -.openwebrx-record-button { - color: #ff8080; - float: right; - margin-top: 13px; - animation-duration: 1s; - animation-iteration-count: infinite; -} - -@keyframes openwebrx-scan-animation { - 0% { background: #00ff00; color: white; } - 50% { background: #008000; color: white; } - 100% { background: #00ff00; color: white; } -} - -.openwebrx-squelch-auto { - animation-duration: 1s; - animation-iteration-count: infinite; -} - -.openwebrx-squelch-auto .scanner { - display: none; -} - -.openwebrx-squelch-auto.highlighted .scanner { - display: initial; -} - -.openwebrx-squelch-auto.highlighted .squelch { - display: none; -} - .openwebrx-slider-button svg { position:relative; top: 1px; @@ -1730,6 +1693,123 @@ img.openwebrx-mirror-img flex: 0 0 auto; } +#openwebrx-panel-metadata-wfm { + width: 300px; + max-height: 300px; +} + +.rds-container { + width: 100%; + text-align: center; + overflow: hidden auto; +} + +.rds-container > *, .rds-radiotext-plus > * { + margin: 2px 0; +} + +.rds-container .rds-stationname { + font-family: roboto-mono; + font-size: 18pt; + padding: 10px 0; +} + +.rds-container .rds-stationname, +.rds-container .rds-identifier, +.rds-container .rds-prog_type { + min-height: 1lh; +} + +.rds-container .rds-radiotext-plus .rds-rtplus-item:not(:empty):before { + content: "♫ "; +} + +.rds-container .rds-radiotext-plus .rds-rtplus-programme:not(:empty):before { + content: "📅 "; +} + +.rds-container .rds-radiotext-plus ul.rds-rtplus-news { + list-style-type: "📰 "; + padding-left: 1.5lh; +} + +.rds-container .rds-radiotext-plus .rds-rtplus-weather:not(:empty):before { + content: "⛅ "; +} + +.rds-container .rds-radiotext-plus .rds-rtplus-homepage:not(:empty):before { + content: "🔗 "; +} + +#openwebrx-panel-metadata-dab { + width: 300px; +} + +#openwebrx-panel-metadata-dab .dab-container { + width: 100%; +} + +.dab-container > * { + margin: 2px 0; + text-align: center; + overflow: hidden auto; +} + +.dab-container label { + display: block; + margin: 5px 0; +} + +.dab-container select#dab-service-id { + width: 100%; + padding: 3px; +} + +.dab-container .dab-ensemble-id:not(:empty):before { + content: "Ensemble ID: "; +} + +.dab-container .dab-ensemble-label:not(:empty):before { + content: "Ensemble: "; +} + +@keyframes openwebrx-record-animation { + 0% { background: #ff0000; color: white; } + 50% { background: #800000; color: white; } + 100% { background: #ff0000; color: white; } +} + +.openwebrx-record-button { + color: #ff8080; + float: right; + margin-top: 13px; + animation-duration: 1s; + animation-iteration-count: infinite; +} + +@keyframes openwebrx-scan-animation { + 0% { background: #00ff00; color: white; } + 50% { background: #008000; color: white; } + 100% { background: #00ff00; color: white; } +} + +.openwebrx-squelch-auto { + animation-duration: 1s; + animation-iteration-count: infinite; +} + +.openwebrx-squelch-auto .scanner { + display: none; +} + +.openwebrx-squelch-auto.highlighted .scanner { + display: initial; +} + +.openwebrx-squelch-auto.highlighted .squelch { + display: none; +} + .openwebrx-spectrum-container { transition-property: height, opacity; transition-duration: 1s; @@ -1773,3 +1853,4 @@ img.openwebrx-mirror-img .openwebrx-section.closed { max-height: 0; } + diff --git a/htdocs/index.html b/htdocs/index.html index 6c08683a..13f205dd 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -4,8 +4,8 @@ This file is part of OpenWebRX, an open-source SDR receiver software with a web UI. Copyright (c) 2013-2015 by Andras Retzler - Copyright (c) 2019-2021 by Jakob Ketterl - Copyright (c) 2022-2023 by Marat Fayzullin + Copyright (c) 2019-2024 by Jakob Ketterl + Copyright (c) 2022-2024 by Marat Fayzullin This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -164,6 +164,8 @@ + +
diff --git a/htdocs/lib/AprsMarker.js b/htdocs/lib/AprsMarker.js index 381c4ea9..e1673e29 100644 --- a/htdocs/lib/AprsMarker.js +++ b/htdocs/lib/AprsMarker.js @@ -6,10 +6,10 @@ AprsMarker.prototype.isFacingEast = function(symbol) { var candidates = '' if (symbol.table === '/') { // primary table - candidates = '(*<=>CFPUXYabefghjkpsuv['; + candidates = '(*<=>CFPUXYZabefgjkpsuv['; } else { // alternate table - candidates = 'hkluv'; + candidates = '(T`efhjktuvw'; } return candidates.includes(symbol.symbol); }; diff --git a/htdocs/lib/Demodulator.js b/htdocs/lib/Demodulator.js index 5eda28b7..a3c38274 100644 --- a/htdocs/lib/Demodulator.js +++ b/htdocs/lib/Demodulator.js @@ -15,6 +15,10 @@ Filter.prototype.getLimits = function() { max_bw = 50000; } else if (this.demodulator.get_modulation() === "freedv") { max_bw = 4000; + } else if (this.demodulator.get_modulation() === "dab") { + max_bw = 1000000; + } else if (this.demodulator.get_secondary_demod() === "ism") { + max_bw = 600000; } else { max_bw = (audioEngine.getOutputRate() / 2) - 1; } @@ -218,6 +222,7 @@ function Demodulator(offset_frequency, modulation) { this.filter = new Filter(this); this.squelch_level = -150; this.dmr_filter = 3; + this.dab_service_id = 0; this.started = false; this.state = {}; this.secondary_demod = false; @@ -307,6 +312,7 @@ Demodulator.prototype.set = function () { //this function sends demodulator par "offset_freq": this.offset_frequency, "mod": this.modulation, "dmr_filter": this.dmr_filter, + "dab_service_id": this.dab_service_id, "squelch_level": this.squelch_level, "secondary_mod": this.secondary_demod, "secondary_offset_freq": this.secondary_offset_freq @@ -344,6 +350,11 @@ Demodulator.prototype.setDmrFilter = function(dmr_filter) { this.set(); }; +Demodulator.prototype.setDabServiceId = function(dab_service_id) { + this.dab_service_id = dab_service_id; + this.set(); +} + Demodulator.prototype.setBandpass = function(bandpass) { this.bandpass = bandpass; this.low_cut = bandpass.low_cut; diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js index e1eec4d4..cf75228b 100644 --- a/htdocs/lib/DemodulatorPanel.js +++ b/htdocs/lib/DemodulatorPanel.js @@ -181,7 +181,7 @@ DemodulatorPanel.prototype.updatePanels = function() { var showing = 'openwebrx-panel-metadata-' + modulation; var metaPanels = $(".openwebrx-meta-panel"); metaPanels.each(function (_, p) { - toggle_panel(p.id, p.id === showing); + toggle_panel(p.id, p.id === showing && !p.classList.contains('disabled')); }); metaPanels.metaPanel().each(function() { this.clear(); diff --git a/htdocs/lib/MetaPanel.js b/htdocs/lib/MetaPanel.js index 8eabece9..584fadf6 100644 --- a/htdocs/lib/MetaPanel.js +++ b/htdocs/lib/MetaPanel.js @@ -359,12 +359,280 @@ M17MetaPanel.prototype.clear = function() { this.setDestination(); }; +function WfmMetaPanel(el) { + MetaPanel.call(this, el); + this.modes = ['WFM']; + this.enabled = false; + this.timeout = false; + this.clear(); +} + +WfmMetaPanel.prototype = new MetaPanel(); + +WfmMetaPanel.prototype.update = function(data) { + if (!this.isSupported(data)) return; + var me = this; + + // automatically clear metadata panel when no RDS data is received for more than ten seconds + if (this.timeout) clearTimeout(this.timeout); + this.timeout = setTimeout(function(){ + me.clear(); + }, 10000); + + if ('pi' in data && data.pi !== this.pi) { + this.clear(); + this.pi = data.pi; + } + + var $el = $(this.el); + + if ('ps' in data) { + this.ps = data.ps; + } + + if ('prog_type' in data) { + $el.find('.rds-prog_type').text(data['prog_type']); + } + + if ('callsign' in data) { + this.callsign = data.callsign; + } + + if ('pi' in data) { + this.pi = data.pi + } + + if ('clock_time' in data) { + var date = new Date(Date.parse(data.clock_time)); + $el.find('.rds-clock').text(date.toLocaleString([], {dateStyle: 'short', timeStyle: 'short'})); + } + + if ('radiotext_plus' in data) { + // prefer displaying radiotext plus over radiotext + this.radiotext_plus = this.radiotext_plus || { + item_toggle: -1, + news: [] + }; + + var tags = {}; + if ('tags' in data.radiotext_plus) { + tags = Object.fromEntries(data.radiotext_plus.tags.map(function (tag) { + return [tag['content-type'], tag['data']] + })); + } + + if (data.radiotext_plus.item_toggle !== this.radiotext_plus.item_toggle) { + this.radiotext_plus.item_toggle = data.radiotext_plus.item_toggle; + this.radiotext_plus.item = ''; + } + + this.radiotext_plus.item_running = !!data.radiotext_plus.item_running; + + if ('item.artist' in tags && 'item.title' in tags) { + this.radiotext_plus.item = tags['item.artist'] + ' - ' + tags['item.title']; + } else { + var items = Object.entries(tags).filter(function (e) { + return e[0].startsWith("item.") + }) + if (items.length) { + this.radiotext_plus.item = items.map(function (e) { + return e[0].substr(5, 1).toUpperCase() + e[0].substr(6) + ': ' + e[1]; + }).join('; '); + } + } + + if ('programme.now' in tags) { + this.radiotext_plus.programme = tags['programme.now']; + } + + if ('programme.homepage' in tags) { + this.radiotext_plus.homepage = tags['programme.homepage']; + } + + if ('stationname.long' in tags) { + this.long_stationname = tags['stationname.long']; + } + + if ('stationname.short' in tags) { + this.short_stationname = tags['stationname.short']; + } + + if ('info.news' in tags) { + var n = tags['info.news']; + var i = this.radiotext_plus.news.indexOf(n); + if (i >= 0) { + this.radiotext_plus.news.splice(i, 1); + } + this.radiotext_plus.news.push(n); + // limit the number of items + this.radiotext_plus.news = this.radiotext_plus.news.slice(-5); + } + + if ('info.weather' in tags) { + this.radiotext_plus.weather = tags['info.weather']; + } + + } + + if ('radiotext' in data && !this.radiotext_plus) { + this.radiotext = data.radiotext; + } + + if (this.radiotext_plus) { + $el.find('.rds-radiotext').empty(); + if (this.radiotext_plus.item_running) { + $el.find('.rds-rtplus-item').text(this.radiotext_plus.item || ''); + } else { + $el.find('.rds-rtplus-item').empty(); + } + $el.find('.rds-rtplus-programme').text(this.radiotext_plus.programme || ''); + $el.find('.rds-rtplus-news').empty().html(this.radiotext_plus.news.map(function(n){ + return $('
  • ').text(n); + })); + $el.find('.rds-rtplus-weather').text(this.radiotext_plus.weather || ''); + if (this.radiotext_plus.homepage) { + var url = this.radiotext_plus.homepage; + // prefix with a protcol if not present. we'll assume https, should be generally available these days. + if (url.indexOf('://') < 0) url = 'https://' + url; + // avoid updating the link if not necessary since that would prevent the user from clicking it + if ($el.find('.rds-rtplus-homepage a').attr('href') !== url) { + var link = $('').text(this.radiotext_plus.homepage); + $el.find('.rds-rtplus-homepage').html(link); + } + } + } else { + $el.find('.rds-radiotext-plus .autoclear').empty(); + $el.find('.rds-radiotext').text(this.radiotext || ''); + } + + $el.find('.rds-stationname').text(this.long_stationname || this.ps); + $el.find('.rds-identifier').text(this.short_stationname || this.callsign || this.pi); +}; + +WfmMetaPanel.prototype.isSupported = function(data) { + return this.modes.includes(data.mode); +}; + +WfmMetaPanel.prototype.setEnabled = function(enabled) { + if (enabled === this.enabled) return; + this.enabled = enabled; + if (enabled) { + $(this.el).removeClass('disabled').html( + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + ); + } else { + $(this.el).addClass('disabled').emtpy() + } +}; + +WfmMetaPanel.prototype.clear = function() { + $(this.el).find('.rds-autoclear').empty(); + this.pi = ''; + this.ps = ''; + this.callsign = ''; + this.long_stationname = ''; + this.short_stationname = ''; + + this.radiotext = ''; + this.radiotext_plus = false; +}; + +function DabMetaPanel(el) { + MetaPanel.call(this, el); + var me = this; + this.modes = ['DAB']; + this.service_id = 0; + this.$select = $(''); + this.$select.on("change", function() { + me.service_id = parseInt($(this).val()); + $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().setDabServiceId(me.service_id); + }); + var $container = $( + '
      ' + + '
      ' + + '
      ' + + '
      ' + + '' + + '
      ' + ); + $container.append(this.$select); + $(this.el).append($container); + this.clear(); + this.programmeTimeout = false; +} + +DabMetaPanel.prototype = new MetaPanel(); + +DabMetaPanel.prototype.isSupported = function(data) { + return this.modes.includes(data.mode); +} + + +DabMetaPanel.prototype.update = function(data) { + if (!this.isSupported(data)) return; + + if ('ensemble_id' in data) { + $(this.el).find('.dab-ensemble-id').text('0x' + data.ensemble_id.toString(16)); + } + + if ('ensemble_label' in data) { + $(this.el).find('.dab-ensemble-label').text(data.ensemble_label); + } + + if ('timestamp' in data) { + var date = new Date(data.timestamp * 1000); + $(this.el).find('.dab-timestamp').text(date.toLocaleString([], {dateStyle: 'short', timeStyle: 'medium'})); + } + + if ('programmes' in data) { + var options = Object.entries(data.programmes).map(function(e) { + return ''; + }); + this.$select.html( + options.join('') + + '' + ); + + var me = this; + if (this.programmeTimeout) clearTimeout(this.programmeTimeout); + this.programmeTimeout = setTimeout(function() { + // user has selected a programme to play. don't interfere. + me.$select.val(this.service_id); + if (me.$select.val()) return; + me.$select.val(me.$select.find('option:first').val()).change(); + }, 1000); + } +} + +DabMetaPanel.prototype.clear = function() { + this.service_id = 0; + $(this.el).find('.dab-auto-clear').empty(); + this.$select.html( + '' + ); +} + MetaPanel.types = { dmr: DmrMetaPanel, ysf: YsfMetaPanel, dstar: DStarMetaPanel, nxdn: NxdnMetaPanel, m17: M17MetaPanel, + wfm: WfmMetaPanel, + dab: DabMetaPanel, }; $.fn.metaPanel = function() { diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 24751da1..1499c796 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -4,7 +4,7 @@ an open-source SDR receiver software with a web UI. Copyright (c) 2013-2015 by Andras Retzler Copyright (c) 2019-2021 by Jakob Ketterl - Copyright (c) 2022-2023 by Marat Fayzullin + Copyright (c) 2022-2024 by Marat Fayzullin This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as diff --git a/htdocs/settings.html b/htdocs/settings.html index dbc9667f..a7b52f97 100644 --- a/htdocs/settings.html +++ b/htdocs/settings.html @@ -57,7 +57,3 @@ ${header}
    • - - -
      -