Adding all current customizations to the original forked code.

This commit is contained in:
Marat Fayzullin 2022-11-19 14:34:47 -05:00
parent e20d94e241
commit e3780f6aea
13 changed files with 255 additions and 49 deletions

View File

@ -1,15 +1,18 @@
from csdr.chain import Chain from csdr.chain import Chain
from pycsdr.modules import AudioResampler, Convert, AdpcmEncoder, Limit from pycsdr.modules import AudioResampler, Convert, AdpcmEncoder, Limit, NoiseFilter
from pycsdr.types import Format from pycsdr.types import Format
class Converter(Chain): class Converter(Chain):
def __init__(self, format: Format, inputRate: int, clientRate: int): def __init__(self, format: Format, inputRate: int, clientRate: int, nrEnabled: bool, nrThreshold: int):
workers = [] workers = []
if inputRate != clientRate: # we only have an audio resampler and noise filter for float ATM,
# we only have an audio resampler for float ATM so if we need to resample, we need to convert # so if we need to resample or remove noise, we need to convert
if format != Format.FLOAT: if (inputRate != clientRate or nrEnabled) and format != Format.FLOAT:
workers += [Convert(format, Format.FLOAT)] workers += [Convert(format, Format.FLOAT)]
if nrEnabled:
workers += [NoiseFilter(nrThreshold)]
if inputRate != clientRate:
workers += [AudioResampler(inputRate, clientRate), Limit(), Convert(Format.FLOAT, Format.SHORT)] workers += [AudioResampler(inputRate, clientRate), Limit(), Convert(Format.FLOAT, Format.SHORT)]
elif format != Format.SHORT: elif format != Format.SHORT:
workers += [Convert(format, Format.SHORT)] workers += [Convert(format, Format.SHORT)]
@ -17,10 +20,12 @@ class Converter(Chain):
class ClientAudioChain(Chain): class ClientAudioChain(Chain):
def __init__(self, format: Format, inputRate: int, clientRate: int, compression: str): def __init__(self, format: Format, inputRate: int, clientRate: int, compression: str, nrEnabled: bool, nrThreshold: int):
self.format = format self.format = format
self.inputRate = inputRate self.inputRate = inputRate
self.clientRate = clientRate self.clientRate = clientRate
self.nrEnabled = nrEnabled
self.nrThreshold = nrThreshold
workers = [] workers = []
converter = self._buildConverter() converter = self._buildConverter()
if not converter.empty(): if not converter.empty():
@ -30,7 +35,7 @@ class ClientAudioChain(Chain):
super().__init__(workers) super().__init__(workers)
def _buildConverter(self): def _buildConverter(self):
return Converter(self.format, self.inputRate, self.clientRate) return Converter(self.format, self.inputRate, self.clientRate, self.nrEnabled, self.nrThreshold)
def _updateConverter(self): def _updateConverter(self):
converter = self._buildConverter() converter = self._buildConverter()
@ -70,3 +75,15 @@ class ClientAudioChain(Chain):
else: else:
if index >= 0: if index >= 0:
self.remove(index) self.remove(index)
def setNrEnabled(self, nrEnabled: bool) -> None:
if nrEnabled == self.nrEnabled:
return
self.nrEnabled = nrEnabled
self._updateConverter()
def setNrThreshold(self, nrThreshold: int) -> None:
if nrThreshold == self.nrThreshold:
return
self.nrThreshold = nrThreshold
self._updateConverter()

View File

@ -24,5 +24,7 @@
<g id="play-button"><circle cx="350" cy="350" r="330" fill="none" stroke="#fff" stroke-width="36"/><path d="M195 211v278l366-139z" fill="#fff"/></g> <g id="play-button"><circle cx="350" cy="350" r="330" fill="none" stroke="#fff" stroke-width="36"/><path d="M195 211v278l366-139z" fill="#fff"/></g>
<g id="meta-mute" stroke="#fff" stroke-width="5"><path stroke-linejoin="round" style="paint-order:fill" fill="none" d="m21.989 47.699 17.4 15.051V13.769L22.235 28.606H6v19.093z" transform="matrix(5.3513 0 0 5.3723 -.73 -1.542)"/><path d="m48.652 50.27 20.743-24.299M69.395 50.27 48.652 25.971" stroke-linecap="round" transform="matrix(5.3513 0 0 5.3723 -.73 -1.542)"/></g> <g id="meta-mute" stroke="#fff" stroke-width="5"><path stroke-linejoin="round" style="paint-order:fill" fill="none" d="m21.989 47.699 17.4 15.051V13.769L22.235 28.606H6v19.093z" transform="matrix(5.3513 0 0 5.3723 -.73 -1.542)"/><path d="m48.652 50.27 20.743-24.299M69.395 50.27 48.652 25.971" stroke-linecap="round" transform="matrix(5.3513 0 0 5.3723 -.73 -1.542)"/></g>
<g id="waterfall-continuous"><g stroke="#fff" stroke-width="8"><path d="M5 40A35 35 0 0 1 26.606 7.664a35 35 0 0 1 38.143 7.587" fill="none"/><path d="m68.284 11.716 2.828 9.9-9.899-2.829z" fill="#fff"/></g><path d="m48.008 48.144 2.816 8.624h9.035L45.075 14h-9.739L20.141 56.768h8.976l2.875-8.624zm-2.405-7.333H34.456l5.573-16.72z" fill="#fff" aria-label="A"/><g stroke="#fff" stroke-width="8"><path d="M75 40a35 35 0 0 1-21.606 32.336 35 35 0 0 1-38.143-7.587" fill="none"/><path d="m11.716 68.284-2.828-9.9 9.899 2.829z" fill="#fff"/></g></g> <g id="waterfall-continuous"><g stroke="#fff" stroke-width="8"><path d="M5 40A35 35 0 0 1 26.606 7.664a35 35 0 0 1 38.143 7.587" fill="none"/><path d="m68.284 11.716 2.828 9.9-9.899-2.829z" fill="#fff"/></g><path d="m48.008 48.144 2.816 8.624h9.035L45.075 14h-9.739L20.141 56.768h8.976l2.875-8.624zm-2.405-7.333H34.456l5.573-16.72z" fill="#fff" aria-label="A"/><g stroke="#fff" stroke-width="8"><path d="M75 40a35 35 0 0 1-21.606 32.336 35 35 0 0 1-38.143-7.587" fill="none"/><path d="m11.716 68.284-2.828-9.9 9.899 2.829z" fill="#fff"/></g></g>
<g id="noise-reduce" fill="#fff" stroke-width=".767"><path d="m 36.606611,74.237282 -9.008789,0 L 11.11833,30.951149 q 0.659179,3.164062 0.659179,4.96582 l 0,38.320313 -10.0195308,0 0,-68.7744143 9.0087888,0 16.479492,42.4072263 Q 26.58708,44.706032 26.58708,42.904274 l 0,-37.4414063 10.019531,0 0,68.7744143 z"/><path d="m 80.200361,74.237282 -10.722656,0 L 58.139814,42.28904 q 0,0 0,-6.196289 l 10.107422,0 0,-21.796875 -12.963867,0 0,59.941406 -10.283203,0 0,-68.7744143 27.290039,0 q 6.196289,0 6.196289,6.3281253 l 0,25.004883 q 0,4.350585 -2.944336,5.625 -1.582031,0.659179 -6.679688,0.659179 l 11.337891,31.157227 z"/></g>
<g id="disabled" fill="#fff" stroke-width=".767"><path d="M 59.826868,-0.58229828 28.318079,81.155983 l -7.602539,0 31.376953,-81.73828128 7.734375,0 z"/></g>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -201,6 +201,12 @@
</div> </div>
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()"> <input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
</div> </div>
<div class="openwebrx-panel-line">
<div title="Noise reduction on/off" class="openwebrx-nr-toggle openwebrx-button openwebrx-slider-button" onclick="toggleNR();">
<svg viewBox="0 0 80 80"><use xlink:href="static/gfx/svg-defs.svg#noise-reduce"></use></svg>
</div>
<input title="Noise reduction level" disabled id="openwebrx-panel-nr" class="openwebrx-panel-slider" type="range" min="-10" max="10" value="0" step="1" onchange="updateNR()" oninput="updateNR()">
</div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">
<div class="openwebrx-button openwebrx-square-button openwebrx-zoom-button" onclick="zoomInOneStep();" title="Zoom in one step"><svg viewBox="0 0 80 80"><use xlink:href="static/gfx/svg-defs.svg#zoom-in"></use></svg></div> <div class="openwebrx-button openwebrx-square-button openwebrx-zoom-button" onclick="zoomInOneStep();" title="Zoom in one step"><svg viewBox="0 0 80 80"><use xlink:href="static/gfx/svg-defs.svg#zoom-in"></use></svg></div>
<div class="openwebrx-button openwebrx-square-button openwebrx-zoom-button" onclick="zoomOutOneStep();" title="Zoom out one step"><svg viewBox="0 0 80 80"><use xlink:href="static/gfx/svg-defs.svg#zoom-out"></use></svg></div> <div class="openwebrx-button openwebrx-square-button openwebrx-zoom-button" onclick="zoomOutOneStep();" title="Zoom out one step"><svg viewBox="0 0 80 80"><use xlink:href="static/gfx/svg-defs.svg#zoom-out"></use></svg></div>

View File

@ -81,6 +81,12 @@ Envelope.prototype.draw = function(visible_range){
scale_ctx.fill(); scale_ctx.fill();
scale_ctx.globalAlpha = 1; scale_ctx.globalAlpha = 1;
scale_ctx.stroke(); scale_ctx.stroke();
scale_ctx.lineWidth = 1;
scale_ctx.textAlign = "left";
scale_ctx.fillText(this.demodulator.high_cut.toString(), to_px + env_att_w, env_h2);
scale_ctx.textAlign = "right";
scale_ctx.fillText(this.demodulator.low_cut.toString(), from_px - env_att_w, env_h2);
scale_ctx.lineWidth = 3;
} }
if (typeof line !== "undefined") // out of screen? if (typeof line !== "undefined") // out of screen?
{ {

View File

@ -37,6 +37,7 @@ $(function(){
var retention_time = 2 * 60 * 60 * 1000; var retention_time = 2 * 60 * 60 * 1000;
var strokeOpacity = 0.8; var strokeOpacity = 0.8;
var fillOpacity = 0.35; var fillOpacity = 0.35;
var callsign_url = null;
var colorKeys = {}; var colorKeys = {};
var colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl'); var colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl');
@ -286,6 +287,9 @@ $(function(){
if ('map_position_retention_time' in config) { if ('map_position_retention_time' in config) {
retention_time = config.map_position_retention_time * 1000; retention_time = config.map_position_retention_time * 1000;
} }
if ('callsign_url' in config) {
callsign_url = config['callsign_url'];
}
break; break;
case "update": case "update":
processUpdates(json.value); processUpdates(json.value);
@ -340,6 +344,32 @@ $(function(){
return infowindow; return infowindow;
} }
var linkifyCallsign = function(callsign) {
if ((callsign_url == null) || (callsign_url == ''))
return callsign;
else
return '<a target="callsign_info" href="' +
callsign_url.replaceAll('{}', callsign.replace(new RegExp('-.*$'), '')) +
'">' + callsign + '</a>';
};
var distanceKm = function(p1, p2) {
// Earth radius in km
var R = 6371.0;
// Convert degrees to radians
var rlat1 = p1.lat() * (Math.PI/180);
var rlat2 = p2.lat() * (Math.PI/180);
// Compute difference in radians
var difflat = rlat2-rlat1;
var difflon = (p2.lng()-p1.lng()) * (Math.PI/180);
// Compute distance
d = 2 * R * Math.asin(Math.sqrt(
Math.sin(difflat/2) * Math.sin(difflat/2) +
Math.cos(rlat1) * Math.cos(rlat2) * Math.sin(difflon/2) * Math.sin(difflon/2)
));
return Math.round(d);
}
var infowindow; var infowindow;
var showLocatorInfoWindow = function(locator, pos) { var showLocatorInfoWindow = function(locator, pos) {
var infowindow = getInfoWindow(); var infowindow = getInfoWindow();
@ -351,13 +381,15 @@ $(function(){
}).sort(function(a, b){ }).sort(function(a, b){
return b.lastseen - a.lastseen; return b.lastseen - a.lastseen;
}); });
var distance = receiverMarker?
" at " + distanceKm(receiverMarker.position, pos) + " km" : "";
infowindow.setContent( infowindow.setContent(
'<h3>Locator: ' + locator + '</h3>' + '<h3>Locator: ' + locator + distance + '</h3>' +
'<div>Active Callsigns:</div>' + '<div>Active Callsigns:</div>' +
'<ul>' + '<ul>' +
inLocator.map(function(i){ inLocator.map(function(i){
var timestring = moment(i.lastseen).fromNow(); var timestring = moment(i.lastseen).fromNow();
var message = i.callsign + ' (' + timestring + ' using ' + i.mode; var message = linkifyCallsign(i.callsign) + ' (' + timestring + ' using ' + i.mode;
if (i.band) message += ' on ' + i.band; if (i.band) message += ' on ' + i.band;
message += ')'; message += ')';
return '<li>' + message + '</li>' return '<li>' + message + '</li>'
@ -374,11 +406,15 @@ $(function(){
var marker = markers[callsign]; var marker = markers[callsign];
var timestring = moment(marker.lastseen).fromNow(); var timestring = moment(marker.lastseen).fromNow();
var commentString = ""; var commentString = "";
var distance = "";
if (marker.comment) { if (marker.comment) {
commentString = '<div>' + marker.comment + '</div>'; commentString = '<div>' + marker.comment + '</div>';
} }
if (receiverMarker) {
distance = " at " + distanceKm(receiverMarker.position, marker.position) + " km";
}
infowindow.setContent( infowindow.setContent(
'<h3>' + callsign + '</h3>' + '<h3>' + linkifyCallsign(callsign) + distance + '</h3>' +
'<div>' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '</div>' + '<div>' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '</div>' +
commentString commentString
); );

View File

@ -31,6 +31,9 @@ var fft_compression = "none";
var fft_codec; var fft_codec;
var waterfall_setup_done = 0; var waterfall_setup_done = 0;
var secondary_fft_size; var secondary_fft_size;
var tuning_step = 1;
var nr_enabled = false;
var nr_threshold = 0;
function updateVolume() { function updateVolume() {
audioEngine.setVolume(parseFloat($("#openwebrx-panel-volume").val()) / 100); audioEngine.setVolume(parseFloat($("#openwebrx-panel-volume").val()) / 100);
@ -51,6 +54,28 @@ function toggleMute() {
updateVolume(); updateVolume();
} }
function updateNR() {
var $nrPanel = $('#openwebrx-panel-nr');
nr_threshold = Math.round(parseFloat($nrPanel.val()));
$nrPanel.attr('title', 'Noise level (' + nr_threshold + ' dB)');
nr_changed();
}
function toggleNR() {
var $nrPanel = $('#openwebrx-panel-nr');
if ($nrPanel.prop('disabled')) {
$nrPanel.prop('disabled', false);
nr_enabled = true;
} else {
$nrPanel.prop('disabled', true);
nr_enabled = false;
}
nr_changed();
}
function zoomInOneStep() { function zoomInOneStep() {
zoom_set(zoom_level + 1); zoom_set(zoom_level + 1);
} }
@ -257,18 +282,23 @@ var scale_canvas_drag_params = {
}; };
function scale_canvas_mousedown(evt) { function scale_canvas_mousedown(evt) {
// Left button only
if (evt.button == 0) {
scale_canvas_drag_params.mouse_down = true; scale_canvas_drag_params.mouse_down = true;
scale_canvas_drag_params.drag = false; scale_canvas_drag_params.drag = false;
scale_canvas_drag_params.start_x = evt.pageX; scale_canvas_drag_params.start_x = evt.pageX;
scale_canvas_drag_params.key_modifiers.shiftKey = evt.shiftKey; scale_canvas_drag_params.key_modifiers.shiftKey = evt.shiftKey;
scale_canvas_drag_params.key_modifiers.altKey = evt.altKey; scale_canvas_drag_params.key_modifiers.altKey = evt.altKey;
scale_canvas_drag_params.key_modifiers.ctrlKey = evt.ctrlKey; scale_canvas_drag_params.key_modifiers.ctrlKey = evt.ctrlKey;
}
evt.preventDefault(); evt.preventDefault();
} }
function scale_offset_freq_from_px(x, visible_range) { function scale_offset_freq_from_px(x, visible_range) {
if (typeof visible_range === "undefined") visible_range = get_visible_freq_range(); if (typeof visible_range === "undefined") visible_range = get_visible_freq_range();
return (visible_range.start + visible_range.bw * (x / waterfallWidth())) - center_freq;
var f = (visible_range.start + visible_range.bw * (x / waterfallWidth())) - center_freq;
return tuning_step>0? Math.round(f / tuning_step) * tuning_step : f;
} }
function scale_canvas_mousemove(evt) { function scale_canvas_mousemove(evt) {
@ -307,6 +337,7 @@ function scale_canvas_end_drag(x) {
} }
function scale_canvas_mouseup(evt) { function scale_canvas_mouseup(evt) {
if (evt.button == 0)
scale_canvas_end_drag(evt.pageX); scale_canvas_end_drag(evt.pageX);
} }
@ -513,11 +544,16 @@ function resize_scale() {
function canvas_get_freq_offset(relativeX) { function canvas_get_freq_offset(relativeX) {
var rel = (relativeX / canvas_container.clientWidth); var rel = (relativeX / canvas_container.clientWidth);
return Math.round((bandwidth * rel) - (bandwidth / 2)); var off = (bandwidth * rel) - (bandwidth / 2);
return tuning_step>0?
Math.round(off / tuning_step) * tuning_step : Math.round(off);
} }
function canvas_get_frequency(relativeX) { function canvas_get_frequency(relativeX) {
return center_freq + canvas_get_freq_offset(relativeX); var f = center_freq + canvas_get_freq_offset(relativeX);
return tuning_step>0? Math.round(f / tuning_step) * tuning_step : f;
} }
@ -535,16 +571,23 @@ function format_frequency(format, freq_hz, pre_divide, decimals) {
var canvas_drag = false; var canvas_drag = false;
var canvas_drag_min_delta = 1; var canvas_drag_min_delta = 1;
var canvas_mouse_down = false; var canvas_mouse_down = false;
var canvas_mouse2_down = 0;
var canvas_drag_last_x; var canvas_drag_last_x;
var canvas_drag_last_y; var canvas_drag_last_y;
var canvas_drag_start_x; var canvas_drag_start_x;
var canvas_drag_start_y; var canvas_drag_start_y;
function canvas_mousedown(evt) { function canvas_mousedown(evt) {
if (evt.button > 0)
if (canvas_mouse2_down == 0)
canvas_mouse2_down = evt.button;
else {
canvas_mouse_down = true; canvas_mouse_down = true;
canvas_drag = false; canvas_drag = false;
canvas_drag_last_x = canvas_drag_start_x = evt.pageX; canvas_drag_last_x = canvas_drag_start_x = evt.pageX;
canvas_drag_last_y = canvas_drag_start_y = evt.pageY; canvas_drag_last_y = canvas_drag_start_y = evt.pageY;
}
evt.preventDefault(); //don't show text selection mouse pointer evt.preventDefault(); //don't show text selection mouse pointer
} }
@ -581,6 +624,10 @@ function canvas_container_mouseleave() {
} }
function canvas_mouseup(evt) { function canvas_mouseup(evt) {
if (evt.button > 0) {
if (evt.button == canvas_mouse2_down)
canvas_mouse2_down = 0;
} else {
if (!waterfall_setup_done) return; if (!waterfall_setup_done) return;
var relativeX = get_relative_x(evt); var relativeX = get_relative_x(evt);
@ -591,6 +638,7 @@ function canvas_mouseup(evt) {
canvas_end_drag(); canvas_end_drag();
} }
canvas_mouse_down = false; canvas_mouse_down = false;
}
} }
function canvas_end_drag() { function canvas_end_drag() {
@ -618,7 +666,16 @@ function canvas_mousewheel(evt) {
if (!waterfall_setup_done) return; if (!waterfall_setup_done) return;
var relativeX = get_relative_x(evt); var relativeX = get_relative_x(evt);
var dir = (evt.deltaY / Math.abs(evt.deltaY)) > 0; var dir = (evt.deltaY / Math.abs(evt.deltaY)) > 0;
// Zoom when mouse button down, tune otherwise
if (canvas_mouse2_down > 0) {
zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX)); zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX));
} else {
var f = $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().get_offset_frequency();
f += dir? -tuning_step : tuning_step;
$('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().set_offset_frequency(f);
}
evt.preventDefault(); evt.preventDefault();
} }
@ -771,11 +828,15 @@ function on_ws_recv(evt) {
$('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString()); $('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString());
waterfall_clear(); waterfall_clear();
zoom_set(0);
} }
if ('tuning_precision' in config) if ('tuning_precision' in config)
$('#openwebrx-panel-receiver').demodulatorPanel().setTuningPrecision(config['tuning_precision']); $('#openwebrx-panel-receiver').demodulatorPanel().setTuningPrecision(config['tuning_precision']);
if ('tuning_step' in config)
tuning_step = config['tuning_step'];
break; break;
case "secondary_config": case "secondary_config":
var s = json['value']; var s = json['value'];
@ -935,9 +996,15 @@ var waterfall_measure_minmax_now = false;
var waterfall_measure_minmax_continuous = false; var waterfall_measure_minmax_continuous = false;
function waterfall_measure_minmax_do(what) { function waterfall_measure_minmax_do(what) {
// Get visible range
var range = get_visible_freq_range();
var start = center_freq - bandwidth / 2;
// this is based on an oversampling factor of about 1,25 // this is based on an oversampling factor of about 1,25
var ignored = .1 * what.length; range.start = Math.max(0.1, (range.start - start) / bandwidth);
var data = what.slice(ignored, -ignored); range.end = Math.min(0.9, (range.end - start) / bandwidth);
var data = what.slice(range.start * what.length, range.end * what.length);
return { return {
min: Math.min.apply(Math, data), min: Math.min.apply(Math, data),
max: Math.max.apply(Math, data) max: Math.max.apply(Math, data)
@ -1556,3 +1623,13 @@ function sdr_profile_changed() {
var value = $('#openwebrx-sdr-profiles-listbox').val(); var value = $('#openwebrx-sdr-profiles-listbox').val();
ws.send(JSON.stringify({type: "selectprofile", params: {profile: value}})); ws.send(JSON.stringify({type: "selectprofile", params: {profile: value}}));
} }
function nr_changed() {
ws.send(JSON.stringify({
"type": "connectionproperties",
"params": {
"nr_enabled": nr_enabled,
"nr_threshold": nr_threshold
}
}));
}

View File

@ -33,6 +33,7 @@ defaultConfig = PropertyLayer(
samp_rate=2400000, samp_rate=2400000,
start_freq=439275000, start_freq=439275000,
start_mod="nfm", start_mod="nfm",
tuning_step="1000",
), ),
"2m": PropertyLayer( "2m": PropertyLayer(
name="2m", name="2m",
@ -41,6 +42,7 @@ defaultConfig = PropertyLayer(
samp_rate=2048000, samp_rate=2048000,
start_freq=145725000, start_freq=145725000,
start_mod="nfm", start_mod="nfm",
tuning_step="1000",
), ),
} }
), ),
@ -57,6 +59,7 @@ defaultConfig = PropertyLayer(
samp_rate=384000, samp_rate=384000,
start_freq=14070000, start_freq=14070000,
start_mod="usb", start_mod="usb",
tuning_step="500",
), ),
"30m": PropertyLayer( "30m": PropertyLayer(
name="30m", name="30m",
@ -64,6 +67,7 @@ defaultConfig = PropertyLayer(
samp_rate=192000, samp_rate=192000,
start_freq=10142000, start_freq=10142000,
start_mod="usb", start_mod="usb",
tuning_step="500",
), ),
"40m": PropertyLayer( "40m": PropertyLayer(
name="40m", name="40m",
@ -71,6 +75,7 @@ defaultConfig = PropertyLayer(
samp_rate=256000, samp_rate=256000,
start_freq=7070000, start_freq=7070000,
start_mod="lsb", start_mod="lsb",
tuning_step="500",
), ),
"80m": PropertyLayer( "80m": PropertyLayer(
name="80m", name="80m",
@ -78,6 +83,7 @@ defaultConfig = PropertyLayer(
samp_rate=384000, samp_rate=384000,
start_freq=3570000, start_freq=3570000,
start_mod="lsb", start_mod="lsb",
tuning_step="500",
), ),
"49m": PropertyLayer( "49m": PropertyLayer(
name="49m Broadcast", name="49m Broadcast",
@ -85,6 +91,7 @@ defaultConfig = PropertyLayer(
samp_rate=384000, samp_rate=384000,
start_freq=6070000, start_freq=6070000,
start_mod="am", start_mod="am",
tuning_step="1000",
), ),
} }
), ),
@ -102,6 +109,7 @@ defaultConfig = PropertyLayer(
samp_rate=500000, samp_rate=500000,
start_freq=14070000, start_freq=14070000,
start_mod="usb", start_mod="usb",
tuning_step="500",
), ),
"30m": PropertyLayer( "30m": PropertyLayer(
name="30m", name="30m",
@ -110,6 +118,7 @@ defaultConfig = PropertyLayer(
samp_rate=250000, samp_rate=250000,
start_freq=10142000, start_freq=10142000,
start_mod="usb", start_mod="usb",
tuning_step="500",
), ),
"40m": PropertyLayer( "40m": PropertyLayer(
name="40m", name="40m",
@ -118,6 +127,7 @@ defaultConfig = PropertyLayer(
samp_rate=500000, samp_rate=500000,
start_freq=7070000, start_freq=7070000,
start_mod="lsb", start_mod="lsb",
tuning_step="500",
), ),
"80m": PropertyLayer( "80m": PropertyLayer(
name="80m", name="80m",
@ -126,6 +136,7 @@ defaultConfig = PropertyLayer(
samp_rate=500000, samp_rate=500000,
start_freq=3570000, start_freq=3570000,
start_mod="lsb", start_mod="lsb",
tuning_step="500",
), ),
"49m": PropertyLayer( "49m": PropertyLayer(
name="49m Broadcast", name="49m Broadcast",
@ -134,6 +145,7 @@ defaultConfig = PropertyLayer(
samp_rate=500000, samp_rate=500000,
start_freq=6070000, start_freq=6070000,
start_mod="am", start_mod="am",
tuning_step="1000",
), ),
} }
), ),
@ -147,6 +159,7 @@ defaultConfig = PropertyLayer(
squelch_auto_margin=10, squelch_auto_margin=10,
google_maps_api_key="", google_maps_api_key="",
map_position_retention_time=2 * 60 * 60, map_position_retention_time=2 * 60 * 60,
callsign_url="https://www.qrzcq.com/call/{}",
decoding_queue_workers=2, decoding_queue_workers=2,
decoding_queue_length=10, decoding_queue_length=10,
wsjt_decoding_depth=3, wsjt_decoding_depth=3,

View File

@ -120,6 +120,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
"start_mod", "start_mod",
"start_freq", "start_freq",
"center_freq", "center_freq",
"tuning_step",
"initial_squelch_level", "initial_squelch_level",
"sdr_id", "sdr_id",
"profile_id", "profile_id",
@ -456,6 +457,7 @@ class MapConnection(OpenWebRxClient):
"google_maps_api_key", "google_maps_api_key",
"receiver_gps", "receiver_gps",
"map_position_retention_time", "map_position_retention_time",
"callsign_url",
"receiver_name", "receiver_name",
) )
filtered_config.wire(self.write_config) filtered_config.wire(self.write_config)

View File

@ -168,6 +168,13 @@ class GeneralSettingsController(SettingsFormController):
infotext="Specifies how log markers / grids will remain visible on the map", infotext="Specifies how log markers / grids will remain visible on the map",
append="s", append="s",
), ),
TextInput(
"callsign_url",
"Callsign database URL",
infotext="Specifies callsign lookup URL, such as QRZ.COM "
+ "or QRZCQ.COM. Place curly brackers ({}) where callsign "
+ "is supposed to be.",
),
), ),
] ]

View File

@ -34,10 +34,12 @@ class ClientDemodulatorSecondaryDspEventClient(ABC):
class ClientDemodulatorChain(Chain): class ClientDemodulatorChain(Chain):
def __init__(self, demod: BaseDemodulatorChain, sampleRate: int, outputRate: int, hdOutputRate: int, audioCompression: str, secondaryDspEventReceiver: ClientDemodulatorSecondaryDspEventClient): def __init__(self, demod: BaseDemodulatorChain, sampleRate: int, outputRate: int, hdOutputRate: int, audioCompression: str, nrEnabled: bool, nrThreshold: int, secondaryDspEventReceiver: ClientDemodulatorSecondaryDspEventClient):
self.sampleRate = sampleRate self.sampleRate = sampleRate
self.outputRate = outputRate self.outputRate = outputRate
self.hdOutputRate = hdOutputRate self.hdOutputRate = hdOutputRate
self.nrEnabled = nrEnabled
self.nrThreshold = nrThreshold
self.secondaryDspEventReceiver = secondaryDspEventReceiver self.secondaryDspEventReceiver = secondaryDspEventReceiver
self.selector = Selector(sampleRate, outputRate) self.selector = Selector(sampleRate, outputRate)
self.selector.setBandpass(-4000, 4000) self.selector.setBandpass(-4000, 4000)
@ -50,7 +52,7 @@ class ClientDemodulatorChain(Chain):
self.wfmDeemphasisTau = 50e-6 self.wfmDeemphasisTau = 50e-6
inputRate = demod.getFixedAudioRate() if isinstance(demod, FixedAudioRateChain) else outputRate inputRate = demod.getFixedAudioRate() if isinstance(demod, FixedAudioRateChain) else outputRate
oRate = hdOutputRate if isinstance(demod, HdAudio) else outputRate oRate = hdOutputRate if isinstance(demod, HdAudio) else outputRate
self.clientAudioChain = ClientAudioChain(demod.getOutputFormat(), inputRate, oRate, audioCompression) self.clientAudioChain = ClientAudioChain(demod.getOutputFormat(), inputRate, oRate, audioCompression, nrEnabled, nrThreshold)
self.secondaryFftSize = 2048 self.secondaryFftSize = 2048
self.secondaryFftOverlapFactor = 0.3 self.secondaryFftOverlapFactor = 0.3
self.secondaryFftFps = 9 self.secondaryFftFps = 9
@ -251,6 +253,12 @@ class ClientDemodulatorChain(Chain):
def setAudioCompression(self, compression: str) -> None: def setAudioCompression(self, compression: str) -> None:
self.clientAudioChain.setAudioCompression(compression) self.clientAudioChain.setAudioCompression(compression)
def setNrEnabled(self, nrEnabled: bool) -> None:
self.clientAudioChain.setNrEnabled(nrEnabled)
def setNrThreshold(self, nrThreshold: int) -> None:
self.clientAudioChain.setNrThreshold(nrThreshold)
def setSquelchLevel(self, level: float) -> None: def setSquelchLevel(self, level: float) -> None:
if level == self.squelchLevel: if level == self.squelchLevel:
return return
@ -409,6 +417,8 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
"mod": ModulationValidator(), "mod": ModulationValidator(),
"secondary_offset_freq": "int", "secondary_offset_freq": "int",
"dmr_filter": "int", "dmr_filter": "int",
"nr_enabled": "bool",
"nr_threshold": "int",
} }
self.localProps = PropertyValidator(PropertyLayer().filter(*validators.keys()), validators) self.localProps = PropertyValidator(PropertyLayer().filter(*validators.keys()), validators)
@ -436,6 +446,8 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
output_rate=12000, output_rate=12000,
hd_output_rate=48000, hd_output_rate=48000,
digital_voice_codecserver="", digital_voice_codecserver="",
nr_enabled=False,
nr_threshold=0
).readonly() ).readonly()
) )
@ -445,6 +457,8 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
self.props["output_rate"], self.props["output_rate"],
self.props["hd_output_rate"], self.props["hd_output_rate"],
self.props["audio_compression"], self.props["audio_compression"],
self.props["nr_enabled"],
self.props["nr_threshold"],
self self
) )
@ -487,6 +501,8 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
self.props.wireProperty("wfm_deemphasis_tau", self.chain.setWfmDeemphasisTau), self.props.wireProperty("wfm_deemphasis_tau", self.chain.setWfmDeemphasisTau),
self.props.wireProperty("secondary_mod", self.setSecondaryDemodulator), self.props.wireProperty("secondary_mod", self.setSecondaryDemodulator),
self.props.wireProperty("secondary_offset_freq", self.chain.setSecondaryFrequencyOffset), self.props.wireProperty("secondary_offset_freq", self.chain.setSecondaryFrequencyOffset),
self.props.wireProperty("nr_enabled", self.chain.setNrEnabled),
self.props.wireProperty("nr_threshold", self.chain.setNrThreshold),
] ]
# wire power level output # wire power level output

View File

@ -94,4 +94,5 @@ validator_types = {
"int": IntegerValidator, "int": IntegerValidator,
"number": NumberValidator, "number": NumberValidator,
"num": NumberValidator, "num": NumberValidator,
"bool": BoolValidator,
} }

View File

@ -12,8 +12,8 @@ from owrx.command import CommandMapper
from owrx.socket import getAvailablePort from owrx.socket import getAvailablePort
from owrx.property import PropertyStack, PropertyLayer, PropertyFilter, PropertyCarousel, PropertyDeleted from owrx.property import PropertyStack, PropertyLayer, PropertyFilter, PropertyCarousel, PropertyDeleted
from owrx.property.filter import ByLambda from owrx.property.filter import ByLambda
from owrx.form.input import Input, TextInput, NumberInput, CheckboxInput, ModesInput, ExponentialInput from owrx.form.input import Input, TextInput, NumberInput, CheckboxInput, ModesInput, ExponentialInput, DropdownInput, Option
from owrx.form.input.converter import OptionalConverter from owrx.form.input.converter import OptionalConverter, IntConverter
from owrx.form.input.device import GainInput, SchedulerInput, WaterfallLevelsInput from owrx.form.input.device import GainInput, SchedulerInput, WaterfallLevelsInput
from owrx.form.input.validator import RequiredValidator from owrx.form.input.validator import RequiredValidator
from owrx.form.section import OptionalSection from owrx.form.section import OptionalSection
@ -277,8 +277,9 @@ class SdrSource(ABC):
if self.monitor: if self.monitor:
return return
if self.isFailed(): # @@@
return # if self.isFailed():
# return
try: try:
self.preStart() self.preStart()
@ -568,6 +569,12 @@ class SdrDeviceDescription(object):
ExponentialInput("samp_rate", "Sample rate", "S/s"), ExponentialInput("samp_rate", "Sample rate", "S/s"),
ExponentialInput("start_freq", "Initial frequency", "Hz"), ExponentialInput("start_freq", "Initial frequency", "Hz"),
ModesInput("start_mod", "Initial modulation"), ModesInput("start_mod", "Initial modulation"),
DropdownInput(
"tuning_step",
"Tuning step",
options=[Option(str(i), "{} Hz".format(i)) for i in [1, 100, 500, 1000, 2500, 3000, 5000, 6000, 10000, 12000, 50000]],
converter=IntConverter(),
),
NumberInput("initial_squelch_level", "Initial squelch level", append="dBFS"), NumberInput("initial_squelch_level", "Initial squelch level", append="dBFS"),
] ]
@ -592,7 +599,7 @@ class SdrDeviceDescription(object):
return keys return keys
def getProfileMandatoryKeys(self): def getProfileMandatoryKeys(self):
return ["name", "center_freq", "samp_rate", "start_freq", "start_mod"] return ["name", "center_freq", "samp_rate", "start_freq", "start_mod", "tuning_step"]
def getProfileOptionalKeys(self): def getProfileOptionalKeys(self):
return ["initial_squelch_level", "rf_gain", "lfo_offset", "waterfall_levels"] return ["initial_squelch_level", "rf_gain", "lfo_offset", "waterfall_levels"]

View File

@ -1,6 +1,7 @@
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
from owrx.form.input import Input, CheckboxInput, DropdownInput, DropdownEnum from owrx.form.input import Input, CheckboxInput, DropdownInput, NumberInput, DropdownEnum
from owrx.form.input.device import BiasTeeInput from owrx.form.input.device import BiasTeeInput, GainInput
from owrx.form.input.validator import RangeValidator
from typing import List from typing import List
@ -14,6 +15,8 @@ class SdrplaySource(SoapyConnectorSource):
"dab_notch": "dabnotch_ctrl", "dab_notch": "dabnotch_ctrl",
"if_mode": "if_mode", "if_mode": "if_mode",
"external_reference": "extref_ctrl", "external_reference": "extref_ctrl",
"rfgain_sel": "rfgain_sel",
"agc_setpoint": "agc_setpoint",
} }
) )
return mappings return mappings
@ -36,9 +39,6 @@ class SdrplayDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self): def getName(self):
return "SDRPlay device (RSP1, RSP2, RSPDuo, RSPDx)" return "SDRPlay device (RSP1, RSP2, RSPDuo, RSPDx)"
def getGainStages(self):
return ["RFGR", "IFGR"]
def getInputs(self) -> List[Input]: def getInputs(self) -> List[Input]:
return super().getInputs() + [ return super().getInputs() + [
BiasTeeInput(), BiasTeeInput(),
@ -55,10 +55,26 @@ class SdrplayDeviceDescription(SoapyConnectorDeviceDescription):
"IF Mode", "IF Mode",
IfModeOptions, IfModeOptions,
), ),
NumberInput(
"rfgain_sel",
"RF gain reduction",
validator=RangeValidator(0, 32),
),
NumberInput(
"agc_setpoint",
"AGC setpoint",
append="dBFS",
validator=RangeValidator(-60, 0),
),
GainInput(
"rf_gain",
"IF gain reduction",
has_agc=self.hasAgc(),
),
] ]
def getDeviceOptionalKeys(self): def getDeviceOptionalKeys(self):
return super().getDeviceOptionalKeys() + ["bias_tee", "rf_notch", "dab_notch", "if_mode"] return super().getDeviceOptionalKeys() + ["bias_tee", "rf_notch", "dab_notch", "if_mode", "rfgain_sel", "agc_setpoint"]
def getProfileOptionalKeys(self): def getProfileOptionalKeys(self):
return super().getProfileOptionalKeys() + ["bias_tee", "rf_notch", "dab_notch", "if_mode"] return super().getProfileOptionalKeys() + ["bias_tee", "rf_notch", "dab_notch", "if_mode", "rfgain_sel", "agc_setpoint"]