From ddac30db638a9450b7c4ab114e9cf341d7782321 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 15 Oct 2016 14:30:30 +0000 Subject: [PATCH 0001/2616] add integrations for dmr, d-star and nxdn via dsd --- htdocs/index.wrx | 3 +++ htdocs/openwebrx.css | 2 +- htdocs/openwebrx.js | 5 +++++ openwebrx.py | 10 +++++++--- plugins/dsp/csdr/plugin.py | 27 +++++++++++++++++++++++++-- 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/htdocs/index.wrx b/htdocs/index.wrx index 2debd546c..81441092c 100644 --- a/htdocs/index.wrx +++ b/htdocs/index.wrx @@ -88,6 +88,9 @@
LSB
USB
CW
+
DMR
+
DStar
+
NXDN
diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index a11714335..bf09036fd 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -560,7 +560,7 @@ input[type=range]:focus::-ms-fill-upper .openwebrx-demodulator-button { - width: 35px; + width: 50px; height: 19px; font-size: 12pt; text-align: center; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 7c9ab968d..ed170ce5d 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -429,6 +429,11 @@ function demodulator_default_analog(offset_frequency,subtype) this.low_cut=-4000; this.high_cut=4000; } + else if(subtype=="dmr" || subtype=="dstar" || subtype=="nxdn") + { + this.low_cut=-6500; + this.high_cut=6500; + } else if(subtype=="am") { this.low_cut=-4000; diff --git a/openwebrx.py b/openwebrx.py index 45df6739e..088130129 100755 --- a/openwebrx.py +++ b/openwebrx.py @@ -469,9 +469,13 @@ def do_GET(self): # ========= send audio ========= if dsp_initialized: myclient.loopstat=10 - temp_audio_data=dsp.read(256) - myclient.loopstat=11 - rxws.send(self, temp_audio_data, "AUD ") + temp_audio_data=dsp.read_async(256) + if (temp_audio_data is not None): + myclient.loopstat=11 + rxws.send(self, temp_audio_data, "AUD ") + else: + #time.sleep((256.0 * 32) / 11025) + time.sleep(.01) # ========= send spectrum ========= while not myclient.spectrum_queue.empty(): diff --git a/plugins/dsp/csdr/plugin.py b/plugins/dsp/csdr/plugin.py index be31bfa05..ea39cddad 100644 --- a/plugins/dsp/csdr/plugin.py +++ b/plugins/dsp/csdr/plugin.py @@ -26,6 +26,7 @@ import code import signal import fcntl +import select class dsp_plugin: @@ -53,6 +54,10 @@ def __init__(self): self.squelch_level = 0 def chain(self,which): + if which in [ "dmr", "dstar", "nxdn" ]: + self.set_output_rate(48000) + else: + self.set_output_rate(11025) any_chain_base="ncat -v 127.0.0.1 {nc_port} | " if self.csdr_dynamic_bufsize: any_chain_base+="csdr setbuf {start_bufsize} | " if self.csdr_through: any_chain_base+="csdr through | " @@ -67,7 +72,19 @@ def chain(self,which): chain_end = "" if self.audio_compression=="adpcm": chain_end = " | csdr encode_ima_adpcm_i16_u8" - if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr fastagc_ff 1024 | csdr convert_f_s16"+chain_end + if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr convert_f_s16"+chain_end + if which in [ "dmr", "dstar", "nxdn" ]: + c = chain_begin + c += "csdr fmdemod_quadri_cf | csdr convert_f_s16" + if which == "dmr": + c += " | dsd -i - -o - -mg -fr -u 1" + elif which == "dstar": + c += " | dsd -i - -o - -fd" + elif which == "nxdn": + c += " | dsd -i - -o -" + c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 256 - -t raw -r 11025 -e signed-integer -b 16 -c 1 -" + c += chain_end + return c elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end @@ -85,7 +102,7 @@ def set_samp_rate(self,samp_rate): #to change this, restart is required self.samp_rate=samp_rate self.decimation=1 - while self.samp_rate/(self.decimation+1)>self.output_rate: + while self.samp_rate/(self.decimation+1)>=self.output_rate: self.decimation+=1 self.last_decimation=float(self.if_samp_rate())/self.output_rate @@ -211,6 +228,12 @@ def start(self): def read(self,size): return self.process.stdout.read(size) + def read_async(self, size): + if (select.select([self.process.stdout], [], [], 0)[0] != []): + return self.process.stdout.read(size) + else: + return None + def stop(self): os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) #if(self.process.poll()!=None):return # returns None while subprocess is running From 0a389256eb2c38322063b23be8d3593dc9d70731 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 16 Oct 2016 19:40:03 +0000 Subject: [PATCH 0002/2616] mute audio when buffer is empty --- htdocs/openwebrx.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index ed170ce5d..38138a123 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1409,9 +1409,21 @@ if (!AudioBuffer.prototype.copyToChannel) function audio_onprocess(e) { //console.log("audio onprocess"); - if(audio_buffering) return; - if(audio_prepared_buffers.length==0) { audio_buffer_progressbar_update(); /*add_problem("audio underrun");*/ audio_buffering=true; } - else { e.outputBuffer.copyToChannel(audio_prepared_buffers.shift(),0); } + if(audio_buffering) { + var silence = new Float32Array(4096); + e.outputBuffer.copyToChannel(silence, 0); + return; + } + if(audio_prepared_buffers.length==0) { + audio_buffer_progressbar_update(); + /*add_problem("audio underrun");*/ + audio_buffering=true; + var silence = new Float32Array(4096); + e.outputBuffer.copyToChannel(silence, 0); + } else { + var buf = audio_prepared_buffers.shift(); + e.outputBuffer.copyToChannel(buf,0); + } } var audio_buffer_progressbar_update_disabled=false; From 9569fbd72e9ae7634a87d6eee741a73d2c153399 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 22 Oct 2016 21:51:51 +0000 Subject: [PATCH 0003/2616] narrower filter for dstar & nxdn --- htdocs/openwebrx.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 38138a123..c4479d72c 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -429,11 +429,16 @@ function demodulator_default_analog(offset_frequency,subtype) this.low_cut=-4000; this.high_cut=4000; } - else if(subtype=="dmr" || subtype=="dstar" || subtype=="nxdn") + else if(subtype=="dmr") { this.low_cut=-6500; this.high_cut=6500; } + else if(subtype=="dstar" || subtype=="nxdn") + { + this.low_cut=-3250; + this.high_cut=3250; + } else if(subtype=="am") { this.low_cut=-4000; From fba07c521ac561542ab560251a01d3b76bf05862 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 22 Oct 2016 21:52:22 +0000 Subject: [PATCH 0004/2616] refactor dsd parametrization --- plugins/dsp/csdr/plugin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/dsp/csdr/plugin.py b/plugins/dsp/csdr/plugin.py index ea39cddad..48c853e67 100644 --- a/plugins/dsp/csdr/plugin.py +++ b/plugins/dsp/csdr/plugin.py @@ -77,12 +77,13 @@ def chain(self,which): c = chain_begin c += "csdr fmdemod_quadri_cf | csdr convert_f_s16" if which == "dmr": - c += " | dsd -i - -o - -mg -fr -u 1" + c += " | dsd -mg -fr" elif which == "dstar": - c += " | dsd -i - -o - -fd" + c += " | dsd -fd" elif which == "nxdn": - c += " | dsd -i - -o -" - c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 256 - -t raw -r 11025 -e signed-integer -b 16 -c 1 -" + c += " | dsd -fi" + c += " -i - -o - -u 2 -g 10" + c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256" c += chain_end return c elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end From 49e3bd3b80bf4721a372bbd4fdb2eda5b939f0fb Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 29 Oct 2016 19:43:18 +0000 Subject: [PATCH 0005/2616] remove old canvases from the dom to reduce memory footprint --- htdocs/openwebrx.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index c4479d72c..5351e7236 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1724,6 +1724,11 @@ function add_canvas() new_canvas.addEventListener("mousedown", canvas_mousedown, false); new_canvas.addEventListener("wheel",canvas_mousewheel, false); canvases.push(new_canvas); + while (canvas_container && canvas_container.clientHeight + canvas_default_height * 2 < canvases.length * canvas_default_height) { + var c = canvases.shift(); + if (!c) break; + canvas_container.removeChild(c); + } } function init_canvas_container() From 7700214e5ff97d1c503e97f316fa644871be685d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 11 Nov 2016 20:56:17 +0000 Subject: [PATCH 0006/2616] add metadata pipe to allow digital protocol information to be displayed in the website --- htdocs/index.wrx | 6 ++++-- htdocs/openwebrx.js | 11 ++++++++++- openwebrx.py | 11 +++++++++++ plugins/dsp/csdr/plugin.py | 19 +++++++++++++++++-- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/htdocs/index.wrx b/htdocs/index.wrx index 81441092c..87a03ddd3 100644 --- a/htdocs/index.wrx +++ b/htdocs/index.wrx @@ -78,7 +78,7 @@
-
+
---.--- MHz
---.--- MHz
@@ -117,7 +117,7 @@
-
+
OpenWebRX client log
Author: András Retzler, HA7ILM
You can support OpenWebRX development via PayPal!
@@ -132,6 +132,8 @@
Server CPU [0%]
Clients [1]
+
+
Under construction
We're working on the code right now, so the application might fail. diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 5351e7236..deb308279 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1221,7 +1221,16 @@ function on_ws_recv(evt) { divlog("Received invalid message over WebSocket."); }*/ - } + } else if (firstChars=='MET') + { + var stringData=arrayBufferToString(evt.data); + var metaPanels = Array.prototype.filter.call(document.getElementsByClassName('openwebrx-panel'), function(el) { + return el.dataset.panelName == 'metadata'; + }); + metaPanels.forEach(function(el) { + el.innerHTML = stringData; + }); + } } diff --git a/openwebrx.py b/openwebrx.py index 088130129..066849ec1 100755 --- a/openwebrx.py +++ b/openwebrx.py @@ -499,6 +499,17 @@ def do_GET(self): if smeter_level!=None: myclient.loopstat=31 rxws.send(self, "MSG s={0}".format(smeter_level)) + + # ========= send metadata ========= + metadata = None + while True: + try: + myclient.loopstat=35 + metadata = dsp.get_metadata(); + if metadata == None: break + rxws.send(self, "MET {0}".format(metadata)) + except: + break # ========= send bcastmsg ========= if myclient.bcastmsg!="": diff --git a/plugins/dsp/csdr/plugin.py b/plugins/dsp/csdr/plugin.py index 48c853e67..bb1fbfb3e 100644 --- a/plugins/dsp/csdr/plugin.py +++ b/plugins/dsp/csdr/plugin.py @@ -168,6 +168,10 @@ def get_smeter_level(self): line=self.smeter_pipe_file.readline() return float(line[:-1]) + def get_metadata(self): + if self.running and self.meta_pipe: + return self.meta_pipe_file.readline() + def mkfifo(self,path): try: os.unlink(path) @@ -183,7 +187,7 @@ def start(self): #create control pipes for csdr pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self)) - self.bpf_pipe = self.shift_pipe = self.squelch_pipe = self.smeter_pipe = None + self.bpf_pipe = self.shift_pipe = self.squelch_pipe = self.smeter_pipe = self.meta_pipe = None if "{bpf_pipe}" in command_base: self.bpf_pipe=pipe_base_path+"bpf" self.mkfifo(self.bpf_pipe) @@ -196,13 +200,18 @@ def start(self): if "{smeter_pipe}" in command_base: self.smeter_pipe=pipe_base_path+"smeter" self.mkfifo(self.smeter_pipe) + if "{meta_pipe}" in command_base: + self.meta_pipe=pipe_base_path+"meta" + self.mkfifo(self.meta_pipe) + else: + self.meta_pipe=None #run the command command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, \ last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), \ bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), \ flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, \ - squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe ) + squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe ) print "[openwebrx-dsp-plugin:csdr] Command =",command #code.interact(local=locals()) @@ -225,6 +234,9 @@ def start(self): if self.smeter_pipe != None: self.smeter_pipe_file=open(self.smeter_pipe,"r") fcntl.fcntl(self.smeter_pipe_file, fcntl.F_SETFL, os.O_NONBLOCK) + if self.meta_pipe != None: + self.meta_pipe_file=open(self.meta_pipe,"r") + fcntl.fcntl(self.meta_pipe_file, fcntl.F_SETFL, os.O_NONBLOCK) def read(self,size): return self.process.stdout.read(size) @@ -256,6 +268,9 @@ def stop(self): if self.smeter_pipe: try: os.unlink(self.smeter_pipe) except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.smeter_pipe + if self.meta_pipe: + try: os.unlink(self.meta_pipe) + except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.meta_pipe self.running = False def restart(self): From 95acf40eb6c93436ddc22d13553e417dc4bda5df Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 11 Nov 2016 21:42:45 +0000 Subject: [PATCH 0007/2616] more effort displaying meta information --- htdocs/openwebrx.js | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index deb308279..7bb39cc92 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1227,9 +1227,37 @@ function on_ws_recv(evt) var metaPanels = Array.prototype.filter.call(document.getElementsByClassName('openwebrx-panel'), function(el) { return el.dataset.panelName == 'metadata'; }); - metaPanels.forEach(function(el) { - el.innerHTML = stringData; + + var meta = {}; + stringData.substr(4).split(";").forEach(function(s) { + var item = s.split(":"); + meta[item[0]] = item[1]; }); + + var update = function(el) { + el.innerHTML = ""; + } + if (meta.protocol) switch (meta.protocol) { + case 'DMR': + if (meta.slot) { + var html = 'Timeslot: ' + meta.slot; + if (meta.type) html += ' Typ: ' + meta.type; + if (meta.source && meta.target) html += ' Source: ' + meta.source + ' Target: ' + meta.target; + update = function(el) { + var slotEl = el.getElementsByClassName('slot-' + meta.slot); + if (!slotEl.length) { + slotEl = document.createElement('div'); + slotEl.class = 'slot-' + meta.slot; + el.appendChild(slotEl); + } else { + slotEl = SlotEl[0]; + } + slotEl.innerHTML = html; + }; + } + } + + metaPanels.forEach(update); } } From 6d5a7ffefcb22bb86abb1a6ec5788ac77e5fd4f3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 12 Nov 2016 19:55:43 +0000 Subject: [PATCH 0008/2616] fix javascript errors --- htdocs/openwebrx.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 7bb39cc92..5352332bf 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1247,10 +1247,10 @@ function on_ws_recv(evt) var slotEl = el.getElementsByClassName('slot-' + meta.slot); if (!slotEl.length) { slotEl = document.createElement('div'); - slotEl.class = 'slot-' + meta.slot; + slotEl.className = 'slot-' + meta.slot; el.appendChild(slotEl); } else { - slotEl = SlotEl[0]; + slotEl = slotEl[0]; } slotEl.innerHTML = html; }; From aa959cdc932c9dd8957363896c57b628816a8491 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 15 Nov 2016 19:20:18 +0000 Subject: [PATCH 0009/2616] strip newlines from metadata --- openwebrx.py | 2 +- plugins/dsp/csdr/plugin.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/openwebrx.py b/openwebrx.py index 066849ec1..48eff4b74 100755 --- a/openwebrx.py +++ b/openwebrx.py @@ -507,7 +507,7 @@ def do_GET(self): myclient.loopstat=35 metadata = dsp.get_metadata(); if metadata == None: break - rxws.send(self, "MET {0}".format(metadata)) + rxws.send(self, "MET {0}".format(metadata.rstrip("\n"))) except: break diff --git a/plugins/dsp/csdr/plugin.py b/plugins/dsp/csdr/plugin.py index bb1fbfb3e..470bb4cfb 100644 --- a/plugins/dsp/csdr/plugin.py +++ b/plugins/dsp/csdr/plugin.py @@ -73,16 +73,21 @@ def chain(self,which): if self.audio_compression=="adpcm": chain_end = " | csdr encode_ima_adpcm_i16_u8" if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr convert_f_s16"+chain_end - if which in [ "dmr", "dstar", "nxdn" ]: + if which in [ "dstar", "nxdn" ]: c = chain_begin c += "csdr fmdemod_quadri_cf | csdr convert_f_s16" - if which == "dmr": - c += " | dsd -mg -fr" - elif which == "dstar": + if which == "dstar": c += " | dsd -fd" elif which == "nxdn": c += " | dsd -fi" c += " -i - -o - -u 2 -g 10" + c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --input-buffer 160 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 220" + c += chain_end + return c + elif which == "dmr": + c = chain_begin + c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" + c += " | rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer" c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256" c += chain_end return c From 89740b1a93699bb178615c2659651c66834b6477 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 27 Nov 2016 01:29:17 +0000 Subject: [PATCH 0010/2616] add ysf to the receiver --- htdocs/index.wrx | 1 + htdocs/openwebrx.js | 2 +- plugins/dsp/csdr/plugin.py | 9 ++++++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/htdocs/index.wrx b/htdocs/index.wrx index 87a03ddd3..ae9d4b9fa 100644 --- a/htdocs/index.wrx +++ b/htdocs/index.wrx @@ -91,6 +91,7 @@
DMR
DStar
NXDN
+
YSF
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 5352332bf..07459a5bc 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -429,7 +429,7 @@ function demodulator_default_analog(offset_frequency,subtype) this.low_cut=-4000; this.high_cut=4000; } - else if(subtype=="dmr") + else if(subtype=="dmr" || subtype=="ysf") { this.low_cut=-6500; this.high_cut=6500; diff --git a/plugins/dsp/csdr/plugin.py b/plugins/dsp/csdr/plugin.py index 470bb4cfb..04b43f2a0 100644 --- a/plugins/dsp/csdr/plugin.py +++ b/plugins/dsp/csdr/plugin.py @@ -54,7 +54,7 @@ def __init__(self): self.squelch_level = 0 def chain(self,which): - if which in [ "dmr", "dstar", "nxdn" ]: + if which in [ "dmr", "dstar", "nxdn", "ysf" ]: self.set_output_rate(48000) else: self.set_output_rate(11025) @@ -91,6 +91,13 @@ def chain(self,which): c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256" c += chain_end return c + elif which == "ysf": + c = chain_begin + c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" + c += " | rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer" + c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256" + c += chain_end + return c elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end From ac6e001fd65e221aaf88513f9c3c1b488c37d882 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 20 Jan 2017 12:26:09 +0000 Subject: [PATCH 0011/2616] metadata for ysf --- htdocs/openwebrx.js | 12 ++++++++++++ plugins/dsp/csdr/plugin.py | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 07459a5bc..9caa4d172 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1255,6 +1255,18 @@ function on_ws_recv(evt) slotEl.innerHTML = html; }; } + break; + case 'YSF': + var strings = []; + if (meta.source) strings.push("Source: " + meta.source); + if (meta.target) strings.push("Destination: " + meta.target); + if (meta.up) strings.push("Up: " + meta.up); + if (meta.down) strings.push("Down: " + meta.down); + var html = strings.join(' '); + update = function(el) { + el.innerHTML = html; + } + break; } metaPanels.forEach(update); diff --git a/plugins/dsp/csdr/plugin.py b/plugins/dsp/csdr/plugin.py index 04b43f2a0..ffabf3bea 100644 --- a/plugins/dsp/csdr/plugin.py +++ b/plugins/dsp/csdr/plugin.py @@ -75,7 +75,7 @@ def chain(self,which): if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr convert_f_s16"+chain_end if which in [ "dstar", "nxdn" ]: c = chain_begin - c += "csdr fmdemod_quadri_cf | csdr convert_f_s16" + c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" if which == "dstar": c += " | dsd -fd" elif which == "nxdn": @@ -94,7 +94,7 @@ def chain(self,which): elif which == "ysf": c = chain_begin c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" - c += " | rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer" + c += " | rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y" c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256" c += chain_end return c From bf4c70dfef8a2cb416dff70b0f2d983104119794 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 25 Sep 2018 14:56:47 +0200 Subject: [PATCH 0012/2616] merge recent openwebrx changes into our work --- .gitignore | 2 + README.md | 35 +- config_webrx.py | 101 ++- csdr.py | 453 ++++++++++ htdocs/gfx/openwebrx-3d-spectrum.png | Bin 0 -> 10110 bytes htdocs/index.wrx | 294 ++++--- htdocs/jquery-3.2.1.min.js | 4 + htdocs/jquery.nanoscroller.js | 1000 +++++++++++++++++++++++ htdocs/mathbox-bundle.min.js | 33 + htdocs/mathbox.css | 461 +++++++++++ htdocs/nanoscroller.css | 55 ++ htdocs/openwebrx.css | 187 ++++- htdocs/openwebrx.js | 690 +++++++++++++++- openwebrx.py | 1135 ++++++++++++++------------ plugins/__init__.py | 0 plugins/dsp/__init__.py | 0 plugins/dsp/csdr/__init__.py | 0 plugins/dsp/csdr/plugin.py | 294 ------- rxws.py | 236 +++--- screenshot-sdrhu.png | Bin 523468 -> 0 bytes screenshot.png | Bin 1480850 -> 0 bytes sdrhu.py | 51 +- 22 files changed, 3840 insertions(+), 1191 deletions(-) create mode 100755 csdr.py create mode 100644 htdocs/gfx/openwebrx-3d-spectrum.png create mode 100644 htdocs/jquery-3.2.1.min.js create mode 100644 htdocs/jquery.nanoscroller.js create mode 100644 htdocs/mathbox-bundle.min.js create mode 100644 htdocs/mathbox.css create mode 100644 htdocs/nanoscroller.css delete mode 100644 plugins/__init__.py delete mode 100644 plugins/dsp/__init__.py delete mode 100644 plugins/dsp/csdr/__init__.py delete mode 100644 plugins/dsp/csdr/plugin.py delete mode 100644 screenshot-sdrhu.png delete mode 100644 screenshot.png diff --git a/.gitignore b/.gitignore index 0d20b6487..6a211b7e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ *.pyc +*.swp +tags diff --git a/README.md b/README.md index 9717f9db7..d308c36f7 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,26 @@ OpenWebRX ========= +[:floppy_disk: Setup guide for Ubuntu](http://blog.sdr.hu/2015/06/30/quick-setup-openwebrx.html) | [:blue_book: Knowledge base on the Wiki](https://github.com/simonyiszk/openwebrx/wiki/) | [:earth_americas: Receivers on SDR.hu](http://sdr.hu/) + OpenWebRX is a multi-user SDR receiver software with a web interface. -![OpenWebRX](/screenshot.png?raw=true) +![OpenWebRX](http://blog.sdr.hu/images/openwebrx/screenshot.png) It has the following features: -- libcsdr based demodulators (AM/FM/SSB), +- csdr based demodulators (AM/FM/SSB/CW/BPSK31), - filter passband can be set from GUI, - waterfall display can be shifted back in time, -- it extensively uses HTML5 features like WebSocket, Web Audio API, and <canvas>. +- it extensively uses HTML5 features like WebSocket, Web Audio API, and <canvas>, - it works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28), -- currently supports RTL-SDR and HackRF; other SDR hardware may be easily added. +- currently supports RTL-SDR, HackRF, SDRplay, AirSpy and many other devices, see the OpenWebRX Wiki, +- it has a 3D waterfall display: + +![OpenWebRX 3D waterfall](http://blog.sdr.hu/images/openwebrx/screenshot-3d.gif) **News (2015-08-18)** -- My BSc. thesis written on OpenWebRX is available here. +- My BSc. thesis written on OpenWebRX is available here. - Several bugs were fixed to improve reliability and stability. - OpenWebRX now supports compression of audio and waterfall stream, so the required network uplink bandwidth has been decreased from 2 Mbit/s to about 200 kbit/s per client! (Measured with the default settings. It is also dependent on `fft_size`.) - OpenWebRX now uses sdr.js (*libcsdr* compiled to JavaScript) for some client-side DSP tasks. @@ -28,13 +33,20 @@ It has the following features: - OpenWebRX now supports URLs like: `http://localhost:8073/#freq=145555000,mod=usb` - UI improvements were made, thanks to John Seamons and Gnoxter. -> When upgrading OpenWebRX, please make sure that you also upgrade *csdr*, and install the new dependency, *ncat*! +**News (2017-04-04)** +- *ncat* has been replaced with a custom implementation called *nmux* due to a bug that caused regular crashes on some machines. The *nmux* tool is part of the *csdr* package. +- Most consumer SDR devices are supported via rx_tools, see the OpenWebRX Wiki on that. + +**News (2017-07-12)** +- OpenWebRX now has a BPSK31 demodulator and a 3D waterfall display. + +> When upgrading OpenWebRX, please make sure that you also upgrade *csdr*! ## OpenWebRX servers on SDR.hu [SDR.hu](http://sdr.hu) is a site which lists the active, public OpenWebRX servers. Your receiver [can also be part of it](http://sdr.hu/openwebrx), if you want. -![sdr.hu](/screenshot-sdrhu.png?raw=true) +![sdr.hu](http://blog.sdr.hu/images/openwebrx/screenshot-sdrhu.png) ## Setup @@ -44,11 +56,6 @@ First you will need to install the dependencies: - libcsdr - rtl-sdr -- ncat (On Debian/Ubuntu, it is in the *nmap* package). - -> By the way, *nmap* is a tool commonly used for auditing network security, and it is not used by OpenWebRX in any way. We need to install it, because the *ncat* command is packaged with it. -> -> *ncat* is a better *netcat* alternative, which is used by OpenWebRX for internally distributing the I/Q data stream. It also solves the problem of having different versions of *netcat* on different Linux distributions, which are not compatible by their command-line arguments. After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server: @@ -62,7 +69,7 @@ Please note that the server is also listening on the following ports (on localho Now the next step is to customize the parameters of your server in `config_webrx.py`. -Actually, if you do something cool with OpenWebRX (or just have a problem), please drop me a mail: +Actually, if you do something cool with OpenWebRX, please drop me a mail: *Andras Retzler, HA7ILM <randras@sdr.hu>* ## Usage tips @@ -85,4 +92,4 @@ If you want to run OpenWebRX on a remote server instead of *localhost*, do not f OpenWebRX is available under Affero GPL v3 license (summary). -OpenWebRX is also available under a commercial license on request. Please contact me at the address *<randras@sdr.hu>* for other licensing options. +OpenWebRX is also available under a commercial license on request. Please contact me at the address *<randras@sdr.hu>* for licensing options. diff --git a/config_webrx.py b/config_webrx.py index ea693c141..34e480c6b 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -3,9 +3,9 @@ """ config_webrx: configuration options for OpenWebRX - This file is part of OpenWebRX, - an open-source SDR receiver software with a web UI. - Copyright (c) 2013-2015 by Andras Retzler + This file is part of OpenWebRX, + an open-source SDR receiver software with a web UI. + Copyright (c) 2013-2015 by Andras Retzler This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -20,15 +20,15 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - In addition, as a special exception, the copyright holders - state that config_rtl.py and config_webrx.py are not part of the - Corresponding Source defined in GNU AGPL version 3 section 1. + In addition, as a special exception, the copyright holders + state that config_rtl.py and config_webrx.py are not part of the + Corresponding Source defined in GNU AGPL version 3 section 1. - (It means that you do not have to redistribute config_rtl.py and - config_webrx.py if you make any changes to these two configuration files, - and use them for running your web service with OpenWebRX.) + (It means that you do not have to redistribute config_rtl.py and + config_webrx.py if you make any changes to these two configuration files, + and use them for running your web service with OpenWebRX.) """ # NOTE: you can find additional information about configuring OpenWebRX in the Wiki: @@ -67,34 +67,49 @@ sdrhu_public_listing = False # ==== DSP/RX settings ==== -dsp_plugin="csdr" fft_fps=9 -fft_size=4096 -samp_rate = 250000 +fft_size=4096 #Should be power of 2 +fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. -center_freq = 145525000 +# samp_rate = 250000 +samp_rate = 2400000 +center_freq = 144250000 rf_gain = 5 #in dB. For an RTL-SDR, rf_gain=0 will set the tuner to auto gain mode, else it will be in manual gain mode. ppm = 0 audio_compression="adpcm" #valid values: "adpcm", "none" fft_compression="adpcm" #valid values: "adpcm", "none" +digimodes_enable=True #Decoding digimodes come with higher CPU usage. +digimodes_fft_size=1024 + start_rtl_thread=True +""" +Note: if you experience audio underruns while CPU usage is 100%, you can: +- decrease `samp_rate`, +- set `fft_voverlap_factor` to 0, +- decrease `fft_fps` and `fft_size`, +- limit the number of users by decreasing `max_clients`. +""" + # ==== I/Q sources ==== # (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.) -# There are guides for setting may different SDR hardware including AirSpy, AFEDRI-SDR, RTL-SDR in direct sampling mode, etc. in the Wiki: -# https://github.com/simonyiszk/openwebrx/wiki +################################################################################################# +# Is my SDR hardware supported? # +# Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support # +################################################################################################# # You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... Some examples of configuration are available here (default is RTL-SDR): # >> RTL-SDR via rtl_sdr - start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) format_conversion="csdr convert_u8_f" -#start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l16 -a0 -q -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) +#lna_gain=8 +#rf_amp=1 +#start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm, rf_amp=rf_amp, lna_gain=lna_gain) #format_conversion="csdr convert_s8_f" """ To use a HackRF, compile the HackRF host tools from its "stdout" branch: @@ -117,20 +132,29 @@ #format_conversion="csdr convert_s16_f | csdr gain_ff 30" # >> /dev/urandom test signal source -#samp_rate = 2400000 -#start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate) -#format_conversion="csdr convert_u8_f" +# samp_rate = 2400000 +# start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate) +# format_conversion="csdr convert_u8_f" # >> Pre-recorded raw I/Q file as signal source # You will have to correctly specify: samp_rate, center_freq, format_conversion in order to correctly play an I/Q file. #start_rtl_command="(while true; do cat my_iq_file.raw; done) | csdr flowcontrol {sr} 20 ".format(sr=samp_rate*2*1.05) #format_conversion="csdr convert_u8_f" +#>> The rx_sdr command works with a variety of SDR harware: RTL-SDR, HackRF, SDRplay, UHD, Airspy, Red Pitaya, audio devices, etc. +# It will auto-detect your SDR hardware if the following tools are installed: +# * the vendor provided driver and library, +# * the vendor-specific SoapySDR wrapper library, +# * and SoapySDR itself. +# Check out this article on the OpenWebRX Wiki: https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX/ +#start_rtl_command="rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) +#format_conversion="" + # >> gr-osmosdr signal source using GNU Radio (follow this guide: https://github.com/simonyiszk/openwebrx/wiki/Using-GrOsmoSDR-as-signal-source) #start_rtl_command="cat /tmp/osmocom_fifo" #format_conversion="" -# ==== Misc options ==== +# ==== Misc settings ==== shown_center_freq = center_freq #you can change this if you use an upconverter @@ -146,16 +170,43 @@ #access_log = "~/openwebrx_access.log" -waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" -waterfall_min_level = -115 #in dB -waterfall_max_level = 0 +# ==== Color themes ==== + #A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels +### default theme by teejez: +waterfall_colors = "[0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]" +waterfall_min_level = -88 #in dB +waterfall_max_level = -20 +waterfall_auto_level_margin = (5, 40) +### old theme by HA7ILM: +#waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" +#waterfall_min_level = -115 #in dB +#waterfall_max_level = 0 +#waterfall_auto_level_margin = (20, 30) +##For the old colors, you might also want to set [fft_voverlap_factor] to 0. + +#Note: When the auto waterfall level button is clicked, the following happens: +# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]] +# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]] +# +# ___|____________________________________|____________________________________|____________________________________|___> signal power +# \_waterfall_auto_level_margin[0]_/ |__ current_min_power_level | \_waterfall_auto_level_margin[1]_/ +# current_max_power_level __| + +# 3D view settings +mathbox_waterfall_frequency_resolution = 128 #bins +mathbox_waterfall_history_length = 10 #seconds +mathbox_waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" + +# === Experimental settings === #Warning! The settings below are very experimental. csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr. csdr_print_bufsizes = False # This prints the buffer sizes used for csdr processes. csdr_through = False # Setting this True will print out how much data is going into the DSP chains. +nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux. + #Look up external IP address automatically from icanhazip.com, and use it as [server_hostname] """ print "[openwebrx-config] Detecting external IP address..." diff --git a/csdr.py b/csdr.py new file mode 100755 index 000000000..f83ccb0d5 --- /dev/null +++ b/csdr.py @@ -0,0 +1,453 @@ +""" +OpenWebRX csdr plugin: do the signal processing with csdr + + This file is part of OpenWebRX, + an open-source SDR receiver software with a web UI. + Copyright (c) 2013-2015 by Andras Retzler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +""" + +import subprocess +import time +import os +import code +import signal +import fcntl + +class dsp: + + def __init__(self): + self.samp_rate = 250000 + self.output_rate = 11025 #this is default, and cannot be set at the moment + self.fft_size = 1024 + self.fft_fps = 5 + self.offset_freq = 0 + self.low_cut = -4000 + self.high_cut = 4000 + self.bpf_transition_bw = 320 #Hz, and this is a constant + self.ddc_transition_bw_rate = 0.15 # of the IF sample rate + self.running = False + self.secondary_processes_running = False + self.audio_compression = "none" + self.fft_compression = "none" + self.demodulator = "nfm" + self.name = "csdr" + self.format_conversion = "csdr convert_u8_f" + self.base_bufsize = 512 + self.nc_port = 4951 + self.csdr_dynamic_bufsize = False + self.csdr_print_bufsizes = False + self.csdr_through = False + self.squelch_level = 0 + self.fft_averages = 50 + self.iqtee = False + self.iqtee2 = False + self.secondary_demodulator = None + self.secondary_fft_size = 1024 + self.secondary_process_fft = None + self.secondary_process_demod = None + self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "iqtee_pipe", "iqtee2_pipe"] + self.secondary_pipe_names=["secondary_shift_pipe"] + self.secondary_offset_freq = 1000 + + def chain(self,which): + if which in [ "dmr", "dstar", "nxdn", "ysf" ]: + self.set_output_rate(48000) + else: + self.set_output_rate(11025) + any_chain_base="nc -v 127.0.0.1 {nc_port} | " + if self.csdr_dynamic_bufsize: any_chain_base+="csdr setbuf {start_bufsize} | " + if self.csdr_through: any_chain_base+="csdr through | " + any_chain_base+=self.format_conversion+(" | " if self.format_conversion!="" else "") ##"csdr flowcontrol {flowcontrol} auto 1.5 10 | " + if which == "fft": + fft_chain_base = any_chain_base+"csdr fft_cc {fft_size} {fft_block_size} | " + \ + ("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \ + "csdr fft_exchange_sides_ff {fft_size}" + if self.fft_compression=="adpcm": + return fft_chain_base+" | csdr compress_fft_adpcm_f_u8 {fft_size}" + else: + return fft_chain_base + chain_begin=any_chain_base+"csdr shift_addition_cc --fifo {shift_pipe} | csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | " + if self.secondary_demodulator: + chain_begin+="csdr tee {iqtee_pipe} | " + chain_begin+="csdr tee {iqtee2_pipe} | " + chain_end = "" + if self.audio_compression=="adpcm": + chain_end = " | csdr encode_ima_adpcm_i16_u8" + if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr convert_f_s16"+chain_end + if which in [ "dstar", "nxdn" ]: + c = chain_begin + c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" + if which == "dstar": + c += " | dsd -fd" + elif which == "nxdn": + c += " | dsd -fi" + c += " -i - -o - -u 2 -g 10" + c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --input-buffer 160 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 220" + c += chain_end + return c + elif which == "dmr": + c = chain_begin + c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" + c += " | rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer" + c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256" + c += chain_end + return c + elif which == "ysf": + c = chain_begin + c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" + c += " | rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y" + c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256" + c += chain_end + return c + elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end + elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end + + def secondary_chain(self, which): + secondary_chain_base="cat {input_pipe} | " + if which == "fft": + return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "") + elif which == "bpsk31": + return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \ + "csdr bandpass_fir_fft_cc $(csdr '=-(31.25)/{if_samp_rate}') $(csdr '=(31.25)/{if_samp_rate}') $(csdr '=31.25/{if_samp_rate}') | " + \ + "csdr simple_agc_cc 0.001 0.5 | " + \ + "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ + "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ + "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" + + def set_secondary_demodulator(self, what): + self.secondary_demodulator = what + + def secondary_fft_block_size(self): + return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here + + def secondary_decimation(self): + return 1 #currently unused + + def secondary_bpf_cutoff(self): + if self.secondary_demodulator == "bpsk31": + return (31.25/2) / self.if_samp_rate() + return 0 + + def secondary_bpf_transition_bw(self): + if self.secondary_demodulator == "bpsk31": + return (31.25/2) / self.if_samp_rate() + return 0 + + def secondary_samples_per_bits(self): + if self.secondary_demodulator == "bpsk31": + return int(round(self.if_samp_rate()/31.25))&~3 + return 0 + + def secondary_bw(self): + if self.secondary_demodulator == "bpsk31": + return 31.25 + + def start_secondary_demodulator(self): + if(not self.secondary_demodulator): return + print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate() + secondary_command_fft=self.secondary_chain("fft") + secondary_command_demod=self.secondary_chain(self.secondary_demodulator) + self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft) + + secondary_command_fft=secondary_command_fft.format( \ + input_pipe=self.iqtee_pipe, \ + secondary_fft_input_size=self.secondary_fft_size, \ + secondary_fft_size=self.secondary_fft_size, \ + secondary_fft_block_size=self.secondary_fft_block_size(), \ + ) + secondary_command_demod=secondary_command_demod.format( \ + input_pipe=self.iqtee2_pipe, \ + secondary_shift_pipe=self.secondary_shift_pipe, \ + secondary_decimation=self.secondary_decimation(), \ + secondary_samples_per_bits=self.secondary_samples_per_bits(), \ + secondary_bpf_cutoff=self.secondary_bpf_cutoff(), \ + secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), \ + if_samp_rate=self.if_samp_rate() + ) + + print "[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft + print "[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod + #code.interact(local=locals()) + my_env=os.environ.copy() + #if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; + if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; + self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) + print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)" + self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes + print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)" #TODO digimodes + self.secondary_processes_running = True + + #open control pipes for csdr and send initialization data + # print "==========> 1" + if self.secondary_shift_pipe != None: #TODO digimodes + # print "==========> 2", self.secondary_shift_pipe + self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes + # print "==========> 3" + self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes + # print "==========> 4" + + self.set_pipe_nonblocking(self.secondary_process_demod.stdout) + self.set_pipe_nonblocking(self.secondary_process_fft.stdout) + + def set_secondary_offset_freq(self, value): + self.secondary_offset_freq=value + if self.secondary_processes_running: + self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate())) + self.secondary_shift_pipe_file.flush() + + def stop_secondary_demodulator(self): + if self.secondary_processes_running == False: return + self.try_delete_pipes(self.secondary_pipe_names) + if self.secondary_process_fft: os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM) + if self.secondary_process_demod: os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM) + self.secondary_processes_running = False + + def read_secondary_demod(self, size): + return self.secondary_process_demod.stdout.read(size) + + def read_secondary_fft(self, size): + return self.secondary_process_fft.stdout.read(size) + + def get_secondary_demodulator(self): + return self.secondary_demodulator + + def set_secondary_fft_size(self,secondary_fft_size): + #to change this, restart is required + self.secondary_fft_size=secondary_fft_size + + def set_audio_compression(self,what): + self.audio_compression = what + + def set_fft_compression(self,what): + self.fft_compression = what + + def get_fft_bytes_to_read(self): + if self.fft_compression=="none": return self.fft_size*4 + if self.fft_compression=="adpcm": return (self.fft_size/2)+(10/2) + + def get_secondary_fft_bytes_to_read(self): + if self.fft_compression=="none": return self.secondary_fft_size*4 + if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2) + + def set_samp_rate(self,samp_rate): + #to change this, restart is required + self.samp_rate=samp_rate + self.decimation=1 + while self.samp_rate/(self.decimation+1)>self.output_rate: + self.decimation+=1 + self.last_decimation=float(self.if_samp_rate())/self.output_rate + + def if_samp_rate(self): + return self.samp_rate/self.decimation + + def get_name(self): + return self.name + + def get_output_rate(self): + return self.output_rate + + def set_output_rate(self,output_rate): + self.output_rate=output_rate + self.set_samp_rate(self.samp_rate) #as it depends on output_rate + + def set_demodulator(self,demodulator): + #to change this, restart is required + self.demodulator=demodulator + + def get_demodulator(self): + return self.demodulator + + def set_fft_size(self,fft_size): + #to change this, restart is required + self.fft_size=fft_size + + def set_fft_fps(self,fft_fps): + #to change this, restart is required + self.fft_fps=fft_fps + + def set_fft_averages(self,fft_averages): + #to change this, restart is required + self.fft_averages=fft_averages + + def fft_block_size(self): + if self.fft_averages == 0: return self.samp_rate/self.fft_fps + else: return self.samp_rate/self.fft_fps/self.fft_averages + + def set_format_conversion(self,format_conversion): + self.format_conversion=format_conversion + + def set_offset_freq(self,offset_freq): + self.offset_freq=offset_freq + if self.running: + self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate)) + self.shift_pipe_file.flush() + + def set_bpf(self,low_cut,high_cut): + self.low_cut=low_cut + self.high_cut=high_cut + if self.running: + self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) ) + self.bpf_pipe_file.flush() + + def get_bpf(self): + return [self.low_cut, self.high_cut] + + def set_squelch_level(self, squelch_level): + self.squelch_level=squelch_level + if self.running: + self.squelch_pipe_file.write( "%g\n"%(float(self.squelch_level)) ) + self.squelch_pipe_file.flush() + + def get_smeter_level(self): + if self.running: + line=self.smeter_pipe_file.readline() + return float(line[:-1]) + + def mkfifo(self,path): + try: + os.unlink(path) + except: + pass + os.mkfifo(path) + + def ddc_transition_bw(self): + return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate)) + + def try_create_pipes(self, pipe_names, command_base): + # print "try_create_pipes" + for pipe_name in pipe_names: + # print "\t"+pipe_name + if "{"+pipe_name+"}" in command_base: + setattr(self, pipe_name, self.pipe_base_path+pipe_name) + self.mkfifo(getattr(self, pipe_name)) + else: + setattr(self, pipe_name, None) + + def try_delete_pipes(self, pipe_names): + for pipe_name in pipe_names: + pipe_path = getattr(self,pipe_name,None) + if pipe_path: + try: os.unlink(pipe_path) + except Exception as e: print "[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e + + def set_pipe_nonblocking(self, pipe): + flags = fcntl.fcntl(pipe, fcntl.F_GETFL) + fcntl.fcntl(pipe, fcntl.F_SETFL, flags | os.O_NONBLOCK) + + def start(self): + command_base=self.chain(self.demodulator) + + #create control pipes for csdr + self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self)) + # self.bpf_pipe = self.shift_pipe = self.squelch_pipe = self.smeter_pipe = None + + self.try_create_pipes(self.pipe_names, command_base) + + # if "{bpf_pipe}" in command_base: + # self.bpf_pipe=pipe_base_path+"bpf" + # self.mkfifo(self.bpf_pipe) + # if "{shift_pipe}" in command_base: + # self.shift_pipe=pipe_base_path+"shift" + # self.mkfifo(self.shift_pipe) + # if "{squelch_pipe}" in command_base: + # self.squelch_pipe=pipe_base_path+"squelch" + # self.mkfifo(self.squelch_pipe) + # if "{smeter_pipe}" in command_base: + # self.smeter_pipe=pipe_base_path+"smeter" + # self.mkfifo(self.smeter_pipe) + # if "{iqtee_pipe}" in command_base: + # self.iqtee_pipe=pipe_base_path+"iqtee" + # self.mkfifo(self.iqtee_pipe) + # if "{iqtee2_pipe}" in command_base: + # self.iqtee2_pipe=pipe_base_path+"iqtee2" + # self.mkfifo(self.iqtee2_pipe) + + #run the command + command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, \ + last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, \ + bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), \ + flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, \ + squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe ) + + print "[openwebrx-dsp-plugin:csdr] Command =",command + #code.interact(local=locals()) + my_env=os.environ.copy() + if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; + if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; + self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) + self.running = True + + #open control pipes for csdr and send initialization data + if self.bpf_pipe != None: + self.bpf_pipe_file=open(self.bpf_pipe,"w") + self.set_bpf(self.low_cut,self.high_cut) + if self.shift_pipe != None: + self.shift_pipe_file=open(self.shift_pipe,"w") + self.set_offset_freq(self.offset_freq) + if self.squelch_pipe != None: + self.squelch_pipe_file=open(self.squelch_pipe,"w") + self.set_squelch_level(self.squelch_level) + if self.smeter_pipe != None: + self.smeter_pipe_file=open(self.smeter_pipe,"r") + self.set_pipe_nonblocking(self.smeter_pipe_file) + + self.start_secondary_demodulator() + + def read(self,size): + return self.process.stdout.read(size) + + def stop(self): + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + self.stop_secondary_demodulator() + #if(self.process.poll()!=None):return # returns None while subprocess is running + #while(self.process.poll()==None): + # #self.process.kill() + # print "killproc",os.getpgid(self.process.pid),self.process.pid + # os.killpg(self.process.pid, signal.SIGTERM) + # + # time.sleep(0.1) + + self.try_delete_pipes(self.pipe_names) + + # if self.bpf_pipe: + # try: os.unlink(self.bpf_pipe) + # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe + # if self.shift_pipe: + # try: os.unlink(self.shift_pipe) + # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.shift_pipe + # if self.squelch_pipe: + # try: os.unlink(self.squelch_pipe) + # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.squelch_pipe + # if self.smeter_pipe: + # try: os.unlink(self.smeter_pipe) + # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.smeter_pipe + # if self.iqtee_pipe: + # try: os.unlink(self.iqtee_pipe) + # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee_pipe + # if self.iqtee2_pipe: + # try: os.unlink(self.iqtee2_pipe) + # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee2_pipe + + self.running = False + + def restart(self): + self.stop() + self.start() + + def __del__(self): + self.stop() + del(self.process) diff --git a/htdocs/gfx/openwebrx-3d-spectrum.png b/htdocs/gfx/openwebrx-3d-spectrum.png new file mode 100644 index 0000000000000000000000000000000000000000..06ad495026a97c1300b4c29b353b7e473db219e9 GIT binary patch literal 10110 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 - - OpenWebRX | Open Source SDR Web App for Everyone! - - - - - - - + + OpenWebRX | Open Source SDR Web App for Everyone! + + + + + + + + + + +
-
-
- -
%[RX_PHOTO_TITLE]
-
%[RX_PHOTO_DESC]
-
-
-
- - - - -
%[RX_TITLE]
-
%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, [maps]
-
- - -
-
-
    -

  • Status
  • -

  • Log
  • -

  • Receiver
  • -
-
-
-
-
-
- -
-
- -
- -
-
-
-
---.--- MHz
-
---.--- MHz
- -
-
FM
-
AM
-
LSB
-
USB
-
CW
-
DMR
-
DStar
-
NXDN
-
YSF
-
-
-
- -
- -
-
-
- -
- -
-
-
-
-
-
-
0 dB
-
-
-
-
-
-
-
-
-
-
OpenWebRX client log
- Author: András Retzler, HA7ILM
You can support OpenWebRX development via PayPal!
-
-
-
-
-
Audio buffer [0 ms]
-
Audio output [0 sps]
-
Audio stream [0 kbps]
-
Network usage [0 kbps]
-
Server CPU [0%]
-
Clients [1]
-
+
+
+ +
%[RX_PHOTO_TITLE]
+
%[RX_PHOTO_DESC]
+
+
+
+ + + + +
%[RX_TITLE]
+
%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, [maps]
+
+ + +
+
+
    +

  • Status
  • +

  • Log
  • +

  • Receiver
  • +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
---.--- MHz
+
---.--- MHz
+
+
FM
+
AM
+
LSB
+
USB
+
CW
+
DMR
+
DStar
+
NXDN
+
YSF
+
+
+
DIG
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+
+
0 dB
+
+
+
+
+
+
+
+
+
+
+
OpenWebRX client log
+ Author: András Retzler, HA7ILM
You can support OpenWebRX development via PayPal!
+
+
+
+
+
+
Audio buffer [0 ms]
+
Audio output [0 sps]
+
Audio stream [0 kbps]
+
Network usage [0 kbps]
+
Server CPU [0%]
+
Clients [1]
+
+
+ Under construction +
We're working on the code right now, so the application might fail. +
+
+
+
+
+
+
+
+ +
+
+
-
- Under construction -
We're working on the code right now, so the application might fail. -
-
-
+
+
-
-
- -

Start OpenWebRX -
-
- +
+
+ +

Start OpenWebRX +
+
+ diff --git a/htdocs/jquery-3.2.1.min.js b/htdocs/jquery-3.2.1.min.js new file mode 100644 index 000000000..644d35e27 --- /dev/null +++ b/htdocs/jquery-3.2.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), +a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), +null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" - - - - - - - + + + + + + +
- +
%[RX_PHOTO_TITLE]
%[RX_PHOTO_DESC]
- - - - + + + +
%[RX_TITLE]
%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, [maps]
- - + +
    -

  • Status
  • -

  • Log
  • -

  • Receiver
  • +

  • Status
  • +

  • Log
  • +

  • Receiver
@@ -110,23 +110,23 @@
-
+
-
+
-
+
-
+
-
-
-
-
-
+
+
+
+
+
0 dB
@@ -172,7 +172,7 @@
- +

Start OpenWebRX
diff --git a/owrx/controllers.py b/owrx/controllers.py new file mode 100644 index 000000000..c7989c155 --- /dev/null +++ b/owrx/controllers.py @@ -0,0 +1,43 @@ +import mimetypes + +class Controller(object): + def __init__(self, handler, matches): + self.handler = handler + self.matches = matches + def send_response(self, content, code = 200, content_type = "text/html"): + self.handler.send_response(code) + if content_type is not None: + self.handler.send_header("Content-Type", content_type) + self.handler.end_headers() + if (type(content) == str): + content = content.encode() + self.handler.wfile.write(content) + def serve_file(self, file): + try: + f = open('htdocs/' + file, 'rb') + data = f.read() + f.close() + + (content_type, encoding) = mimetypes.MimeTypes().guess_type(file) + self.send_response(data, content_type = content_type) + except FileNotFoundError: + self.send_response("file not found", code = 404) + def render_template(self, template, **variables): + f = open('htdocs/' + template) + data = f.read() + f.close() + + self.send_response(data) + +class StatusController(Controller): + def handle_request(self): + self.send_response("you have reached the status page!") + +class IndexController(Controller): + def handle_request(self): + self.render_template("index.wrx") + +class AssetsController(Controller): + def handle_request(self): + filename = self.matches.group(1) + self.serve_file(filename) \ No newline at end of file diff --git a/owrx/http.py b/owrx/http.py new file mode 100644 index 000000000..b5ac0ae06 --- /dev/null +++ b/owrx/http.py @@ -0,0 +1,35 @@ +from owrx.controllers import StatusController, IndexController, AssetsController +from http.server import BaseHTTPRequestHandler +import re + +class RequestHandler(BaseHTTPRequestHandler): + def __init__(self, request, client_address, server): + self.router = Router() + super().__init__(request, client_address, server) + def do_GET(self): + self.router.route(self) + +class Router(object): + mappings = [ + {"route": "/", "controller": IndexController}, + {"route": "/status", "controller": StatusController}, + {"regex": "/static/(.+)", "controller": AssetsController} + ] + def find_controller(self, path): + for m in Router.mappings: + if "route" in m: + if m["route"] == path: + return (m["controller"], None) + if "regex" in m: + regex = re.compile(m["regex"]) + matches = regex.match(path) + if matches: + return (m["controller"], matches) + def route(self, handler): + res = self.find_controller(handler.path) + #print("path: {0}, controller: {1}, matches: {2}".format(handler.path, controller, matches)) + if res is not None: + (controller, matches) = res + controller(handler, matches).handle_request() + else: + handler.send_error(404, "Not Found", "The page you requested could not be found.") diff --git a/server.py b/server.py new file mode 100644 index 000000000..2b14a5202 --- /dev/null +++ b/server.py @@ -0,0 +1,6 @@ +from http.server import HTTPServer +from owrx.http import RequestHandler + +server = HTTPServer(('0.0.0.0', 3000), RequestHandler) +server.serve_forever() + From 89690d214d8f2091142cd9f232db28b553d6f8b5 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 4 May 2019 16:56:23 +0200 Subject: [PATCH 0037/2616] first work on the websocket connection --- htdocs/index.wrx | 11 ++------- htdocs/openwebrx.js | 57 ++++++++++++++++++++++++++++++++++++--------- owrx/config.py | 23 ++++++++++++++++++ owrx/controllers.py | 41 +++++++++++++++++++++++--------- owrx/http.py | 5 ++-- owrx/websocket.py | 47 +++++++++++++++++++++++++++++++++++++ server.py | 7 ++++++ 7 files changed, 158 insertions(+), 33 deletions(-) create mode 100644 owrx/config.py create mode 100644 owrx/websocket.py diff --git a/htdocs/index.wrx b/htdocs/index.wrx index 2cd64da41..885e3efbe 100644 --- a/htdocs/index.wrx +++ b/htdocs/index.wrx @@ -22,23 +22,16 @@ OpenWebRX | Open Source SDR Web App for Everyone! - --> diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index c7fd7c093..6efc93d33 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -79,7 +79,8 @@ is_chrome = /Chrome/.test(navigator.userAgent); function init_rx_photo() { - e("webrx-top-photo-clip").style.maxHeight=rx_photo_height.toString()+"px"; + var clip = e("webrx-top-photo-clip"); + clip.style.maxHeight=clip.clientHeight+"px"; window.setTimeout(function() { animate(e("webrx-rx-photo-title"),"opacity","",1,0,1,500,30); },1000); window.setTimeout(function() { animate(e("webrx-rx-photo-desc"),"opacity","",1,0,1,500,30); },1500); window.setTimeout(function() { close_rx_photo() },2500); @@ -1145,6 +1146,46 @@ var COMPRESS_FFT_PAD_N=10; //should be the same as in csdr.c function on_ws_recv(evt) { + if (typeof evt.data == 'string') { + // text messages + if (evt.data.substr(0, 16) == "CLIENT DE SERVER") { + divlog("Server acknowledged WebSocket connection."); + } else { + try { + json = JSON.parse(evt.data) + switch (json.type) { + case "config": + config = json.value; + window.waterfall_colors = config.waterfall_colors; + window.waterfall_min_level_default = config.waterfall_min_level; + window.waterfall_max_level_default = config.waterfall_max_level; + window.waterfall_auto_level_margin = config.waterfall_auto_level_margin; + waterfallColorsDefault(); + + bandwidth = config.samp_rate; + center_freq = config.shown_center_freq; + fft_size = config.fft_size; + fft_fps = config.fft_fps; + audio_compression = config.audio_compression; + divlog( "Audio stream is "+ ((audio_compression=="adpcm")?"compressed":"uncompressed")+"." ) + fft_compression = config.fft_compression; + divlog( "FFT stream is "+ ((fft_compression=="adpcm")?"compressed":"uncompressed")+"." ) + max_clients_num = config.max_clients; + waterfall_init(); + audio_preinit(); + break; + default: + console.warn('received message of unknown type', json); + } + } catch (e) { + // don't lose exception + console.error(e) + } + } + } else if (evt.data instanceof ArrayBuffer) { + // binary messages + } + return if(!(evt.data instanceof ArrayBuffer)) { divlog("on_ws_recv(): Not ArrayBuffer received...",1); return; } // debug_ws_data_received+=evt.data.byteLength/1000; @@ -1152,8 +1193,6 @@ function on_ws_recv(evt) first3Chars=first4Chars.slice(0,3); if(first3Chars=="CLI") { - var stringData=arrayBufferToString(evt.data); - if(stringData.substring(0,16)=="CLIENT DE SERVER") divlog("Server acknowledged WebSocket connection."); } if(first3Chars=="AUD") @@ -1574,7 +1613,7 @@ function parsehash() if(harr[0]=="mute") toggleMute(); else if(harr[0]=="mod") starting_mod = harr[1]; else if(harr[0]=="sql") - { + { config e("openwebrx-panel-squelch").value=harr[1]; updateSquelch(); } @@ -1692,14 +1731,10 @@ String.prototype.startswith=function(str){ return this.indexOf(str) == 0; }; //h function open_websocket() { - //if(ws_url.startswith("ws://localhost:")&&window.location.hostname!="127.0.0.1"&&window.location.hostname!="localhost") - //{ - //divlog("Server administrator should set server_hostname correctly, because it is left as \"localhost\". Now guessing hostname from page URL.",1); - ws_url="ws://"+(window.location.origin.split("://")[1])+"/ws/"; //guess automatically -> now default behaviour - //} + ws_url="ws://"+(window.location.origin.split("://")[1])+"/ws/"; //guess automatically -> now default behaviour if (!("WebSocket" in window)) divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser."); - ws = new WebSocket(ws_url+client_id); + ws = new WebSocket(ws_url); ws.onopen = on_ws_opened; ws.onmessage = on_ws_recv; ws.onclose = on_ws_closed; @@ -2196,7 +2231,7 @@ function openwebrx_init() //Synchronise volume with slider updateVolume(); - waterfallColorsDefault(); + } function iosPlayButtonClick() diff --git a/owrx/config.py b/owrx/config.py new file mode 100644 index 000000000..7e4e7e585 --- /dev/null +++ b/owrx/config.py @@ -0,0 +1,23 @@ +class Property(object): + def __init__(self, value = None): + self.value = value + def getValue(self): + return self.value + def setValue(self, value): + self.value = value + +class PropertyManager(object): + sharedInstance = None + @staticmethod + def getSharedInstance(): + if PropertyManager.sharedInstance is None: + PropertyManager.sharedInstance = PropertyManager() + return PropertyManager.sharedInstance + + def __init__(self): + self.properties = {} + + def getProperty(self, name): + if not name in self.properties: + self.properties[name] = Property() + return self.properties[name] diff --git a/owrx/controllers.py b/owrx/controllers.py index c7989c155..12a4aa213 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -1,4 +1,6 @@ import mimetypes +from owrx.websocket import WebSocketConnection +from owrx.config import PropertyManager class Controller(object): def __init__(self, handler, matches): @@ -12,16 +14,6 @@ def send_response(self, content, code = 200, content_type = "text/html"): if (type(content) == str): content = content.encode() self.handler.wfile.write(content) - def serve_file(self, file): - try: - f = open('htdocs/' + file, 'rb') - data = f.read() - f.close() - - (content_type, encoding) = mimetypes.MimeTypes().guess_type(file) - self.send_response(data, content_type = content_type) - except FileNotFoundError: - self.send_response("file not found", code = 404) def render_template(self, template, **variables): f = open('htdocs/' + template) data = f.read() @@ -38,6 +30,33 @@ def handle_request(self): self.render_template("index.wrx") class AssetsController(Controller): + def serve_file(self, file): + try: + f = open('htdocs/' + file, 'rb') + data = f.read() + f.close() + + (content_type, encoding) = mimetypes.MimeTypes().guess_type(file) + self.send_response(data, content_type = content_type) + except FileNotFoundError: + self.send_response("file not found", code = 404) def handle_request(self): filename = self.matches.group(1) - self.serve_file(filename) \ No newline at end of file + self.serve_file(filename) + + +class WebSocketController(Controller): + def handle_request(self): + conn = WebSocketConnection(self.handler) + conn.send("CLIENT DE SERVER openwebrx.py") + + config = {} + pm = PropertyManager.getSharedInstance() + + for key in ["waterfall_colors", "waterfall_min_level", "waterfall_max_level", "waterfall_auto_level_margin", + "shown_center_freq", "samp_rate", "fft_size", "fft_fps", "audio_compression", "fft_compression", + "max_clients"]: + + config[key] = pm.getProperty(key).getValue() + + conn.send({"type":"config","value":config}) diff --git a/owrx/http.py b/owrx/http.py index b5ac0ae06..ab4399b32 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -1,4 +1,4 @@ -from owrx.controllers import StatusController, IndexController, AssetsController +from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController from http.server import BaseHTTPRequestHandler import re @@ -13,7 +13,8 @@ class Router(object): mappings = [ {"route": "/", "controller": IndexController}, {"route": "/status", "controller": StatusController}, - {"regex": "/static/(.+)", "controller": AssetsController} + {"regex": "/static/(.+)", "controller": AssetsController}, + {"route": "/ws/", "controller": WebSocketController} ] def find_controller(self, path): for m in Router.mappings: diff --git a/owrx/websocket.py b/owrx/websocket.py new file mode 100644 index 000000000..8bd305ee8 --- /dev/null +++ b/owrx/websocket.py @@ -0,0 +1,47 @@ +import base64 +import hashlib +import json + +class WebSocketConnection(object): + def __init__(self, handler): + self.handler = handler + my_headers = self.handler.headers.items() + my_header_keys = list(map(lambda x:x[0],my_headers)) + h_key_exists = lambda x:my_header_keys.count(x) + h_value = lambda x:my_headers[my_header_keys.index(x)][1] + if (not h_key_exists("Upgrade")) or not (h_value("Upgrade")=="websocket") or (not h_key_exists("Sec-WebSocket-Key")): + raise WebSocketException + ws_key = h_value("Sec-WebSocket-Key") + shakey = hashlib.sha1() + shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key = ws_key).encode()) + ws_key_toreturn = base64.b64encode(shakey.digest()) + self.handler.wfile.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format(ws_key_toreturn.decode()).encode()) + + def get_header(self, size, opcode): + ws_first_byte = 0b10000000 | (opcode & 0x0F) + if(size>125): + return bytes([ws_first_byte, 126, (size>>8) & 0xff, size & 0xff]) + else: + # 256 bytes binary message in a single unmasked frame + return bytes([ws_first_byte, size]) + + def send(self, data): + # convenience + if (type(data) == dict): + data = json.dumps(data) + + # string-type messages are sent as text frames + if (type(data) == str): + header = self.get_header(len(data), 1) + self.handler.wfile.write(header) + self.handler.wfile.write(data.encode('utf-8')) + self.handler.wfile.flush() + # anything else as binary + else: + header = self.get_header(len(data), 2) + self.handler.wfile.write(header) + self.handler.wfile.write(data.encode()) + self.handler.wfile.flush() + +class WebSocketException(Exception): + pass diff --git a/server.py b/server.py index 2b14a5202..32850a5e7 100644 --- a/server.py +++ b/server.py @@ -1,5 +1,12 @@ from http.server import HTTPServer from owrx.http import RequestHandler +from owrx.config import PropertyManager + +cfg=__import__("config_webrx") +pm = PropertyManager.getSharedInstance() +for name, value in cfg.__dict__.items(): + if (name.startswith("__")): continue + pm.getProperty(name).setValue(value) server = HTTPServer(('0.0.0.0', 3000), RequestHandler) server.serve_forever() From 1f909080db4aed248299f37e769cb189023c3493 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 4 May 2019 20:26:11 +0200 Subject: [PATCH 0038/2616] we got fft --- config_webrx.py | 2 +- csdr.py | 14 +++--- htdocs/openwebrx.js | 53 ++++++++++++++--------- owrx/config.py | 3 ++ owrx/controllers.py | 72 ++++++++++++++++++++++++++----- owrx/source.py | 101 ++++++++++++++++++++++++++++++++++++++++++++ owrx/websocket.py | 32 +++++++++++++- server.py | 24 ++++++++--- 8 files changed, 254 insertions(+), 47 deletions(-) create mode 100644 owrx/source.py diff --git a/config_webrx.py b/config_webrx.py index 34e480c6b..a4f63ef0f 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -175,7 +175,7 @@ #A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels ### default theme by teejez: -waterfall_colors = "[0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]" +waterfall_colors = [0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff] waterfall_min_level = -88 #in dB waterfall_max_level = -20 waterfall_auto_level_margin = (5, 40) diff --git a/csdr.py b/csdr.py index a2fb4902b..a7f1f897c 100755 --- a/csdr.py +++ b/csdr.py @@ -129,7 +129,7 @@ def secondary_bw(self): def start_secondary_demodulator(self): if(not self.secondary_demodulator): return - print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate() + print("[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate()) secondary_command_fft=self.secondary_chain("fft") secondary_command_demod=self.secondary_chain(self.secondary_demodulator) self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft) @@ -150,16 +150,16 @@ def start_secondary_demodulator(self): if_samp_rate=self.if_samp_rate() ) - print "[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft - print "[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod + print("[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft) + print("[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod) #code.interact(local=locals()) my_env=os.environ.copy() #if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) - print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)" + print("[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)") self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes - print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)" #TODO digimodes + print("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") #TODO digimodes self.secondary_processes_running = True #open control pipes for csdr and send initialization data @@ -313,7 +313,7 @@ def try_delete_pipes(self, pipe_names): pipe_path = getattr(self,pipe_name,None) if pipe_path: try: os.unlink(pipe_path) - except Exception as e: print "[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e + except Exception as e: print("[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e) def set_pipe_nonblocking(self, pipe): flags = fcntl.fcntl(pipe, fcntl.F_GETFL) @@ -354,7 +354,7 @@ def start(self): flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, \ squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe ) - print "[openwebrx-dsp-plugin:csdr] Command =",command + print("[openwebrx-dsp-plugin:csdr] Command =",command) #code.interact(local=locals()) my_env=os.environ.copy() if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 6efc93d33..de12eea13 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1184,6 +1184,25 @@ function on_ws_recv(evt) } } else if (evt.data instanceof ArrayBuffer) { // binary messages + type = new Uint8Array(evt.data, 0, 1)[0] + data = evt.data.slice(1) + + switch (type) { + case 1: + if (fft_compression=="none") { + waterfall_add_queue(new Float32Array(data)); + } else if (fft_compression == "adpcm") { + fft_codec.reset(); + + var waterfall_i16=fft_codec.decode(new Uint8Array(data)); + var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N); + for(var i=0;i>>0)>>((3-i)*8))&0xff; }*/ - if(mathbox_mode==MATHBOX_MODES.WATERFALL) - { + if (mathbox_mode==MATHBOX_MODES.WATERFALL) { //Handle mathbox for(var i=0;i>>0)>>((3-i)*8))&0xff; - } - - //Draw image - canvas_context.putImageData(oneline_image, 0, canvas_actual_line--); - shift_canvases(); - if(canvas_actual_line<0) add_canvas(); + } else { + //Add line to waterfall image + oneline_image = canvas_context.createImageData(w,1); + for (x=0;x>>0)>>((3-i)*8))&0xff; + } + + //Draw image + canvas_context.putImageData(oneline_image, 0, canvas_actual_line--); + shift_canvases(); + if(canvas_actual_line<0) add_canvas(); } diff --git a/owrx/config.py b/owrx/config.py index 7e4e7e585..9866c9857 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -21,3 +21,6 @@ def getProperty(self, name): if not name in self.properties: self.properties[name] = Property() return self.properties[name] + + def getPropertyValue(self, name): + return self.getProperty(name).getValue() diff --git a/owrx/controllers.py b/owrx/controllers.py index 12a4aa213..f1c6ccde1 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -1,6 +1,9 @@ import mimetypes from owrx.websocket import WebSocketConnection from owrx.config import PropertyManager +from owrx.source import SpectrumThread +import csdr +import json class Controller(object): def __init__(self, handler, matches): @@ -44,19 +47,66 @@ def handle_request(self): filename = self.matches.group(1) self.serve_file(filename) +class SpectrumForwarder(object): + def __init__(self, conn): + self.conn = conn + def write_spectrum_data(self, data): + self.conn.send(bytes([0x01]) + data) -class WebSocketController(Controller): - def handle_request(self): - conn = WebSocketConnection(self.handler) - conn.send("CLIENT DE SERVER openwebrx.py") +class WebSocketMessageHandler(object): + def __init__(self): + self.forwarder = None + + def handleTextMessage(self, conn, message): + if (message[:16] == "SERVER DE CLIENT"): + config = {} + pm = PropertyManager.getSharedInstance() + + for key in ["waterfall_colors", "waterfall_min_level", "waterfall_max_level", "waterfall_auto_level_margin", + "shown_center_freq", "samp_rate", "fft_size", "fft_fps", "audio_compression", "fft_compression", + "max_clients"]: + + config[key] = pm.getProperty(key).getValue() - config = {} - pm = PropertyManager.getSharedInstance() + conn.send({"type":"config","value":config}) + print("client connection intitialized") - for key in ["waterfall_colors", "waterfall_min_level", "waterfall_max_level", "waterfall_auto_level_margin", - "shown_center_freq", "samp_rate", "fft_size", "fft_fps", "audio_compression", "fft_compression", - "max_clients"]: + dsp = self.dsp = csdr.dsp() + dsp_initialized=False + dsp.set_audio_compression(pm.getPropertyValue("audio_compression")) + dsp.set_fft_compression(pm.getPropertyValue("fft_compression")) #used by secondary chains + dsp.set_format_conversion(pm.getPropertyValue("format_conversion")) + dsp.set_offset_freq(0) + dsp.set_bpf(-4000,4000) + dsp.set_secondary_fft_size(pm.getPropertyValue("digimodes_fft_size")) + dsp.nc_port=pm.getPropertyValue("iq_server_port") + dsp.csdr_dynamic_bufsize = pm.getPropertyValue("csdr_dynamic_bufsize") + dsp.csdr_print_bufsizes = pm.getPropertyValue("csdr_print_bufsizes") + dsp.csdr_through = pm.getPropertyValue("csdr_through") + do_secondary_demod=False - config[key] = pm.getProperty(key).getValue() + self.forwarder = SpectrumForwarder(conn) + SpectrumThread.getSharedInstance().add_client(self.forwarder) - conn.send({"type":"config","value":config}) + else: + try: + message = json.loads(message) + if message["type"] == "start": + self.dsp.set_samp_rate(message["params"]["output_rate"]) + self.dsp.start() + except json.JSONDecodeError: + print("message is not json: {0}".format(message)) + + def handleBinaryMessage(self, conn, data): + print("unsupported binary message, discarding") + + def handleClose(self, conn): + if self.forwarder: + SpectrumThread.getSharedInstance().remove_client(self.forwarder) + +class WebSocketController(Controller): + def handle_request(self): + conn = WebSocketConnection(self.handler, WebSocketMessageHandler()) + conn.send("CLIENT DE SERVER openwebrx.py") + # enter read loop + conn.read_loop() diff --git a/owrx/source.py b/owrx/source.py new file mode 100644 index 000000000..0f1801489 --- /dev/null +++ b/owrx/source.py @@ -0,0 +1,101 @@ +import subprocess +from owrx.config import PropertyManager +import threading +import csdr +import time + +class RtlNmuxSource(object): + def __init__(self): + pm = PropertyManager.getSharedInstance() + + nmux_bufcnt = nmux_bufsize = 0 + while nmux_bufsize < pm.getPropertyValue("samp_rate")/4: nmux_bufsize += 4096 + while nmux_bufsize * nmux_bufcnt < pm.getPropertyValue("nmux_memory") * 1e6: nmux_bufcnt += 1 + if nmux_bufcnt == 0 or nmux_bufsize == 0: + print("[openwebrx-main] Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py") + return + print("[openwebrx-main] nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)) + cmd = pm.getPropertyValue("start_rtl_command") + "| nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, pm.getPropertyValue("iq_server_port")) + subprocess.Popen(cmd, shell=True) + print("[openwebrx-main] Started rtl source: " + cmd) + +class SpectrumThread(threading.Thread): + sharedInstance = None + @staticmethod + def getSharedInstance(): + if SpectrumThread.sharedInstance is None: + SpectrumThread.sharedInstance = SpectrumThread() + SpectrumThread.sharedInstance.start() + return SpectrumThread.sharedInstance + + def __init__(self): + self.clients = [] + self.doRun = True + super().__init__() + + def run(self): + pm = PropertyManager.getSharedInstance() + + samp_rate = pm.getPropertyValue("samp_rate") + fft_size = pm.getPropertyValue("fft_size") + fft_fps = pm.getPropertyValue("fft_fps") + fft_voverlap_factor = pm.getPropertyValue("fft_voverlap_factor") + fft_compression = pm.getPropertyValue("fft_compression") + format_conversion = pm.getPropertyValue("format_conversion") + + spectrum_dsp=dsp=csdr.dsp() + dsp.nc_port = pm.getPropertyValue("iq_server_port") + dsp.set_demodulator("fft") + dsp.set_samp_rate(samp_rate) + dsp.set_fft_size(fft_size) + dsp.set_fft_fps(fft_fps) + dsp.set_fft_averages(int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor))) if fft_voverlap_factor>0 else 0) + dsp.set_fft_compression(fft_compression) + dsp.set_format_conversion(format_conversion) + dsp.csdr_dynamic_bufsize = pm.getPropertyValue("csdr_dynamic_bufsize") + dsp.csdr_print_bufsizes = pm.getPropertyValue("csdr_print_bufsizes") + dsp.csdr_through = pm.getPropertyValue("csdr_through") + sleep_sec=0.87/fft_fps + print("[openwebrx-spectrum] Spectrum thread initialized successfully.") + dsp.start() + if pm.getPropertyValue("csdr_dynamic_bufsize"): + dsp.read(8) #dummy read to skip bufsize & preamble + print("[openwebrx-spectrum] Note: CSDR_DYNAMIC_BUFSIZE_ON = 1") + print("[openwebrx-spectrum] Spectrum thread started.") + bytes_to_read=int(dsp.get_fft_bytes_to_read()) + spectrum_thread_counter=0 + while self.doRun: + data=dsp.read(bytes_to_read) + #print("gotcha",len(data),"bytes of spectrum data via spectrum_thread_function()") + if spectrum_thread_counter >= fft_fps: + spectrum_thread_counter=0 + else: spectrum_thread_counter+=1 + for c in self.clients: + c.write_spectrum_data(data) + ''' + correction=0 + for i in range(0,len(clients)): + i-=correction + if (clients[i].ws_started): + if clients[i].spectrum_queue.full(): + print "[openwebrx-spectrum] client spectrum queue full, closing it." + close_client(i, False) + correction+=1 + else: + clients[i].spectrum_queue.put([data]) # add new string by "reference" to all clients + ''' + + print("spectrum thread shut down") + + def add_client(self, c): + self.clients.append(c) + + def remove_client(self, c): + self.clients.remove(c) + if not self.clients: + self.shutdown() + + def shutdown(self): + print("shutting down spectrum thread") + SpectrumThread.sharedInstance = None + self.doRun = False \ No newline at end of file diff --git a/owrx/websocket.py b/owrx/websocket.py index 8bd305ee8..7c96e34b5 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -3,8 +3,9 @@ import json class WebSocketConnection(object): - def __init__(self, handler): + def __init__(self, handler, messageHandler): self.handler = handler + self.messageHandler = messageHandler my_headers = self.handler.headers.items() my_header_keys = list(map(lambda x:x[0],my_headers)) h_key_exists = lambda x:my_header_keys.count(x) @@ -40,8 +41,35 @@ def send(self, data): else: header = self.get_header(len(data), 2) self.handler.wfile.write(header) - self.handler.wfile.write(data.encode()) + self.handler.wfile.write(data) self.handler.wfile.flush() + def read_loop(self): + open = True + while (open): + header = self.handler.rfile.read(2) + opcode = header[0] & 0x0F + length = header[1] & 0x7F + mask = (header[1] & 0x80) >> 7 + if (length == 126): + header = self.handler.rfile.read(2) + length = (header[0] << 8) + header[1] + if (mask): + masking_key = self.handler.rfile.read(4) + data = self.handler.rfile.read(length) + print("opcode: {0}, length: {1}, mask: {2}".format(opcode, length, mask)) + if (mask): + data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) + if (opcode == 1): + message = data.decode('utf-8') + self.messageHandler.handleTextMessage(self, message) + elif (opcode == 2): + self.messageHandler.handleBinaryMessage(self, data) + elif (opcode == 8): + open = False + self.messageHandler.handleClose(self) + else: + print("unsupported opcode: {0}".format(opcode)) + class WebSocketException(Exception): pass diff --git a/server.py b/server.py index 32850a5e7..c267f9e28 100644 --- a/server.py +++ b/server.py @@ -1,13 +1,23 @@ from http.server import HTTPServer from owrx.http import RequestHandler from owrx.config import PropertyManager +from owrx.source import RtlNmuxSource, SpectrumThread +from socketserver import ThreadingMixIn -cfg=__import__("config_webrx") -pm = PropertyManager.getSharedInstance() -for name, value in cfg.__dict__.items(): - if (name.startswith("__")): continue - pm.getProperty(name).setValue(value) +class ThreadedHttpServer(ThreadingMixIn, HTTPServer): + pass -server = HTTPServer(('0.0.0.0', 3000), RequestHandler) -server.serve_forever() +def main(): + cfg=__import__("config_webrx") + pm = PropertyManager.getSharedInstance() + for name, value in cfg.__dict__.items(): + if (name.startswith("__")): continue + pm.getProperty(name).setValue(value) + RtlNmuxSource() + + server = ThreadedHttpServer(('0.0.0.0', 3000), RequestHandler) + server.serve_forever() + +if __name__=="__main__": + main() From 6ec21e6716d9a76127aaa9d56570e3349acc4b98 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 4 May 2019 20:40:13 +0200 Subject: [PATCH 0039/2616] send missing parameters for audio client startup --- htdocs/index.wrx | 2 -- htdocs/openwebrx.js | 2 ++ owrx/controllers.py | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/htdocs/index.wrx b/htdocs/index.wrx index 885e3efbe..f09193348 100644 --- a/htdocs/index.wrx +++ b/htdocs/index.wrx @@ -25,8 +25,6 @@ From 210fe5352fa0afb41512d72c0e50746640490522 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 12 May 2019 14:35:25 +0200 Subject: [PATCH 0103/2616] refactor the sdr.hu updater into the new server, too --- openwebrx.py | 12 ++++++------ owrx/config.py | 15 +++++++++++++++ owrx/sdrhu.py | 36 ++++++++++++++++++++++++++++++++++++ sdrhu.py | 32 +++++++------------------------- 4 files changed, 64 insertions(+), 31 deletions(-) create mode 100644 owrx/sdrhu.py diff --git a/openwebrx.py b/openwebrx.py index 07df451a3..b89d7e52c 100644 --- a/openwebrx.py +++ b/openwebrx.py @@ -3,6 +3,7 @@ from owrx.config import PropertyManager, FeatureDetector from owrx.source import SdrService from socketserver import ThreadingMixIn +from owrx.sdrhu import SdrHuUpdater import logging logging.basicConfig(level = logging.DEBUG, format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s") @@ -22,12 +23,7 @@ def main(): """) - cfg = __import__("config_webrx") - pm = PropertyManager.getSharedInstance() - for name, value in cfg.__dict__.items(): - if name.startswith("__"): - continue - pm[name] = value + pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") featureDetector = FeatureDetector() if not featureDetector.is_available("core"): @@ -39,6 +35,10 @@ def main(): # Get error messages about unknown / unavailable features as soon as possible SdrService.loadProps() + if "sdrhu_key" in pm and pm["sdrhu_public_listing"]: + updater = SdrHuUpdater() + updater.start() + server = ThreadedHttpServer(('0.0.0.0', pm.getPropertyValue("web_port")), RequestHandler) server.serve_forever() diff --git a/owrx/config.py b/owrx/config.py index cc84c266b..a11722835 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -53,6 +53,9 @@ def fireCallbacks(value): prop.wire(fireCallbacks) return self + def __contains__(self, name): + return self.hasProperty(name) + def __getitem__(self, name): return self.getPropertyValue(name) @@ -61,6 +64,9 @@ def __setitem__(self, name, value): self.add(name, Property()) self.getProperty(name).setValue(value) + def __dict__(self): + return {k:v.getValue() for k, v in self.properties.items()} + def hasProperty(self, name): return name in self.properties @@ -86,6 +92,15 @@ def defaults(self, other_pm): p.setValue(other_pm[key]) return self + def loadConfig(self, filename): + cfg = __import__(filename) + for name, value in cfg.__dict__.items(): + if name.startswith("__"): + continue + self[name] = value + return self + + class UnknownFeatureException(Exception): pass diff --git a/owrx/sdrhu.py b/owrx/sdrhu.py new file mode 100644 index 000000000..b2c6b0ff5 --- /dev/null +++ b/owrx/sdrhu.py @@ -0,0 +1,36 @@ +import threading +import subprocess +import time +from owrx.config import PropertyManager + +import logging +logger = logging.getLogger(__name__) + + +class SdrHuUpdater(threading.Thread): + def __init__(self): + self.doRun = True + super().__init__() + + def update(self): + pm = PropertyManager.getSharedInstance() + cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}\" 2>&1".format(**pm.__dict__()) + logger.debug(cmd) + returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate() + returned=returned[0].decode('utf-8') + if "UPDATE:" in returned: + retrytime_mins = 20 + value=returned.split("UPDATE:")[1].split("\n",1)[0] + if value.startswith("SUCCESS"): + logger.info("Update succeeded!") + else: + logger.warning("Update failed, your receiver cannot be listed on sdr.hu! Reason: %s", value) + else: + retrytime_mins = 2 + logger.warning("wget failed while updating, your receiver cannot be listed on sdr.hu!") + return retrytime_mins + + def run(self): + while self.doRun: + retrytime_mins = self.update() + time.sleep(60*retrytime_mins) diff --git a/sdrhu.py b/sdrhu.py index d06ae058e..3060789a5 100755 --- a/sdrhu.py +++ b/sdrhu.py @@ -20,31 +20,13 @@ """ -import config_webrx as cfg, time, subprocess - -def run(continuously=True): - if not cfg.sdrhu_key: return - firsttime="(Your receiver is soon getting listed on sdr.hu!)" - while True: - cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://"+cfg.server_hostname+":"+str(cfg.web_port)+"&apikey="+cfg.sdrhu_key+"\" 2>&1" - print "[openwebrx-sdrhu]", cmd - returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate() - returned=returned[0] - #print returned - if "UPDATE:" in returned: - retrytime_mins = 20 - value=returned.split("UPDATE:")[1].split("\n",1)[0] - if value.startswith("SUCCESS"): - print "[openwebrx-sdrhu] Update succeeded! "+firsttime - firsttime="" - else: - print "[openwebrx-sdrhu] Update failed, your receiver cannot be listed on sdr.hu! Reason:", value - else: - retrytime_mins = 2 - print "[openwebrx-sdrhu] wget failed while updating, your receiver cannot be listed on sdr.hu!" - if not continuously: break - time.sleep(60*retrytime_mins) +from owrx.sdrhu import SdrHuUpdater +from owrx.config import PropertyManager if __name__=="__main__": - run(False) + pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") + + if not "sdrhu_key" in pm: + exit(1) + SdrHuUpdater().update() From da37d03104a6ebf0c6cdd87fbdcd85d9a5fe96ca Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 12 May 2019 15:56:18 +0200 Subject: [PATCH 0104/2616] refactor into more reasonable namespaces --- openwebrx.py | 3 +- owrx/config.py | 67 +--------------- owrx/connection.py | 180 +++++++++++++++++++++++++++++++++++++++++++ owrx/controllers.py | 182 +------------------------------------------- owrx/feature.py | 65 ++++++++++++++++ owrx/source.py | 3 +- 6 files changed, 254 insertions(+), 246 deletions(-) create mode 100644 owrx/connection.py create mode 100644 owrx/feature.py diff --git a/openwebrx.py b/openwebrx.py index b89d7e52c..e41d6c701 100644 --- a/openwebrx.py +++ b/openwebrx.py @@ -1,6 +1,7 @@ from http.server import HTTPServer from owrx.http import RequestHandler -from owrx.config import PropertyManager, FeatureDetector +from owrx.config import PropertyManager +from owrx.feature import FeatureDetector from owrx.source import SdrService from socketserver import ThreadingMixIn from owrx.sdrhu import SdrHuUpdater diff --git a/owrx/config.py b/owrx/config.py index a11722835..8fb65132c 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -1,8 +1,7 @@ -import os - import logging logger = logging.getLogger(__name__) + class Property(object): def __init__(self, value = None): self.value = value @@ -99,67 +98,3 @@ def loadConfig(self, filename): continue self[name] = value return self - - -class UnknownFeatureException(Exception): - pass - -class RequirementMissingException(Exception): - pass - -class FeatureDetector(object): - features = { - "core": [ "csdr", "nmux" ], - "rtl_sdr": [ "rtl_sdr" ], - "sdrplay": [ "rx_tools" ], - "hackrf": [ "hackrf_transfer" ] - } - - def is_available(self, feature): - return self.has_requirements(self.get_requirements(feature)) - - def get_requirements(self, feature): - try: - return FeatureDetector.features[feature] - except KeyError: - raise UnknownFeatureException("Feature \"{0}\" is not known.".format(feature)) - - def has_requirements(self, requirements): - passed = True - for requirement in requirements: - methodname = "has_" + requirement - if hasattr(self, methodname) and callable(getattr(self, methodname)): - passed = passed and getattr(self, methodname)() - else: - logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement)) - return passed - - def has_csdr(self): - return os.system("csdr 2> /dev/null") != 32512 - - def has_nmux(self): - return os.system("nmux --help 2> /dev/null") != 32512 - - def has_rtl_sdr(self): - return os.system("rtl_sdr --help 2> /dev/null") != 32512 - - def has_rx_tools(self): - return os.system("rx_sdr --help 2> /dev/null") != 32512 - - """ - To use a HackRF, compile the HackRF host tools from its "stdout" branch: - git clone https://github.com/mossmann/hackrf/ - cd hackrf - git fetch - git checkout origin/stdout - cd host - mkdir build - cd build - cmake .. -DINSTALL_UDEV_RULES=ON - make - sudo make install - """ - def has_hackrf_transfer(self): - # TODO i don't have a hackrf, so somebody doublecheck this. - # TODO also check if it has the stdout feature - return os.system("hackrf_transfer --help 2> /dev/null") != 32512 diff --git a/owrx/connection.py b/owrx/connection.py new file mode 100644 index 000000000..a0442b821 --- /dev/null +++ b/owrx/connection.py @@ -0,0 +1,180 @@ +from owrx.config import PropertyManager +from owrx.source import DspManager, CpuUsageThread, SdrService, ClientReporterThread +import json + +import logging +logger = logging.getLogger(__name__) + +class OpenWebRxClient(object): + config_keys = ["waterfall_colors", "waterfall_min_level", "waterfall_max_level", + "waterfall_auto_level_margin", "lfo_offset", "samp_rate", "fft_size", "fft_fps", + "audio_compression", "fft_compression", "max_clients", "start_mod", + "client_audio_buffer_size", "start_freq", "center_freq", "mathbox_waterfall_colors", + "mathbox_waterfall_history_length", "mathbox_waterfall_frequency_resolution"] + def __init__(self, conn): + self.conn = conn + + ClientReporterThread.getSharedInstance().addClient(self) + + self.dsp = None + self.sdr = None + self.configProps = None + + pm = PropertyManager.getSharedInstance() + + self.setSdr() + + # send receiver info + receiver_keys = ["receiver_name", "receiver_location", "receiver_qra", "receiver_asl", "receiver_gps", + "photo_title", "photo_desc"] + receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys) + self.write_receiver_details(receiver_details) + + profiles = [{"name": s.getName() + " " + p["name"], "id":sid + "|" + pid} for (sid, s) in SdrService.getSources().items() for (pid, p) in s.getProfiles().items()] + self.write_profiles(profiles) + + CpuUsageThread.getSharedInstance().add_client(self) + + def sendConfig(self, key, value): + config = dict((key, self.configProps[key]) for key in OpenWebRxClient.config_keys) + # TODO mathematical properties? hmmmm + config["start_offset_freq"] = self.configProps["start_freq"] - self.configProps["center_freq"] + self.write_config(config) + def setSdr(self, id = None): + next = SdrService.getSource(id) + if (next == self.sdr): + return + + self.stopDsp() + + if self.configProps is not None: + self.configProps.unwire(self.sendConfig) + + self.sdr = next + + # send initial config + self.configProps = self.sdr.getProps().collect(*OpenWebRxClient.config_keys).defaults(PropertyManager.getSharedInstance()) + + self.configProps.wire(self.sendConfig) + self.sendConfig(None, None) + + self.sdr.addSpectrumClient(self) + + def startDsp(self): + if self.dsp is None: + self.dsp = DspManager(self, self.sdr) + self.dsp.start() + + def close(self): + self.stopDsp() + CpuUsageThread.getSharedInstance().remove_client(self) + try: + ClientReporterThread.getSharedInstance().removeClient(self) + except ValueError: + pass + logger.debug("connection closed") + + def stopDsp(self): + if self.dsp is not None: + self.dsp.stop() + self.dsp = None + if self.sdr is not None: + self.sdr.removeSpectrumClient(self) + + def setParams(self, params): + # only the keys in the protected property manager can be overridden from the web + protected = self.sdr.getProps().collect("samp_rate", "center_freq", "rf_gain", "type") \ + .defaults(PropertyManager.getSharedInstance()) + for key, value in params.items(): + protected[key] = value + + def setDspProperties(self, params): + for key, value in params.items(): + self.dsp.setProperty(key, value) + + def protected_send(self, data): + try: + self.conn.send(data) + # these exception happen when the socket is closed + except OSError: + self.close() + except ValueError: + self.close() + + def write_spectrum_data(self, data): + self.protected_send(bytes([0x01]) + data) + def write_dsp_data(self, data): + self.protected_send(bytes([0x02]) + data) + def write_s_meter_level(self, level): + self.protected_send({"type":"smeter","value":level}) + def write_cpu_usage(self, usage): + self.protected_send({"type":"cpuusage","value":usage}) + def write_clients(self, clients): + self.protected_send({"type":"clients","value":clients}) + def write_secondary_fft(self, data): + self.protected_send(bytes([0x03]) + data) + def write_secondary_demod(self, data): + self.protected_send(bytes([0x04]) + data) + def write_secondary_dsp_config(self, cfg): + self.protected_send({"type":"secondary_config", "value":cfg}) + def write_config(self, cfg): + self.protected_send({"type":"config","value":cfg}) + def write_receiver_details(self, details): + self.protected_send({"type":"receiver_details","value":details}) + def write_profiles(self, profiles): + self.protected_send({"type":"profiles","value":profiles}) + +class WebSocketMessageHandler(object): + def __init__(self): + self.handshake = None + self.client = None + self.dsp = None + + def handleTextMessage(self, conn, message): + if (message[:16] == "SERVER DE CLIENT"): + # maybe put some more info in there? nothing to store yet. + self.handshake = "completed" + logger.debug("client connection intitialized") + + self.client = OpenWebRxClient(conn) + + return + + if not self.handshake: + logger.warning("not answering client request since handshake is not complete") + return + + try: + message = json.loads(message) + if "type" in message: + if message["type"] == "dspcontrol": + if "action" in message and message["action"] == "start": + self.client.startDsp() + + if "params" in message: + params = message["params"] + self.client.setDspProperties(params) + + if message["type"] == "config": + if "params" in message: + self.client.setParams(message["params"]) + if message["type"] == "setsdr": + if "params" in message: + self.client.setSdr(message["params"]["sdr"]) + if message["type"] == "selectprofile": + if "params" in message and "profile" in message["params"]: + profile = message["params"]["profile"].split("|") + self.client.setSdr(profile[0]) + self.client.sdr.activateProfile(profile[1]) + else: + logger.warning("received message without type: {0}".format(message)) + + except json.JSONDecodeError: + logger.warning("message is not json: {0}".format(message)) + + def handleBinaryMessage(self, conn, data): + logger.error("unsupported binary message, discarding") + + def handleClose(self, conn): + if self.client: + self.client.close() diff --git a/owrx/controllers.py b/owrx/controllers.py index 366eb3acf..a41015ac1 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -1,11 +1,11 @@ +import os import mimetypes +from datetime import datetime from owrx.websocket import WebSocketConnection from owrx.config import PropertyManager -from owrx.source import DspManager, CpuUsageThread, SdrService, ClientReporterThread +from owrx.source import ClientReporterThread +from owrx.connection import WebSocketMessageHandler from owrx.version import openwebrx_version -import json -import os -from datetime import datetime import logging logger = logging.getLogger(__name__) @@ -79,180 +79,6 @@ class IndexController(AssetsController): def handle_request(self): self.serve_file("index.wrx", "text/html") -class OpenWebRxClient(object): - config_keys = ["waterfall_colors", "waterfall_min_level", "waterfall_max_level", - "waterfall_auto_level_margin", "lfo_offset", "samp_rate", "fft_size", "fft_fps", - "audio_compression", "fft_compression", "max_clients", "start_mod", - "client_audio_buffer_size", "start_freq", "center_freq", "mathbox_waterfall_colors", - "mathbox_waterfall_history_length", "mathbox_waterfall_frequency_resolution"] - def __init__(self, conn): - self.conn = conn - - ClientReporterThread.getSharedInstance().addClient(self) - - self.dsp = None - self.sdr = None - self.configProps = None - - pm = PropertyManager.getSharedInstance() - - self.setSdr() - - # send receiver info - receiver_keys = ["receiver_name", "receiver_location", "receiver_qra", "receiver_asl", "receiver_gps", - "photo_title", "photo_desc"] - receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys) - self.write_receiver_details(receiver_details) - - profiles = [{"name": s.getName() + " " + p["name"], "id":sid + "|" + pid} for (sid, s) in SdrService.getSources().items() for (pid, p) in s.getProfiles().items()] - self.write_profiles(profiles) - - CpuUsageThread.getSharedInstance().add_client(self) - - def sendConfig(self, key, value): - config = dict((key, self.configProps[key]) for key in OpenWebRxClient.config_keys) - # TODO mathematical properties? hmmmm - config["start_offset_freq"] = self.configProps["start_freq"] - self.configProps["center_freq"] - self.write_config(config) - def setSdr(self, id = None): - next = SdrService.getSource(id) - if (next == self.sdr): - return - - self.stopDsp() - - if self.configProps is not None: - self.configProps.unwire(self.sendConfig) - - self.sdr = next - - # send initial config - self.configProps = self.sdr.getProps().collect(*OpenWebRxClient.config_keys).defaults(PropertyManager.getSharedInstance()) - - self.configProps.wire(self.sendConfig) - self.sendConfig(None, None) - - self.sdr.addSpectrumClient(self) - - def startDsp(self): - if self.dsp is None: - self.dsp = DspManager(self, self.sdr) - self.dsp.start() - - def close(self): - self.stopDsp() - CpuUsageThread.getSharedInstance().remove_client(self) - try: - ClientReporterThread.getSharedInstance().removeClient(self) - except ValueError: - pass - logger.debug("connection closed") - - def stopDsp(self): - if self.dsp is not None: - self.dsp.stop() - self.dsp = None - if self.sdr is not None: - self.sdr.removeSpectrumClient(self) - - def setParams(self, params): - # only the keys in the protected property manager can be overridden from the web - protected = self.sdr.getProps().collect("samp_rate", "center_freq", "rf_gain", "type") \ - .defaults(PropertyManager.getSharedInstance()) - for key, value in params.items(): - protected[key] = value - - def setDspProperties(self, params): - for key, value in params.items(): - self.dsp.setProperty(key, value) - - def protected_send(self, data): - try: - self.conn.send(data) - # these exception happen when the socket is closed - except OSError: - self.close() - except ValueError: - self.close() - - def write_spectrum_data(self, data): - self.protected_send(bytes([0x01]) + data) - def write_dsp_data(self, data): - self.protected_send(bytes([0x02]) + data) - def write_s_meter_level(self, level): - self.protected_send({"type":"smeter","value":level}) - def write_cpu_usage(self, usage): - self.protected_send({"type":"cpuusage","value":usage}) - def write_clients(self, clients): - self.protected_send({"type":"clients","value":clients}) - def write_secondary_fft(self, data): - self.protected_send(bytes([0x03]) + data) - def write_secondary_demod(self, data): - self.protected_send(bytes([0x04]) + data) - def write_secondary_dsp_config(self, cfg): - self.protected_send({"type":"secondary_config", "value":cfg}) - def write_config(self, cfg): - self.protected_send({"type":"config","value":cfg}) - def write_receiver_details(self, details): - self.protected_send({"type":"receiver_details","value":details}) - def write_profiles(self, profiles): - self.protected_send({"type":"profiles","value":profiles}) - -class WebSocketMessageHandler(object): - def __init__(self): - self.handshake = None - self.client = None - self.dsp = None - - def handleTextMessage(self, conn, message): - if (message[:16] == "SERVER DE CLIENT"): - # maybe put some more info in there? nothing to store yet. - self.handshake = "completed" - logger.debug("client connection intitialized") - - self.client = OpenWebRxClient(conn) - - return - - if not self.handshake: - logger.warning("not answering client request since handshake is not complete") - return - - try: - message = json.loads(message) - if "type" in message: - if message["type"] == "dspcontrol": - if "action" in message and message["action"] == "start": - self.client.startDsp() - - if "params" in message: - params = message["params"] - self.client.setDspProperties(params) - - if message["type"] == "config": - if "params" in message: - self.client.setParams(message["params"]) - if message["type"] == "setsdr": - if "params" in message: - self.client.setSdr(message["params"]["sdr"]) - if message["type"] == "selectprofile": - if "params" in message and "profile" in message["params"]: - profile = message["params"]["profile"].split("|") - self.client.setSdr(profile[0]) - self.client.sdr.activateProfile(profile[1]) - else: - logger.warning("received message without type: {0}".format(message)) - - except json.JSONDecodeError: - logger.warning("message is not json: {0}".format(message)) - - def handleBinaryMessage(self, conn, data): - logger.error("unsupported binary message, discarding") - - def handleClose(self, conn): - if self.client: - self.client.close() - class WebSocketController(Controller): def handle_request(self): conn = WebSocketConnection(self.handler, WebSocketMessageHandler()) diff --git a/owrx/feature.py b/owrx/feature.py new file mode 100644 index 000000000..83f9232f2 --- /dev/null +++ b/owrx/feature.py @@ -0,0 +1,65 @@ +import os + +import logging +logger = logging.getLogger(__name__) + + +class UnknownFeatureException(Exception): + pass + +class FeatureDetector(object): + features = { + "core": [ "csdr", "nmux" ], + "rtl_sdr": [ "rtl_sdr" ], + "sdrplay": [ "rx_tools" ], + "hackrf": [ "hackrf_transfer" ] + } + + def is_available(self, feature): + return self.has_requirements(self.get_requirements(feature)) + + def get_requirements(self, feature): + try: + return FeatureDetector.features[feature] + except KeyError: + raise UnknownFeatureException("Feature \"{0}\" is not known.".format(feature)) + + def has_requirements(self, requirements): + passed = True + for requirement in requirements: + methodname = "has_" + requirement + if hasattr(self, methodname) and callable(getattr(self, methodname)): + passed = passed and getattr(self, methodname)() + else: + logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement)) + return passed + + def has_csdr(self): + return os.system("csdr 2> /dev/null") != 32512 + + def has_nmux(self): + return os.system("nmux --help 2> /dev/null") != 32512 + + def has_rtl_sdr(self): + return os.system("rtl_sdr --help 2> /dev/null") != 32512 + + def has_rx_tools(self): + return os.system("rx_sdr --help 2> /dev/null") != 32512 + + """ + To use a HackRF, compile the HackRF host tools from its "stdout" branch: + git clone https://github.com/mossmann/hackrf/ + cd hackrf + git fetch + git checkout origin/stdout + cd host + mkdir build + cd build + cmake .. -DINSTALL_UDEV_RULES=ON + make + sudo make install + """ + def has_hackrf_transfer(self): + # TODO i don't have a hackrf, so somebody doublecheck this. + # TODO also check if it has the stdout feature + return os.system("hackrf_transfer --help 2> /dev/null") != 32512 diff --git a/owrx/source.py b/owrx/source.py index f918e54a1..c45d12f5a 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -1,5 +1,6 @@ import subprocess -from owrx.config import PropertyManager, FeatureDetector, UnknownFeatureException +from owrx.config import PropertyManager +from owrx.feature import FeatureDetector, UnknownFeatureException import threading import csdr import time From ddf9123e8b170a5ff2c7388ca2a4ba9445fcb795 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 12 May 2019 16:02:49 +0200 Subject: [PATCH 0105/2616] fix auto-sqelch --- htdocs/openwebrx.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index cf9f67822..23f17a2cd 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1206,7 +1206,8 @@ function on_ws_recv(evt) e('webrx-rx-photo-desc').innerHTML = r.photo_desc; break; case "smeter": - setSmeterAbsoluteValue(json.value); + smeter_level = json.value; + setSmeterAbsoluteValue(smeter_level); break; case "cpuusage": var server_cpu_usage = json.value; From 85be2e97a1b1186cabb6ca5bb97a7f097bcc33f4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 12 May 2019 17:20:44 +0200 Subject: [PATCH 0106/2616] this is now obsolete, as well --- rxws.py | 171 -------------------------------------------------------- 1 file changed, 171 deletions(-) delete mode 100644 rxws.py diff --git a/rxws.py b/rxws.py deleted file mode 100644 index a1f210cef..000000000 --- a/rxws.py +++ /dev/null @@ -1,171 +0,0 @@ -""" -rxws: WebSocket methods implemented for OpenWebRX - - This file is part of OpenWebRX, - an open-source SDR receiver software with a web UI. - Copyright (c) 2013-2015 by Andras Retzler - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -""" - -import base64 -import sha -import select -import code - -class WebSocketException(Exception): - pass - -def handshake(myself): - my_client_id=myself.path[4:] - my_headers=myself.headers.items() - my_header_keys=map(lambda x:x[0],my_headers) - h_key_exists=lambda x:my_header_keys.count(x) - h_value=lambda x:my_headers[my_header_keys.index(x)][1] - #print "The Lambdas(tm)" - #print h_key_exists("upgrade") - #print h_value("upgrade") - #print h_key_exists("sec-websocket-key") - if (not h_key_exists("upgrade")) or not (h_value("upgrade")=="websocket") or (not h_key_exists("sec-websocket-key")): - raise WebSocketException - ws_key=h_value("sec-websocket-key") - ws_key_toreturn=base64.b64encode(sha.new(ws_key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest()) - #A sample list of keys we get: [('origin', 'http://localhost:8073'), ('upgrade', 'websocket'), ('sec-websocket-extensions', 'x-webkit-deflate-frame'), ('sec-websocket-version', '13'), ('host', 'localhost:8073'), ('sec-websocket-key', 't9J1rgy4fc9fg2Hshhnkmg=='), ('connection', 'Upgrade'), ('pragma', 'no-cache'), ('cache-control', 'no-cache')] - myself.wfile.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "+ws_key_toreturn+"\r\nCQ-CQ-de: HA5KFU\r\n\r\n") - -def get_header(size): - #this does something similar: https://github.com/lemmingzshadow/php-websocket/blob/master/server/lib/WebSocket/Connection.php - ws_first_byte=0b10000010 # FIN=1, OP=2 - if(size>125): - ws_second_byte=126 # The following two bytes will indicate frame size - extended_size=chr((size>>8)&0xff)+chr(size&0xff) #Okay, it uses reverse byte order (little-endian) compared to anything else sent on TCP - else: - ws_second_byte=size - #256 bytes binary message in a single unmasked frame | 0x82 0x7E 0x0100 [256 bytes of binary data] - extended_size="" - return chr(ws_first_byte)+chr(ws_second_byte)+extended_size - -def code_payload(data, masking_key=""): - # both encode or decode - if masking_key=="": - key = (61, 84, 35, 6) - else: - key = [ord(i) for i in masking_key] - encoded="" - for i in range(0,len(data)): - encoded+=chr(ord(data[i])^key[i%4]) - return encoded - -def xxdg(data): - output="" - for i in range(0,len(data)/8): - output+=xxd(data[i:i+8]) - if i%2: output+="\n" - else: output+=" " - return output - - -def xxd(data): - #diagnostic purposes only - output="" - for d in data: - output+=hex(ord(d))[2:].zfill(2)+" " - return output - -#for R/W the WebSocket, use recv/send -#for reading the TCP socket, use readsock -#for writing the TCP socket, use myself.wfile.write and flush - -def readsock(myself,size,blocking): - #http://thenestofheliopolis.blogspot.hu/2011/01/how-to-implement-non-blocking-two-way.html - if blocking: - return myself.rfile.read(size) - else: - poll = select.poll() - poll.register(myself.rfile.fileno(), select.POLLIN or select.POLLPRI) - fd = poll.poll(0) #timeout is 0 - if len(fd): - f = fd[0] - if f[1] > 0: - return myself.rfile.read(size) - return "" - - -def recv(myself, blocking=False, debug=False): - bufsize=70000 - #myself.connection.setblocking(blocking) #umm... we cannot do that with rfile - if debug: print "ws_recv begin" - try: - data=readsock(myself,6,blocking) - #print "rxws.recv bytes:",xxd(data) - except: - if debug: print "ws_recv error" - return "" - if debug: print "ws_recv recved" - if(len(data)==0): return "" - fin=ord(data[0])&128!=0 - is_text_frame=ord(data[0])&15==1 - length=ord(data[1])&0x7f - data+=readsock(myself,length,blocking) - #print "rxws.recv length is ",length," (multiple packets together?) len(data) =",len(data) - has_one_byte_length=length<125 - masked=ord(data[1])&0x80!=0 - #print "len=", length, len(data)-2 - #print "fin, is_text_frame, has_one_byte_length, masked = ", (fin, is_text_frame, has_one_byte_length, masked) - #print xxd(data) - if fin and is_text_frame and has_one_byte_length: - if masked: - return code_payload(data[6:], data[2:6]) - else: - return data[2:] - -#Useful links for ideas on WebSockets: -# http://stackoverflow.com/questions/8125507/how-can-i-send-and-receive-websocket-messages-on-the-server-side -# https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_server -# http://tools.ietf.org/html/rfc6455#section-5.2 - - -def flush(myself): - myself.wfile.flush() - #or the socket, not the rfile: - #lR,lW,lX = select.select([],[myself.connection,],[],60) - - -def send(myself, data, begin_id="", debug=0): - base_frame_size=35000 #could guess by MTU? - debug=0 - #try: - while True: - counter=0 - from_end=len(data)-counter - if from_end+len(begin_id)>base_frame_size: - data_to_send=begin_id+data[counter:counter+base_frame_size-len(begin_id)] - header=get_header(len(data_to_send)) - flush(myself) - myself.wfile.write(header+data_to_send) - flush(myself) - if debug: print "rxws.send ==================== #1 if branch :: from={0} to={1} dlen={2} hlen={3}".format(counter,counter+base_frame_size-len(begin_id),len(data_to_send),len(header)) - else: - data_to_send=begin_id+data[counter:] - header=get_header(len(data_to_send)) - flush(myself) - myself.wfile.write(header+data_to_send) - flush(myself) - if debug: print "rxws.send :: #2 else branch :: dlen={0} hlen={1}".format(len(data_to_send),len(header)) - #if debug: print "header:\n"+xxdg(header)+"\n\nws data:\n"+xxdg(data_to_send) - break - counter+=base_frame_size-len(begin_id) - #except: - # pass From 17a362fe7a9393333d864c7e66e7d66baa9ae3b9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 12 May 2019 17:23:03 +0200 Subject: [PATCH 0107/2616] no longer a template, no need for special file extension --- htdocs/{index.wrx => index.html} | 0 owrx/controllers.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename htdocs/{index.wrx => index.html} (100%) diff --git a/htdocs/index.wrx b/htdocs/index.html similarity index 100% rename from htdocs/index.wrx rename to htdocs/index.html diff --git a/owrx/controllers.py b/owrx/controllers.py index a41015ac1..9100de4d3 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -77,7 +77,7 @@ def handle_request(self): class IndexController(AssetsController): def handle_request(self): - self.serve_file("index.wrx", "text/html") + self.serve_file("index.html", content_type = "text/html") class WebSocketController(Controller): def handle_request(self): From a85a6c694ce008b37ab6750834b95cd78c3ccffc Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 12 May 2019 18:10:24 +0200 Subject: [PATCH 0108/2616] improve shutdown handling --- openwebrx.py | 8 ++++++-- owrx/connection.py | 10 ++++----- owrx/controllers.py | 4 ++-- owrx/sdrhu.py | 2 +- owrx/source.py | 50 +++++++++++++++++++++++++++------------------ owrx/websocket.py | 15 ++++++++++++++ 6 files changed, 58 insertions(+), 31 deletions(-) diff --git a/openwebrx.py b/openwebrx.py index e41d6c701..99b1419d8 100644 --- a/openwebrx.py +++ b/openwebrx.py @@ -2,7 +2,7 @@ from owrx.http import RequestHandler from owrx.config import PropertyManager from owrx.feature import FeatureDetector -from owrx.source import SdrService +from owrx.source import SdrService, ClientRegistry from socketserver import ThreadingMixIn from owrx.sdrhu import SdrHuUpdater @@ -45,4 +45,8 @@ def main(): if __name__ == "__main__": - main() + try: + main() + except KeyboardInterrupt: + for c in ClientRegistry.getSharedInstance().clients: + c.close() diff --git a/owrx/connection.py b/owrx/connection.py index a0442b821..95ce84f89 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -1,5 +1,5 @@ from owrx.config import PropertyManager -from owrx.source import DspManager, CpuUsageThread, SdrService, ClientReporterThread +from owrx.source import DspManager, CpuUsageThread, SdrService, ClientRegistry import json import logging @@ -14,7 +14,7 @@ class OpenWebRxClient(object): def __init__(self, conn): self.conn = conn - ClientReporterThread.getSharedInstance().addClient(self) + ClientRegistry.getSharedInstance().addClient(self) self.dsp = None self.sdr = None @@ -68,10 +68,8 @@ def startDsp(self): def close(self): self.stopDsp() CpuUsageThread.getSharedInstance().remove_client(self) - try: - ClientReporterThread.getSharedInstance().removeClient(self) - except ValueError: - pass + ClientRegistry.getSharedInstance().removeClient(self) + self.conn.close() logger.debug("connection closed") def stopDsp(self): diff --git a/owrx/controllers.py b/owrx/controllers.py index 9100de4d3..774ba9b4a 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -3,7 +3,7 @@ from datetime import datetime from owrx.websocket import WebSocketConnection from owrx.config import PropertyManager -from owrx.source import ClientReporterThread +from owrx.source import ClientRegistry from owrx.connection import WebSocketMessageHandler from owrx.version import openwebrx_version @@ -41,7 +41,7 @@ def handle_request(self): "status": "active", "name": pm["receiver_name"], "op_email": pm["receiver_admin"], - "users": ClientReporterThread.getSharedInstance().clientCount(), + "users": ClientRegistry.getSharedInstance().clientCount(), "users_max": pm["max_clients"], "gps": pm["receiver_gps"], "asl": pm["receiver_asl"], diff --git a/owrx/sdrhu.py b/owrx/sdrhu.py index b2c6b0ff5..5f0d7fb2f 100644 --- a/owrx/sdrhu.py +++ b/owrx/sdrhu.py @@ -10,7 +10,7 @@ class SdrHuUpdater(threading.Thread): def __init__(self): self.doRun = True - super().__init__() + super().__init__(daemon = True) def update(self): pm = PropertyManager.getSharedInstance() diff --git a/owrx/source.py b/owrx/source.py index c45d12f5a..3efc7d4a4 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -467,6 +467,8 @@ def run(self): c.write_cpu_usage(cpu_usage) time.sleep(3) logger.debug("cpu usage thread shut down") + if CpuUsageThread.sharedInstance == self: + CpuUsageThread.sharedInstance = None def get_cpu_usage(self): try: @@ -499,42 +501,49 @@ def remove_client(self, c): self.shutdown() def shutdown(self): - if self.doRun: - if CpuUsageThread.sharedInstance == self: - CpuUsageThread.sharedInstance = None - self.doRun = False + self.doRun = False + +class ClientReportingThread(threading.Thread): + def __init__(self, registry): + self.doRun = True + self.registry = registry + super().__init__() + def run(self): + while self.doRun: + self.registry.broadcast() + time.sleep(3) + def stop(self): + self.doRun = False class TooManyClientsException(Exception): pass -class ClientReporterThread(threading.Thread): +class ClientRegistry(object): sharedInstance = None @staticmethod def getSharedInstance(): - if ClientReporterThread.sharedInstance is None: - ClientReporterThread.sharedInstance = ClientReporterThread() - ClientReporterThread.sharedInstance.start() - ClientReporterThread.sharedInstance.doRun = True - return ClientReporterThread.sharedInstance + if ClientRegistry.sharedInstance is None: + ClientRegistry.sharedInstance = ClientRegistry() + return ClientRegistry.sharedInstance def __init__(self): - self.doRun = True self.clients = [] + self.reporter = None super().__init__() - def run(self): - while (self.doRun): - n = self.clientCount() - for c in self.clients: - c.write_clients(n) - time.sleep(3) - ClientReporterThread.sharedInstance = None + def broadcast(self): + n = self.clientCount() + for c in self.clients: + c.write_clients(n) def addClient(self, client): pm = PropertyManager.getSharedInstance() if len(self.clients) >= pm["max_clients"]: raise TooManyClientsException() self.clients.append(client) + if self.reporter is None: + self.reporter = ClientReportingThread(self) + self.reporter.start() def clientCount(self): return len(self.clients) @@ -544,5 +553,6 @@ def removeClient(self, client): self.clients.remove(client) except ValueError: pass - if not self.clients: - self.doRun = False \ No newline at end of file + if not self.clients and self.reporter is not None: + self.reporter.stop() + self.reporter = None diff --git a/owrx/websocket.py b/owrx/websocket.py index 538778288..d0385b885 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -72,5 +72,20 @@ def read_loop(self): else: logger.warning("unsupported opcode: {0}".format(opcode)) + def close(self): + try: + header = self.get_header(0, 8) + self.handler.wfile.write(header) + self.handler.wfile.flush() + except ValueError: + logger.exception("while writing close frame:") + + try: + self.handler.finish() + self.handler.connection.close() + except Exception: + logger.exception("while closing connection:") + + class WebSocketException(Exception): pass From 2408d77f1559221afb67ad9e21ec80a86adb86ec Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 13 May 2019 19:19:15 +0200 Subject: [PATCH 0109/2616] feature detection for digital voice; display modulator buttons only when available --- htdocs/index.html | 4 ++++ htdocs/openwebrx.js | 5 +++++ owrx/connection.py | 6 ++++++ owrx/feature.py | 47 +++++++++++++++++++++++++++++++++++++++------ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index a223eec4e..0fa04c4b7 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -91,12 +91,16 @@
CW
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index f5b7da8da..8610778ea 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1233,6 +1233,11 @@ function on_ws_recv(evt) return '"; }).join(""); break; + case "features": + for (var feature in json.value) { + $('[data-feature="' + feature + '"')[json.value[feature] ? "show" : "hide"](); + } + break; default: console.warn('received message of unknown type: ' + json.type); } diff --git a/owrx/connection.py b/owrx/connection.py index 95ce84f89..346f56dd6 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -1,5 +1,6 @@ from owrx.config import PropertyManager from owrx.source import DspManager, CpuUsageThread, SdrService, ClientRegistry +from owrx.feature import FeatureDetector import json import logging @@ -33,6 +34,9 @@ def __init__(self, conn): profiles = [{"name": s.getName() + " " + p["name"], "id":sid + "|" + pid} for (sid, s) in SdrService.getSources().items() for (pid, p) in s.getProfiles().items()] self.write_profiles(profiles) + features = FeatureDetector().feature_availability() + self.write_features(features) + CpuUsageThread.getSharedInstance().add_client(self) def sendConfig(self, key, value): @@ -121,6 +125,8 @@ def write_receiver_details(self, details): self.protected_send({"type":"receiver_details","value":details}) def write_profiles(self, profiles): self.protected_send({"type":"profiles","value":profiles}) + def write_features(self, features): + self.protected_send({"type":"features","value":features}) class WebSocketMessageHandler(object): def __init__(self): diff --git a/owrx/feature.py b/owrx/feature.py index 83f9232f2..457588bf8 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -1,4 +1,7 @@ import os +import subprocess +from functools import reduce +from operator import and_ import logging logger = logging.getLogger(__name__) @@ -12,9 +15,13 @@ class FeatureDetector(object): "core": [ "csdr", "nmux" ], "rtl_sdr": [ "rtl_sdr" ], "sdrplay": [ "rx_tools" ], - "hackrf": [ "hackrf_transfer" ] + "hackrf": [ "hackrf_transfer" ], + "digital_voice": [ "digiham" ] } + def feature_availability(self): + return {name: self.is_available(name) for name in FeatureDetector.features} + def is_available(self, feature): return self.has_requirements(self.get_requirements(feature)) @@ -34,17 +41,20 @@ def has_requirements(self, requirements): logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement)) return passed + def command_is_runnable(self, command): + return os.system("{0} 2>/dev/null >/dev/null".format(command)) != 32512 + def has_csdr(self): - return os.system("csdr 2> /dev/null") != 32512 + return self.command_is_runnable("csdr") def has_nmux(self): - return os.system("nmux --help 2> /dev/null") != 32512 + return self.command_is_runnable("nmux --help") def has_rtl_sdr(self): - return os.system("rtl_sdr --help 2> /dev/null") != 32512 + return self.command_is_runnable("rtl_sdr --help") def has_rx_tools(self): - return os.system("rx_sdr --help 2> /dev/null") != 32512 + return self.command_is_runnable("rx_sdr --help") """ To use a HackRF, compile the HackRF host tools from its "stdout" branch: @@ -62,4 +72,29 @@ def has_rx_tools(self): def has_hackrf_transfer(self): # TODO i don't have a hackrf, so somebody doublecheck this. # TODO also check if it has the stdout feature - return os.system("hackrf_transfer --help 2> /dev/null") != 32512 + return self.command_is_runnable("hackrf_transfer --help") + + def command_exists(self, command): + return os.system("which {0}".format(command)) == 0 + + def has_digiham(self): + # the digiham tools expect to be fed via stdin, they will block until their stdin is closed. + def check_with_stdin(command): + try: + process = subprocess.Popen(command, stdin=subprocess.PIPE) + process.communicate("") + return process.wait() == 0 + except FileNotFoundError: + return False + return reduce(and_, + map( + check_with_stdin, + ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer"] + ), + True) + + def has_dsd(self): + return self.command_is_runnable("dsd") + + def has_sox(self): + return self.command_is_runnable("sox") \ No newline at end of file From 2ddfa4d4f693158df2ff27eeef64e00aec0458aa Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 13 May 2019 19:27:25 +0200 Subject: [PATCH 0110/2616] add sox feature dependency --- owrx/feature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/feature.py b/owrx/feature.py index 457588bf8..74b59702a 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -16,7 +16,7 @@ class FeatureDetector(object): "rtl_sdr": [ "rtl_sdr" ], "sdrplay": [ "rx_tools" ], "hackrf": [ "hackrf_transfer" ], - "digital_voice": [ "digiham" ] + "digital_voice": [ "digiham", "sox" ] } def feature_availability(self): From 5733a5be9f3c1bd500f331fbf18984807928dad2 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 13 May 2019 22:45:19 +0200 Subject: [PATCH 0111/2616] separate dsd and digiham modes --- htdocs/index.html | 8 ++++---- owrx/feature.py | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index 0fa04c4b7..4a243821f 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -91,16 +91,16 @@
CW
diff --git a/owrx/feature.py b/owrx/feature.py index 74b59702a..dfc790181 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -16,7 +16,8 @@ class FeatureDetector(object): "rtl_sdr": [ "rtl_sdr" ], "sdrplay": [ "rx_tools" ], "hackrf": [ "hackrf_transfer" ], - "digital_voice": [ "digiham", "sox" ] + "digital_voice_digiham": [ "digiham", "sox" ], + "digital_voice_dsd": [ "dsd", "sox" ] } def feature_availability(self): From 9812d38eee3e1db4ba93095c179e415e27ab9f72 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 14 May 2019 23:30:03 +0200 Subject: [PATCH 0112/2616] refactor dsp outputs add digimode metadata --- csdr.py | 54 +++++++++++--------- htdocs/openwebrx.js | 52 +++++++++++++++++++ owrx/connection.py | 2 + owrx/source.py | 120 +++++++++++++++++++------------------------- 4 files changed, 134 insertions(+), 94 deletions(-) diff --git a/csdr.py b/csdr.py index 51d503464..c0ea54e46 100755 --- a/csdr.py +++ b/csdr.py @@ -25,13 +25,20 @@ import os import signal import threading +from functools import partial import logging logger = logging.getLogger(__name__) -class dsp: +class output(object): + def add_output(self, type, read_fn): + pass + def reset(self): + pass - def __init__(self): +class dsp(object): + + def __init__(self, output): self.samp_rate = 250000 self.output_rate = 11025 #this is default, and cannot be set at the moment self.fft_size = 1024 @@ -64,6 +71,7 @@ def __init__(self): self.secondary_pipe_names=["secondary_shift_pipe"] self.secondary_offset_freq = 1000 self.modification_lock = threading.Lock() + self.output = output def chain(self,which): if which in [ "dmr", "dstar", "nxdn", "ysf" ]: @@ -191,6 +199,9 @@ def start_secondary_demodulator(self): logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") #TODO digimodes self.secondary_processes_running = True + self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read()))) + self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) + #open control pipes for csdr and send initialization data if self.secondary_shift_pipe != None: #TODO digimodes self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes @@ -219,12 +230,6 @@ def stop_secondary_demodulator(self): pass self.secondary_processes_running = False - def read_secondary_demod(self, size): - return self.secondary_process_demod.stdout.read(size) - - def read_secondary_fft(self, size): - return self.secondary_process_fft.stdout.read(size) - def get_secondary_demodulator(self): return self.secondary_demodulator @@ -322,20 +327,6 @@ def set_squelch_level(self, squelch_level): self.squelch_pipe_file.flush() self.modification_lock.release() - def get_smeter_level(self): - if self.running: - line=self.smeter_pipe_file.readline() - try: - return float(line[:-1]) - except ValueError: - return 0 - else: - time.sleep(1) - - def get_metadata(self): - if self.running and self.meta_pipe: - return self.meta_pipe_file.readline() - def mkfifo(self,path): try: os.unlink(path) @@ -398,6 +389,8 @@ def watch_thread(): threading.Thread(target = watch_thread).start() + self.output.add_output("audio", partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256)) + # open control pipes for csdr if self.bpf_pipe != None: self.bpf_pipe_file=open(self.bpf_pipe,"w") @@ -419,11 +412,22 @@ def watch_thread(): self.set_bpf(self.low_cut, self.high_cut) if self.smeter_pipe: self.smeter_pipe_file=open(self.smeter_pipe,"r") + def read_smeter(): + raw = self.smeter_pipe_file.readline() + if len(raw) == 0: + return None + else: + return float(raw.rstrip("\n")) + self.output.add_output("smeter", read_smeter) if self.meta_pipe != None: self.meta_pipe_file=open(self.meta_pipe,"r") - - def read(self,size): - return self.process.stdout.read(size) + def read_meta(): + raw = self.meta_pipe_file.readline() + if len(raw) == 0: + return None + else: + return raw.rstrip("\n") + self.output.add_output("meta", read_meta) def stop(self): self.modification_lock.acquire() diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 8610778ea..0ef67bb84 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1238,6 +1238,9 @@ function on_ws_recv(evt) $('[data-feature="' + feature + '"')[json.value[feature] ? "show" : "hide"](); } break; + case "metadata": + update_metadata(json.value); + break; default: console.warn('received message of unknown type: ' + json.type); } @@ -1303,6 +1306,55 @@ function on_ws_recv(evt) } } +function update_metadata(stringData) { + var metaPanels = Array.prototype.filter.call(document.getElementsByClassName('openwebrx-panel'), function(el) { + return el.dataset.panelName === 'metadata'; + }); + + var meta = {}; + stringData.split(";").forEach(function(s) { + var item = s.split(":"); + meta[item[0]] = item[1]; + }); + + var update = function(el) { + el.innerHTML = ""; + }; + if (meta.protocol) switch (meta.protocol) { + case 'DMR': + if (meta.slot) { + var html = 'Timeslot: ' + meta.slot; + if (meta.type) html += ' Typ: ' + meta.type; + if (meta.source && meta.target) html += ' Source: ' + meta.source + ' Target: ' + meta.target; + update = function(el) { + var slotEl = el.getElementsByClassName('slot-' + meta.slot); + if (!slotEl.length) { + slotEl = document.createElement('div'); + slotEl.className = 'slot-' + meta.slot; + el.appendChild(slotEl); + } else { + slotEl = slotEl[0]; + } + slotEl.innerHTML = html; + }; + } + break; + case 'YSF': + var strings = []; + if (meta.source) strings.push("Source: " + meta.source); + if (meta.target) strings.push("Destination: " + meta.target); + if (meta.up) strings.push("Up: " + meta.up); + if (meta.down) strings.push("Down: " + meta.down); + var html = strings.join(' '); + update = function(el) { + el.innerHTML = html; + } + break; + } + + metaPanels.forEach(update); +} + function add_problem(what) { problems_span=e("openwebrx-problems"); diff --git a/owrx/connection.py b/owrx/connection.py index 346f56dd6..76a93a422 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -127,6 +127,8 @@ def write_profiles(self, profiles): self.protected_send({"type":"profiles","value":profiles}) def write_features(self, features): self.protected_send({"type":"features","value":features}) + def write_metadata(self, metadata): + self.protected_send({"type":"metadata","value":metadata}) class WebSocketMessageHandler(object): def __init__(self): diff --git a/owrx/source.py b/owrx/source.py index 3efc7d4a4..f2be0af2d 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -169,9 +169,6 @@ def wait_for_process_to_end(): self.monitor = threading.Thread(target = wait_for_process_to_end) self.monitor.start() - self.spectrumThread = SpectrumThread(self) - self.spectrumThread.start() - self.modificationLock.release() for c in self.clients: @@ -186,9 +183,6 @@ def stop(self): self.modificationLock.acquire() - if self.spectrumThread is not None: - self.spectrumThread.stop() - if self.process is not None: try: os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) @@ -216,12 +210,18 @@ def removeClient(self, c): def addSpectrumClient(self, c): self.spectrumClients.append(c) + if self.spectrumThread is None: + self.spectrumThread = SpectrumThread(self) + self.spectrumThread.start() def removeSpectrumClient(self, c): try: self.spectrumClients.remove(c) except ValueError: pass + if not self.spectrumClients and self.spectrumThread is not None: + self.spectrumThread.stop() + self.spectrumThread = None def writeSpectrumData(self, data): for c in self.spectrumClients: @@ -249,19 +249,18 @@ def __init__(self, props, port): def sleepOnRestart(self): time.sleep(1) -class SpectrumThread(threading.Thread): +class SpectrumThread(csdr.output): def __init__(self, sdrSource): - self.doRun = True self.sdrSource = sdrSource super().__init__() - def run(self): + def start(self): props = self.sdrSource.props.collect( "samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression", "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through" ).defaults(PropertyManager.getSharedInstance()) - self.dsp = dsp = csdr.dsp() + self.dsp = dsp = csdr.dsp(self) dsp.nc_port = self.sdrSource.getPort() dsp.set_demodulator("fft") props.getProperty("samp_rate").wire(dsp.set_samp_rate) @@ -288,25 +287,27 @@ def set_fft_averages(key, value): dsp.read(8) #dummy read to skip bufsize & preamble logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1") logger.debug("Spectrum thread started.") - bytes_to_read=int(dsp.get_fft_bytes_to_read()) - while self.doRun: - data=dsp.read(bytes_to_read) - if len(data) == 0: - time.sleep(1) - else: - self.sdrSource.writeSpectrumData(data) - dsp.stop() - logger.debug("spectrum thread shut down") + def add_output(self, type, read_fn): + if type != "audio": + logger.error("unsupported output type received by FFT: %s", type) + return - self.thread = None - self.sdrSource.removeClient(self) + def pipe(): + run = True + while run: + data = read_fn() + if len(data) == 0: + run = False + else: + self.sdrSource.writeSpectrumData(data) + + threading.Thread(target = pipe).start() def stop(self): - logger.debug("stopping spectrum thread") - self.doRun = False + self.dsp.stop() -class DspManager(object): +class DspManager(csdr.output): def __init__(self, handler, sdrSource): self.doRun = False self.handler = handler @@ -319,7 +320,7 @@ def __init__(self, handler, sdrSource): "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate" ).defaults(PropertyManager.getSharedInstance()) - self.dsp = csdr.dsp() + self.dsp = csdr.dsp(self) #dsp_initialized=False self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression) self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression) @@ -356,7 +357,6 @@ def set_high_cut(cut): def set_secondary_mod(mod): if mod == False: mod = None if self.dsp.get_secondary_demodulator() == mod: return - self.stopSecondaryThreads() self.dsp.stop() self.dsp.set_secondary_demodulator(mod) if mod is not None: @@ -367,9 +367,6 @@ def set_secondary_mod(mod): }) self.dsp.start() - if mod: - self.startSecondaryThreads() - self.localProps.getProperty("secondary_mod").wire(set_secondary_mod) self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq) @@ -380,47 +377,34 @@ def start(self): self.doRun = self.sdrSource.isAvailable() if self.doRun: self.dsp.start() - threading.Thread(target = self.readDspOutput).start() - threading.Thread(target = self.readSMeterOutput).start() - - def startSecondaryThreads(self): - self.runSecondary = True - self.secondaryDemodThread = threading.Thread(target = self.readSecondaryDemod) - self.secondaryDemodThread.start() - self.secondaryFftThread = threading.Thread(target = self.readSecondaryFft) - self.secondaryFftThread.start() - - def stopSecondaryThreads(self): - self.runSecondary = False - self.secondaryDemodThread = None - self.secondaryFftThread = None - - def readDspOutput(self): - while (self.doRun): - data = self.dsp.read(256) - if len(data) != 256: - time.sleep(1) - else: - self.handler.write_dsp_data(data) - - def readSMeterOutput(self): - while (self.doRun): - level = self.dsp.get_smeter_level() - self.handler.write_s_meter_level(level) - - def readSecondaryDemod(self): - while (self.runSecondary): - data = self.dsp.read_secondary_demod(1) - self.handler.write_secondary_demod(data) - - def readSecondaryFft(self): - while (self.runSecondary): - data = self.dsp.read_secondary_fft(int(self.dsp.get_secondary_fft_bytes_to_read())) - self.handler.write_secondary_fft(data) + + def add_output(self, t, read_fn): + logger.debug("adding new output of type %s", t) + writers = { + "audio": self.handler.write_dsp_data, + "smeter": self.handler.write_s_meter_level, + "secondary_fft": self.handler.write_secondary_fft, + "secondary_demod": self.handler.write_secondary_demod, + "meta": self.handler.write_metadata + } + write = writers[t] + + def pump(read, write): + def copy(): + run = True + while run: + data = read() + if data is None or (isinstance(data, bytes) and len(data) == 0): + logger.warning("zero read on {0}".format(t)) + run = False + else: + write(data) + return copy + + threading.Thread(target=pump(read_fn, write)).start() def stop(self): self.doRun = False - self.runSecondary = False self.dsp.stop() self.sdrSource.removeClient(self) @@ -433,8 +417,6 @@ def onSdrAvailable(self): self.doRun = True if self.dsp is not None: self.dsp.start() - threading.Thread(target = self.readDspOutput).start() - threading.Thread(target = self.readSMeterOutput).start() def onSdrUnavailable(self): logger.debug("received onSdrUnavailable, shutting down DspSource") From 5e67f036b4b0dd50f4a2b881c1998a32b5fbe3d8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 14 May 2019 23:36:37 +0200 Subject: [PATCH 0113/2616] fix demodulator buttons --- htdocs/openwebrx.js | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 0ef67bb84..dfdfafc3a 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -2526,29 +2526,27 @@ function progressbar_set(obj,val,text,over) function demodulator_buttons_update() { $(".openwebrx-demodulator-button").removeClass("highlighted"); - if(secondary_demod) $("#openwebrx-button-dig").addClass("highlighted"); - else switch(demodulators[0].subtype) - { - case "nfm": - $("#openwebrx-button-nfm").addClass("highlighted"); - break; - case "am": - $("#openwebrx-button-am").addClass("highlighted"); - break; - case "lsb": - case "usb": - case "cw": - if(demodulators[0].high_cut-demodulators[0].low_cut<300) - $("#openwebrx-button-cw").addClass("highlighted"); - else - { - if(demodulators[0].high_cut<0) - $("#openwebrx-button-lsb").addClass("highlighted"); - else if(demodulators[0].low_cut>0) - $("#openwebrx-button-usb").addClass("highlighted"); - else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted"); - } - break; + if(secondary_demod) { + $("#openwebrx-button-dig").addClass("highlighted"); + } else switch(demodulators[0].subtype) { + case "lsb": + case "usb": + case "cw": + if(demodulators[0].high_cut-demodulators[0].low_cut<300) + $("#openwebrx-button-cw").addClass("highlighted"); + else + { + if(demodulators[0].high_cut<0) + $("#openwebrx-button-lsb").addClass("highlighted"); + else if(demodulators[0].low_cut>0) + $("#openwebrx-button-usb").addClass("highlighted"); + else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted"); + } + break; + default: + var mod = demodulators[0].subtype; + $("#openwebrx-button-" + mod).addClass("highlighted"); + break; } } function demodulator_analog_replace_last() { demodulator_analog_replace(last_analog_demodulator_subtype); } From 03049b79dd4e2c95f0cdf7ec02d1653c7354f9b9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 15 May 2019 11:33:23 +0200 Subject: [PATCH 0114/2616] narrower bandwidth actually improves decoding --- htdocs/openwebrx.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index dfdfafc3a..979edf5c4 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -436,8 +436,8 @@ function demodulator_default_analog(offset_frequency,subtype) } else if(subtype=="dmr" || subtype=="ysf") { - this.low_cut=-6500; - this.high_cut=6500; + this.low_cut=-4000; + this.high_cut=4000; } else if(subtype=="dstar" || subtype=="nxdn") { From 117d0483f7549b2e844dec79daa01eb953ac21d8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 15 May 2019 11:44:03 +0200 Subject: [PATCH 0115/2616] streamline sdr and dsp integration --- owrx/source.py | 57 ++++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/owrx/source.py b/owrx/source.py index f2be0af2d..4aa9bba97 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -151,6 +151,14 @@ def start(self): self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp) logger.info("Started rtl source: " + cmd) + def wait_for_process_to_end(): + rc = self.process.wait() + logger.debug("shut down with RC={0}".format(rc)) + self.monitor = None + + self.monitor = threading.Thread(target = wait_for_process_to_end) + self.monitor.start() + while True: testsock = socket.socket() try: @@ -160,15 +168,6 @@ def start(self): except: time.sleep(0.1) - - def wait_for_process_to_end(): - rc = self.process.wait() - logger.debug("shut down with RC={0}".format(rc)) - self.monitor = None - - self.monitor = threading.Thread(target = wait_for_process_to_end) - self.monitor.start() - self.modificationLock.release() for c in self.clients: @@ -254,7 +253,6 @@ def __init__(self, sdrSource): self.sdrSource = sdrSource super().__init__() - def start(self): props = self.sdrSource.props.collect( "samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression", "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through" @@ -282,11 +280,17 @@ def set_fft_averages(key, value): dsp.csdr_print_bufsizes = props["csdr_print_bufsizes"] dsp.csdr_through = props["csdr_through"] logger.debug("Spectrum thread initialized successfully.") - dsp.start() - if props["csdr_dynamic_bufsize"]: - dsp.read(8) #dummy read to skip bufsize & preamble - logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1") - logger.debug("Spectrum thread started.") + + def start(self): + self.sdrSource.addClient(self) + if self.sdrSource.isAvailable(): + self.dsp.start() + # TODO this does not work any more + ''' + if props["csdr_dynamic_bufsize"]: + dsp.read(8) #dummy read to skip bufsize & preamble + logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1") + ''' def add_output(self, type, read_fn): if type != "audio": @@ -306,14 +310,17 @@ def pipe(): def stop(self): self.dsp.stop() + self.sdrSource.removeClient(self) + + def onSdrAvailable(self): + self.dsp.start() + def onSdrUnavailable(self): + self.dsp.stop() class DspManager(csdr.output): def __init__(self, handler, sdrSource): - self.doRun = False self.handler = handler self.sdrSource = sdrSource - self.dsp = None - self.sdrSource.addClient(self) self.localProps = self.sdrSource.getProps().collect( "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", @@ -371,11 +378,12 @@ def set_secondary_mod(mod): self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq) + self.sdrSource.addClient(self) + super().__init__() def start(self): - self.doRun = self.sdrSource.isAvailable() - if self.doRun: + if self.sdrSource.isAvailable(): self.dsp.start() def add_output(self, t, read_fn): @@ -404,7 +412,6 @@ def copy(): threading.Thread(target=pump(read_fn, write)).start() def stop(self): - self.doRun = False self.dsp.stop() self.sdrSource.removeClient(self) @@ -413,15 +420,11 @@ def setProperty(self, prop, value): def onSdrAvailable(self): logger.debug("received onSdrAvailable, attempting DspSource restart") - if not self.doRun: - self.doRun = True - if self.dsp is not None: - self.dsp.start() + self.dsp.start() def onSdrUnavailable(self): logger.debug("received onSdrUnavailable, shutting down DspSource") - if self.dsp is not None: - self.dsp.stop() + self.dsp.stop() class CpuUsageThread(threading.Thread): sharedInstance = None From cffb65e37df397f62ab2c60ccc1ca68f54d94b14 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 15 May 2019 19:43:52 +0200 Subject: [PATCH 0116/2616] cpu usage fix --- htdocs/openwebrx.js | 2 +- owrx/source.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 979edf5c4..e71d0fe5c 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1221,7 +1221,7 @@ function on_ws_recv(evt) break; case "cpuusage": var server_cpu_usage = json.value; - progressbar_set(e("openwebrx-bar-server-cpu"),server_cpu_usage/100,"Server CPU [" + server_cpu_usage + "%]",server_cpu_usage>85); + progressbar_set(e("openwebrx-bar-server-cpu"),server_cpu_usage,"Server CPU [" + Math.round(server_cpu_usage * 100) + "%]",server_cpu_usage>85); break; case "clients": var clients = json.value; diff --git a/owrx/source.py b/owrx/source.py index 4aa9bba97..b8e7d1208 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -452,8 +452,6 @@ def run(self): c.write_cpu_usage(cpu_usage) time.sleep(3) logger.debug("cpu usage thread shut down") - if CpuUsageThread.sharedInstance == self: - CpuUsageThread.sharedInstance = None def get_cpu_usage(self): try: @@ -486,6 +484,7 @@ def remove_client(self, c): self.shutdown() def shutdown(self): + CpuUsageThread.sharedInstance = None self.doRun = False class ClientReportingThread(threading.Thread): From 4496fcc8b0004ee7d2ba2cda53814593121f6ceb Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 15 May 2019 19:51:50 +0200 Subject: [PATCH 0117/2616] report client numbers on change only --- htdocs/openwebrx.js | 8 +++++--- owrx/source.py | 21 ++------------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index e71d0fe5c..fa6885965 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1157,6 +1157,7 @@ function audio_calculate_resampling(targetRate) debug_ws_data_received=0; max_clients_num=0; +clients_num = 0; var COMPRESS_FFT_PAD_N=10; //should be the same as in csdr.c @@ -1192,7 +1193,8 @@ function on_ws_recv(evt) fft_compression = config.fft_compression; divlog( "FFT stream is "+ ((fft_compression=="adpcm")?"compressed":"uncompressed")+"." ) max_clients_num = config.max_clients; - mathbox_waterfall_colors = config.mathbox_waterfall_colors; + progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num*0.85); + mathbox_waterfall_colors = config.mathbox_waterfall_colors; mathbox_waterfall_frequency_resolution = config.mathbox_waterfall_frequency_resolution; mathbox_waterfall_history_length = config.mathbox_waterfall_history_length; @@ -1224,8 +1226,8 @@ function on_ws_recv(evt) progressbar_set(e("openwebrx-bar-server-cpu"),server_cpu_usage,"Server CPU [" + Math.round(server_cpu_usage * 100) + "%]",server_cpu_usage>85); break; case "clients": - var clients = json.value; - progressbar_set(e("openwebrx-bar-clients"), clients / max_clients_num, "Clients [" + clients + "]", clients > max_clients_num*0.85); + client_num = json.value; + progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num*0.85); break; case "profiles": var listbox = e("openwebrx-sdr-profiles-listbox"); diff --git a/owrx/source.py b/owrx/source.py index b8e7d1208..0cbc33f05 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -487,18 +487,6 @@ def shutdown(self): CpuUsageThread.sharedInstance = None self.doRun = False -class ClientReportingThread(threading.Thread): - def __init__(self, registry): - self.doRun = True - self.registry = registry - super().__init__() - def run(self): - while self.doRun: - self.registry.broadcast() - time.sleep(3) - def stop(self): - self.doRun = False - class TooManyClientsException(Exception): pass @@ -512,7 +500,6 @@ def getSharedInstance(): def __init__(self): self.clients = [] - self.reporter = None super().__init__() def broadcast(self): @@ -525,9 +512,7 @@ def addClient(self, client): if len(self.clients) >= pm["max_clients"]: raise TooManyClientsException() self.clients.append(client) - if self.reporter is None: - self.reporter = ClientReportingThread(self) - self.reporter.start() + self.broadcast() def clientCount(self): return len(self.clients) @@ -537,6 +522,4 @@ def removeClient(self, client): self.clients.remove(client) except ValueError: pass - if not self.clients and self.reporter is not None: - self.reporter.stop() - self.reporter = None + self.broadcast() \ No newline at end of file From b1596cbb60a7e806df85e21b1e32060f15e9be84 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 15 May 2019 23:08:55 +0200 Subject: [PATCH 0118/2616] clean up chains --- csdr.py | 94 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/csdr.py b/csdr.py index c0ea54e46..969693b86 100755 --- a/csdr.py +++ b/csdr.py @@ -74,56 +74,51 @@ def __init__(self, output): self.output = output def chain(self,which): - if which in [ "dmr", "dstar", "nxdn", "ysf" ]: - self.set_output_rate(48000) - else: - self.set_output_rate(11025) - any_chain_base="nc -v 127.0.0.1 {nc_port} | " - if self.csdr_dynamic_bufsize: any_chain_base+="csdr setbuf {start_bufsize} | " - if self.csdr_through: any_chain_base+="csdr through | " + chain ="nc -v 127.0.0.1 {nc_port} | " + if self.csdr_dynamic_bufsize: chain += "csdr setbuf {start_bufsize} | " + if self.csdr_through: chain +="csdr through | " if which == "fft": - fft_chain_base = any_chain_base+"csdr fft_cc {fft_size} {fft_block_size} | " + \ + chain += "csdr fft_cc {fft_size} {fft_block_size} | " + \ ("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \ "csdr fft_exchange_sides_ff {fft_size}" if self.fft_compression=="adpcm": - return fft_chain_base+" | csdr compress_fft_adpcm_f_u8 {fft_size}" - else: - return fft_chain_base - chain_begin=any_chain_base+"csdr shift_addition_cc --fifo {shift_pipe} | csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | " + chain += " | csdr compress_fft_adpcm_f_u8 {fft_size}" + return chain + chain += "csdr shift_addition_cc --fifo {shift_pipe} | " + if which in ["dstar", "nxdn", "dmr", "ysf"]: + chain += "csdr fir_decimate_cc {digital_decimation} {ddc_transition_bw} HAMMING | " + else: + chain += "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | " + chain += "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | " if self.secondary_demodulator: - chain_begin+="csdr tee {iqtee_pipe} | " - chain_begin+="csdr tee {iqtee2_pipe} | " - chain_end = "" - if self.audio_compression=="adpcm": - chain_end = " | csdr encode_ima_adpcm_i16_u8" - if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr convert_f_s16"+chain_end - if which in [ "dstar", "nxdn" ]: - c = chain_begin - c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" + chain += "csdr tee {iqtee_pipe} | " + chain += "csdr tee {iqtee2_pipe} | " + if which == "nfm": + chain += "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff {output_rate} | csdr convert_f_s16" + elif which in [ "dstar", "nxdn" ]: + chain += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {digital_last_decimation} | csdr convert_f_s16" if which == "dstar": - c += " | dsd -fd" + chain += " | dsd -fd" elif which == "nxdn": - c += " | dsd -fi" - c += " -i - -o - -u 2 -g 10" - c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --input-buffer 160 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 220" - c += chain_end - return c + chain += " | dsd -fi" + chain += " -i - -o - -u 2 -g 10" + chain += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --input-buffer 160 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - | csdr setbuf 220" elif which == "dmr": - c = chain_begin - c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" - c += " | rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer" - c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256" - c += chain_end - return c + chain += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {digital_last_decimation} | csdr convert_f_s16" + chain += " | rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer" + chain += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - | csdr setbuf 256" elif which == "ysf": - c = chain_begin - c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" - c += " | rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y" - c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256" - c += chain_end - return c - elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end - elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end + chain += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {digital_last_decimation} | csdr convert_f_s16" + chain += " | rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y" + chain += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - | csdr setbuf 256" + elif which == "am": + chain += "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16" + elif which == "ssb": + chain += "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16" + + if self.audio_compression=="adpcm": + chain += " | csdr encode_ima_adpcm_i16_u8" + return chain def secondary_chain(self, which): secondary_chain_base="cat {input_pipe} | " @@ -257,10 +252,14 @@ def set_samp_rate(self,samp_rate): if self.running: self.restart() def calculate_decimation(self): - self.decimation=1 - while self.samp_rate/(self.decimation+1)>=self.output_rate: - self.decimation+=1 - self.last_decimation=float(self.if_samp_rate())/self.output_rate + (self.decimation, self.last_decimation) = self.get_decimation(self.output_rate) + + def get_decimation(self, output_rate): + decimation=1 + while self.samp_rate/ (decimation+1) >= output_rate: + decimation += 1 + last_decimation = float(self.samp_rate / decimation) / output_rate + return (decimation, last_decimation) def if_samp_rate(self): return self.samp_rate/self.decimation @@ -367,12 +366,15 @@ def start(self): self.try_create_pipes(self.pipe_names, command_base) + (digital_decimation, digital_last_decimation) = self.get_decimation(48000) + #run the command command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, - squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe ) + squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, + output_rate = self.get_output_rate(), digital_decimation = digital_decimation, digital_last_decimation = digital_last_decimation) logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) my_env=os.environ.copy() From a6c845de16725b43f8c49500f20400c04f48c63b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 16 May 2019 21:26:31 +0200 Subject: [PATCH 0119/2616] demodulator chain optimizations --- csdr.py | 80 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/csdr.py b/csdr.py index 969693b86..915e19fd7 100755 --- a/csdr.py +++ b/csdr.py @@ -68,7 +68,7 @@ def __init__(self, output): self.secondary_process_fft = None self.secondary_process_demod = None self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", "iqtee2_pipe"] - self.secondary_pipe_names=["secondary_shift_pipe"] + self.secondary_pipe_names=["secondary_shift_dpipe"] self.secondary_offset_freq = 1000 self.modification_lock = threading.Lock() self.output = output @@ -85,36 +85,42 @@ def chain(self,which): chain += " | csdr compress_fft_adpcm_f_u8 {fft_size}" return chain chain += "csdr shift_addition_cc --fifo {shift_pipe} | " - if which in ["dstar", "nxdn", "dmr", "ysf"]: - chain += "csdr fir_decimate_cc {digital_decimation} {ddc_transition_bw} HAMMING | " - else: - chain += "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | " + chain += "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | " chain += "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | " if self.secondary_demodulator: chain += "csdr tee {iqtee_pipe} | " chain += "csdr tee {iqtee2_pipe} | " + # safe some cpu cycles... no need to decimate if decimation factor is 1 + last_decimation_block = "csdr old_fractional_decimator_ff {last_decimation} | " if self.last_decimation != 1.0 else "" if which == "nfm": - chain += "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff {output_rate} | csdr convert_f_s16" - elif which in [ "dstar", "nxdn" ]: - chain += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {digital_last_decimation} | csdr convert_f_s16" - if which == "dstar": - chain += " | dsd -fd" - elif which == "nxdn": - chain += " | dsd -fi" - chain += " -i - -o - -u 2 -g 10" - chain += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --input-buffer 160 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - | csdr setbuf 220" - elif which == "dmr": - chain += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {digital_last_decimation} | csdr convert_f_s16" - chain += " | rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer" - chain += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - | csdr setbuf 256" - elif which == "ysf": - chain += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {digital_last_decimation} | csdr convert_f_s16" - chain += " | rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y" - chain += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - | csdr setbuf 256" + chain += "csdr fmdemod_quadri_cf | csdr limit_ff | " + chain += last_decimation_block + chain += "csdr deemphasis_nfm_ff {output_rate} | csdr convert_f_s16" + elif self.isDigitalVoice(which): + chain += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | " + chain += last_decimation_block + chain += "csdr convert_f_s16 | " + if which in [ "dstar", "nxdn" ]: + if which == "dstar": + chain += "dsd -fd" + elif which == "nxdn": + chain += "dsd -fi" + chain += " -i - -o - -u 2 -g 10 | " + chain += "sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --input-buffer 160 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - | csdr setbuf 220" + elif which == "dmr": + chain += "rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer | " + chain += "sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - | csdr setbuf 256" + elif which == "ysf": + chain += "rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y | " + chain += "sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - | csdr setbuf 256" elif which == "am": - chain += "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16" + chain += "csdr amdemod_cf | csdr fastdcblock_ff | " + chain += last_decimation_block + chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" elif which == "ssb": - chain += "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16" + chain += "csdr realpart_cf | " + chain += last_decimation_block + chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" if self.audio_compression=="adpcm": chain += " | csdr encode_ima_adpcm_i16_u8" @@ -252,14 +258,15 @@ def set_samp_rate(self,samp_rate): if self.running: self.restart() def calculate_decimation(self): - (self.decimation, self.last_decimation) = self.get_decimation(self.output_rate) + (self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate()) - def get_decimation(self, output_rate): + def get_decimation(self, input_rate, output_rate): decimation=1 - while self.samp_rate/ (decimation+1) >= output_rate: + while input_rate / (decimation+1) >= output_rate: decimation += 1 - last_decimation = float(self.samp_rate / decimation) / output_rate - return (decimation, last_decimation) + fraction = float(input_rate / decimation) / output_rate + intermediate_rate = input_rate / decimation + return (decimation, fraction, intermediate_rate) def if_samp_rate(self): return self.samp_rate/self.decimation @@ -270,6 +277,16 @@ def get_name(self): def get_output_rate(self): return self.output_rate + def get_audio_rate(self): + if self.isDigitalVoice(): + return 48000 + return self.get_output_rate() + + def isDigitalVoice(self, demodulator = None): + if demodulator is None: + demodulator = self.get_demodulator() + return demodulator in ["dmr", "dstar", "nxdn", "ysf"] + def set_output_rate(self,output_rate): self.output_rate=output_rate self.calculate_decimation() @@ -277,6 +294,7 @@ def set_output_rate(self,output_rate): def set_demodulator(self,demodulator): if (self.demodulator == demodulator): return self.demodulator=demodulator + self.calculate_decimation() self.restart() def get_demodulator(self): @@ -366,15 +384,13 @@ def start(self): self.try_create_pipes(self.pipe_names, command_base) - (digital_decimation, digital_last_decimation) = self.get_decimation(48000) - #run the command command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, - output_rate = self.get_output_rate(), digital_decimation = digital_decimation, digital_last_decimation = digital_last_decimation) + output_rate = self.get_output_rate()) logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) my_env=os.environ.copy() From 3f7ba343a2d27874be161ac8e1db993c49923d8e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 16 May 2019 21:34:08 +0200 Subject: [PATCH 0120/2616] remove stray character --- csdr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csdr.py b/csdr.py index 915e19fd7..e9c45fa08 100755 --- a/csdr.py +++ b/csdr.py @@ -68,7 +68,7 @@ def __init__(self, output): self.secondary_process_fft = None self.secondary_process_demod = None self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", "iqtee2_pipe"] - self.secondary_pipe_names=["secondary_shift_dpipe"] + self.secondary_pipe_names=["secondary_shift_pipe"] self.secondary_offset_freq = 1000 self.modification_lock = threading.Lock() self.output = output From 35757168d44d3fc6bb3947cb1f4ee8ed13c71497 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 16 May 2019 21:44:05 +0200 Subject: [PATCH 0121/2616] add 30m --- config_webrx.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config_webrx.py b/config_webrx.py index 3bd92c60d..f156d0ffa 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -135,6 +135,14 @@ "start_freq": 14070000, "start_mod": "usb" }, + "30m": { + "name":"30m", + "center_freq": 10125000, + "rf_gain": 40, + "samp_rate": 250000, + "start_freq": 10142000, + "start_mod": "usb" + }, "40m": { "name":"40m", "center_freq": 7100000, From 9e0c2580d2db416062144cb71a549f35299f0d97 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 16 May 2019 22:36:37 +0200 Subject: [PATCH 0122/2616] more chain magic; no squelch on digital modes; remove experimental buffer configs --- csdr.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/csdr.py b/csdr.py index e9c45fa08..981cb8e23 100755 --- a/csdr.py +++ b/csdr.py @@ -106,13 +106,11 @@ def chain(self,which): elif which == "nxdn": chain += "dsd -fi" chain += " -i - -o - -u 2 -g 10 | " - chain += "sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --input-buffer 160 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - | csdr setbuf 220" elif which == "dmr": chain += "rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer | " - chain += "sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - | csdr setbuf 256" elif which == "ysf": chain += "rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y | " - chain += "sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - | csdr setbuf 256" + chain += "sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " elif which == "am": chain += "csdr amdemod_cf | csdr fastdcblock_ff | " chain += last_decimation_block @@ -338,9 +336,11 @@ def get_bpf(self): def set_squelch_level(self, squelch_level): self.squelch_level=squelch_level + #no squelch required on digital voice modes + actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level if self.running: self.modification_lock.acquire() - self.squelch_pipe_file.write( "%g\n"%(float(self.squelch_level)) ) + self.squelch_pipe_file.write( "%g\n"%(float(actual_squelch)) ) self.squelch_pipe_file.flush() self.modification_lock.release() From bd27d915298965bebb100d76a6d662235469c227 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 16 May 2019 22:39:50 +0200 Subject: [PATCH 0123/2616] resolve todo --- owrx/source.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/owrx/source.py b/owrx/source.py index 0cbc33f05..c5003d4cb 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -253,7 +253,7 @@ def __init__(self, sdrSource): self.sdrSource = sdrSource super().__init__() - props = self.sdrSource.props.collect( + self.props = props = self.sdrSource.props.collect( "samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression", "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through" ).defaults(PropertyManager.getSharedInstance()) @@ -285,18 +285,16 @@ def start(self): self.sdrSource.addClient(self) if self.sdrSource.isAvailable(): self.dsp.start() - # TODO this does not work any more - ''' - if props["csdr_dynamic_bufsize"]: - dsp.read(8) #dummy read to skip bufsize & preamble - logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1") - ''' def add_output(self, type, read_fn): if type != "audio": logger.error("unsupported output type received by FFT: %s", type) return + if self.props["csdr_dynamic_bufsize"]: + read_fn(8) #dummy read to skip bufsize & preamble + logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1") + def pipe(): run = True while run: From 7d4111fec897c540274ed4c0564e6f4751fa4cb3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 16 May 2019 23:09:57 +0200 Subject: [PATCH 0124/2616] hide metadata panel if no metadata is available --- htdocs/index.html | 2 +- htdocs/openwebrx.js | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index 4a243821f..5a8ed943b 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -168,7 +168,7 @@
-
+
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index fa6885965..38f098e8d 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -619,6 +619,7 @@ function demodulator_analog_replace(subtype, for_digital) } demodulator_add(new demodulator_default_analog(temp_offset,subtype)); demodulator_buttons_update(); + clear_metadata(); } function demodulator_set_offset_frequency(which,to_what) @@ -1309,17 +1310,13 @@ function on_ws_recv(evt) } function update_metadata(stringData) { - var metaPanels = Array.prototype.filter.call(document.getElementsByClassName('openwebrx-panel'), function(el) { - return el.dataset.panelName === 'metadata'; - }); - var meta = {}; stringData.split(";").forEach(function(s) { var item = s.split(":"); meta[item[0]] = item[1]; }); - var update = function(el) { + var update = function(_, el) { el.innerHTML = ""; }; if (meta.protocol) switch (meta.protocol) { @@ -1328,7 +1325,7 @@ function update_metadata(stringData) { var html = 'Timeslot: ' + meta.slot; if (meta.type) html += ' Typ: ' + meta.type; if (meta.source && meta.target) html += ' Source: ' + meta.source + ' Target: ' + meta.target; - update = function(el) { + update = function(_, el) { var slotEl = el.getElementsByClassName('slot-' + meta.slot); if (!slotEl.length) { slotEl = document.createElement('div'); @@ -1348,13 +1345,18 @@ function update_metadata(stringData) { if (meta.up) strings.push("Up: " + meta.up); if (meta.down) strings.push("Down: " + meta.down); var html = strings.join(' '); - update = function(el) { + update = function(_, el) { el.innerHTML = html; } break; } - metaPanels.forEach(update); + $('.openwebrx-panel[data-panel-name="metadata"]').each(update); + toggle_panel("openwebrx-panel-metadata", true); +} + +function clear_metadata() { + toggle_panel("openwebrx-panel-metadata", false); } function add_problem(what) @@ -2301,6 +2303,7 @@ function openwebrx_init() init_rx_photo(); open_websocket(); secondary_demod_init(); + clear_metadata(); place_panels(first_show_panel); window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); window.addEventListener("resize",openwebrx_resize); From 8e195a0de98bece403ec56b176bc7e17fe27a05b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 16 May 2019 23:14:23 +0200 Subject: [PATCH 0125/2616] under construction on top looks nicer --- htdocs/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/index.html b/htdocs/index.html index 5a8ed943b..da3c800e5 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -153,7 +153,7 @@
Server CPU [0%]
Clients [1]
-
+
Under construction
We're working on the code right now, so the application might fail.
From 0ab14f63cb7dbe2f7634ffa57bc545623ef379ba Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 16 May 2019 23:45:24 +0200 Subject: [PATCH 0126/2616] add new logo --- htdocs/gfx/openwebrx-avatar.png | Bin 2721 -> 12935 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/htdocs/gfx/openwebrx-avatar.png b/htdocs/gfx/openwebrx-avatar.png index 7e9736fdbfdd8272afcd27ef3b5819fd8df7d99f..2ae083fc25a91d78dfa5d64e9123fdae530b6306 100644 GIT binary patch literal 12935 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 literal 2721 zcmcguc|4SB8-8XIvNe|MF-BR+mI{+?Vwk~~H0Bh_c91Mlrjwmf2QjiGzLQ-f$(Ail zs5t6aOC@A0jLuAskY&W2*P!$DZNKlY@BRJW-@Dw;{an{`-PiNJcZ`)Kj-N-82LQlt zhQ|^BfI+t~z|9F=&iLIu3tc!!MrK5A=m_Cf}5JJpL_bWo+JP~GG^GL#DI)XLnB_qj}pzFX&vJJ z)~EeTN~V1hYg}_1YRXmT^UAH#UaGDJPP}%QHnjTridd^|aD~Qa91@YDbVXDd+raOj z?*^@S> zEBz>ok@^NnFdJxhhvn>K1J6Y1gHSdNvUbMySJ7+=`N{LhZUM5vH%2&)%%7^p?8$$xH2+W9jh5Zx$ToxA?34oZreHBAI9p&TppctgDr{0hR~8~=ZjW95V>mNlmBH}7q43yT z1vbi0*|w#-H1bun3qbGEO=77L^AWEvm&nKLiS=xa&tv*F1&j>*y7c}?l97mhQ^=RW z^6Oyy)%g455LSEcop-04(MqWOc?8y{R!{0=8=BP=J-cGTN;PNXOSchXGKu#}7c}Dw zq;;1W!6T{aVFNExx_Qe?0kT3^@M5+0yV-)$HC@GrtbEjBHc@EA-KXdl^F!W69nSx4 z2mjaY{r?YQEYF9!T>Uzm=^H7tO*I~j6Hm(}ld`GmO+oIYsT0%~tm|TYt#LxcE^bVG zwHUAFj9Fm_)5i-tdR*4q>zD3=9}wi`3=*M;_=ei}$R;tuK6BP}AE| zm&NHJulI*+OaZ+3Wbw=1-W%c(+LZ0fv{K9E&fp{Rf*tYzy>O5F!nm|86p`A!`(|TfV|`!6viyE{PIF5OcBp*^ zcz^=26>!W+4$Og?a90x`LfQi)1VO6}9J!ypMMYe=0F=+!5V~^tsezUSfCfpnpcM-( zGn3oX)U~G#Bo~tAa?hx>cXa%1>VTaj>>ODqexfBtn15)=#EzwcsXLQODB5FfZJmqr zu;pm3graXNT>QTL{yJ(TEpl)3{Cri4dBNduNuhoONXXI;R=~weC(--Lv?td1yjdue zrna`Cy}iArfq_g=N~$r?v$eBJ9v*h63!FRKJKbvzu-Hi&t)RHrLPjqD0Sz$%dUuKY zprXnf8yg>cVg-2rcT(wVb7eLosSC08b~wJUkpz=TC1fMG&H2U3onoX^^P7R-tIK~eifBb{}cV> z)Crwim8&{`;bPHG!OB%3ETjzK2gXz?XS16#|+1Y7UMEtE)S(y$)6s>wx-^L~U zR_)bil#nL`!#kfnt8mIN<;jgJZ(1&@v?a)W>&{Ml{P=N?%eW-WDy|ZgrP9bW7$R_3c{HE@(J_ER`e9c96&iB$91CF7kbD@{iuEXR$!Z6eR9q|H)!6^0D)DcsHVJ z>(PIntC&BRtC{XaLG`~oDWX`d$bG$_M&+9QrfinEHC{ahs^VopV8n~bx512`BG{TJm^_1$+Ym2fmu2E`4JWv{lYDQ1AY{!WvF2PM*l;Pwe=BZaV5D( VS=<_r)`!|jU}j>8Ej4nw@+Xs9HFy93 From ff8f03c983ed684f8f201d0babffc0f224042769 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 17 May 2019 20:57:55 +0200 Subject: [PATCH 0127/2616] slow down the smeter refresh rate a bit --- csdr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/csdr.py b/csdr.py index 981cb8e23..77b017b0d 100755 --- a/csdr.py +++ b/csdr.py @@ -86,7 +86,7 @@ def chain(self,which): return chain chain += "csdr shift_addition_cc --fifo {shift_pipe} | " chain += "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | " - chain += "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | " + chain += "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every} | " if self.secondary_demodulator: chain += "csdr tee {iqtee_pipe} | " chain += "csdr tee {iqtee2_pipe} | " @@ -390,7 +390,7 @@ def start(self): bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, - output_rate = self.get_output_rate()) + output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000) ) logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) my_env=os.environ.copy() From e6150e4aca5a5a3fecf416820d6e70c2e6443ed4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 18 May 2019 21:38:15 +0200 Subject: [PATCH 0128/2616] introduce subscription concept to simplify unsubscribing from events --- csdr.py | 3 ++ owrx/config.py | 55 ++++++++++++++++++++++++++++--------- owrx/connection.py | 27 ++++++++++-------- owrx/source.py | 68 +++++++++++++++++++++++++--------------------- 4 files changed, 98 insertions(+), 55 deletions(-) diff --git a/csdr.py b/csdr.py index 77b017b0d..6ce0ea83f 100755 --- a/csdr.py +++ b/csdr.py @@ -137,7 +137,10 @@ def secondary_chain(self, which): "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" def set_secondary_demodulator(self, what): + if self.get_secondary_demodulator() == what: + return self.secondary_demodulator = what + self.restart() def secondary_fft_block_size(self): return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here diff --git a/owrx/config.py b/owrx/config.py index 8fb65132c..f3608ceb6 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -2,25 +2,49 @@ logger = logging.getLogger(__name__) +class Subscription(object): + def __init__(self, subscriptee, subscriber): + self.subscriptee = subscriptee + self.subscriber = subscriber + + def call(self, *args, **kwargs): + self.subscriber(*args, **kwargs) + + def cancel(self): + self.subscriptee.unwire(self) + + class Property(object): def __init__(self, value = None): self.value = value - self.callbacks = [] + self.subscribers = [] + def getValue(self): return self.value + def setValue(self, value): if (self.value == value): return self self.value = value - for c in self.callbacks: + for c in self.subscribers: try: - c(self.value) + c.call(self.value) except Exception as e: logger.exception(e) return self + def wire(self, callback): - self.callbacks.append(callback) - if not self.value is None: callback(self.value) + sub = Subscription(self, callback) + self.subscribers.append(sub) + if not self.value is None: sub.call(self.value) + return sub + + def unwire(self, sub): + try: + self.subscribers.remove(sub) + except ValueError: + # happens when already removed before + pass return self class PropertyManager(object): @@ -36,7 +60,7 @@ def collect(self, *props): def __init__(self, properties = None): self.properties = {} - self.callbacks = [] + self.subscribers = [] if properties is not None: for (name, prop) in properties.items(): self.add(name, prop) @@ -44,9 +68,9 @@ def __init__(self, properties = None): def add(self, name, prop): self.properties[name] = prop def fireCallbacks(value): - for c in self.callbacks: + for c in self.subscribers: try: - c(name, value) + c.call(name, value) except Exception as e: logger.exception(e) prop.wire(fireCallbacks) @@ -78,11 +102,16 @@ def getPropertyValue(self, name): return self.getProperty(name).getValue() def wire(self, callback): - self.callbacks.append(callback) - return self - - def unwire(self, callback): - self.callbacks.remove(callback) + sub = Subscription(self, callback) + self.subscribers.append(sub) + return sub + + def unwire(self, sub): + try: + self.subscribers.remove(sub) + except ValueError: + # happens when already removed before + pass return self def defaults(self, other_pm): diff --git a/owrx/connection.py b/owrx/connection.py index 76a93a422..2a301d7fa 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -19,7 +19,7 @@ def __init__(self, conn): self.dsp = None self.sdr = None - self.configProps = None + self.configSub = None pm = PropertyManager.getSharedInstance() @@ -39,11 +39,6 @@ def __init__(self, conn): CpuUsageThread.getSharedInstance().add_client(self) - def sendConfig(self, key, value): - config = dict((key, self.configProps[key]) for key in OpenWebRxClient.config_keys) - # TODO mathematical properties? hmmmm - config["start_offset_freq"] = self.configProps["start_freq"] - self.configProps["center_freq"] - self.write_config(config) def setSdr(self, id = None): next = SdrService.getSource(id) if (next == self.sdr): @@ -51,16 +46,23 @@ def setSdr(self, id = None): self.stopDsp() - if self.configProps is not None: - self.configProps.unwire(self.sendConfig) + if self.configSub is not None: + self.configSub.cancel() + self.configSub = None self.sdr = next # send initial config - self.configProps = self.sdr.getProps().collect(*OpenWebRxClient.config_keys).defaults(PropertyManager.getSharedInstance()) + configProps = self.sdr.getProps().collect(*OpenWebRxClient.config_keys).defaults(PropertyManager.getSharedInstance()) - self.configProps.wire(self.sendConfig) - self.sendConfig(None, None) + def sendConfig(key, value): + config = dict((key, configProps[key]) for key in OpenWebRxClient.config_keys) + # TODO mathematical properties? hmmmm + config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"] + self.write_config(config) + + self.configSub = configProps.wire(sendConfig) + sendConfig(None, None) self.sdr.addSpectrumClient(self) @@ -73,6 +75,9 @@ def close(self): self.stopDsp() CpuUsageThread.getSharedInstance().remove_client(self) ClientRegistry.getSharedInstance().removeClient(self) + if self.configSub is not None: + self.configSub.cancel() + self.configSub = None self.conn.close() logger.debug("connection closed") diff --git a/owrx/source.py b/owrx/source.py index c5003d4cb..b5264b00a 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -261,10 +261,6 @@ def __init__(self, sdrSource): self.dsp = dsp = csdr.dsp(self) dsp.nc_port = self.sdrSource.getPort() dsp.set_demodulator("fft") - props.getProperty("samp_rate").wire(dsp.set_samp_rate) - props.getProperty("fft_size").wire(dsp.set_fft_size) - props.getProperty("fft_fps").wire(dsp.set_fft_fps) - props.getProperty("fft_compression").wire(dsp.set_fft_compression) def set_fft_averages(key, value): samp_rate = props["samp_rate"] @@ -273,7 +269,15 @@ def set_fft_averages(key, value): fft_voverlap_factor = props["fft_voverlap_factor"] dsp.set_fft_averages(int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor))) if fft_voverlap_factor>0 else 0) - props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages) + + self.subscriptions = [ + props.getProperty("samp_rate").wire(dsp.set_samp_rate), + props.getProperty("fft_size").wire(dsp.set_fft_size), + props.getProperty("fft_fps").wire(dsp.set_fft_fps), + props.getProperty("fft_compression").wire(dsp.set_fft_compression), + props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages) + ] + set_fft_averages(None, None) dsp.csdr_dynamic_bufsize = props["csdr_dynamic_bufsize"] @@ -309,6 +313,9 @@ def pipe(): def stop(self): self.dsp.stop() self.sdrSource.removeClient(self) + for c in self.subscriptions: + c.cancel() + self.subscriptions = [] def onSdrAvailable(self): self.dsp.start() @@ -326,43 +333,40 @@ def __init__(self, handler, sdrSource): ).defaults(PropertyManager.getSharedInstance()) self.dsp = csdr.dsp(self) - #dsp_initialized=False - self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression) - self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression) - self.dsp.set_offset_freq(0) - self.dsp.set_bpf(-4000,4000) - self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size) - self.dsp.nc_port = self.sdrSource.getPort() - self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"] - self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"] - self.dsp.csdr_through = self.localProps["csdr_through"] - - self.localProps.getProperty("samp_rate").wire(self.dsp.set_samp_rate) - - self.localProps.getProperty("output_rate").wire(self.dsp.set_output_rate) - self.localProps.getProperty("offset_freq").wire(self.dsp.set_offset_freq) - self.localProps.getProperty("squelch_level").wire(self.dsp.set_squelch_level) def set_low_cut(cut): bpf = self.dsp.get_bpf() bpf[0] = cut self.dsp.set_bpf(*bpf) - self.localProps.getProperty("low_cut").wire(set_low_cut) def set_high_cut(cut): bpf = self.dsp.get_bpf() bpf[1] = cut self.dsp.set_bpf(*bpf) - self.localProps.getProperty("high_cut").wire(set_high_cut) - self.localProps.getProperty("mod").wire(self.dsp.set_demodulator) + self.subscriptions = [ + self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression), + self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression), + self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size), + self.localProps.getProperty("samp_rate").wire(self.dsp.set_samp_rate), + self.localProps.getProperty("output_rate").wire(self.dsp.set_output_rate), + self.localProps.getProperty("offset_freq").wire(self.dsp.set_offset_freq), + self.localProps.getProperty("squelch_level").wire(self.dsp.set_squelch_level), + self.localProps.getProperty("low_cut").wire(set_low_cut), + self.localProps.getProperty("high_cut").wire(set_high_cut), + self.localProps.getProperty("mod").wire(self.dsp.set_demodulator) + ] + + self.dsp.set_offset_freq(0) + self.dsp.set_bpf(-4000,4000) + self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"] + self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"] + self.dsp.csdr_through = self.localProps["csdr_through"] if (self.localProps["digimodes_enable"]): def set_secondary_mod(mod): if mod == False: mod = None - if self.dsp.get_secondary_demodulator() == mod: return - self.dsp.stop() self.dsp.set_secondary_demodulator(mod) if mod is not None: self.handler.write_secondary_dsp_config({ @@ -370,11 +374,10 @@ def set_secondary_mod(mod): "if_samp_rate":self.dsp.if_samp_rate(), "secondary_bw":self.dsp.secondary_bw() }) - self.dsp.start() - - self.localProps.getProperty("secondary_mod").wire(set_secondary_mod) - - self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq) + self.subscriptions += [ + self.localProps.getProperty("secondary_mod").wire(set_secondary_mod), + self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq) + ] self.sdrSource.addClient(self) @@ -412,6 +415,9 @@ def copy(): def stop(self): self.dsp.stop() self.sdrSource.removeClient(self) + for sub in self.subscriptions: + sub.cancel() + self.subscriptions = [] def setProperty(self, prop, value): self.localProps.getProperty(prop).setValue(value) From 0629e6c77724a3b2e3b760a635404486ff7bfc3d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 18 May 2019 22:10:43 +0200 Subject: [PATCH 0129/2616] make the ambe unvoiced quality configurable --- config_webrx.py | 4 ++++ csdr.py | 17 +++++++++++++---- owrx/source.py | 3 ++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index f156d0ffa..47ebe0d70 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -77,6 +77,10 @@ digimodes_enable=True #Decoding digimodes come with higher CPU usage. digimodes_fft_size=1024 +# determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes +# if you're running on a Raspi (up to 3B+) you'll want to leave this on 1 +digital_voice_unvoiced_quality = 1 + """ Note: if you experience audio underruns while CPU usage is 100%, you can: - decrease `samp_rate`, diff --git a/csdr.py b/csdr.py index 6ce0ea83f..f2aa9df54 100755 --- a/csdr.py +++ b/csdr.py @@ -70,6 +70,7 @@ def __init__(self, output): self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", "iqtee2_pipe"] self.secondary_pipe_names=["secondary_shift_pipe"] self.secondary_offset_freq = 1000 + self.unvoiced_quality = 1 self.modification_lock = threading.Lock() self.output = output @@ -105,11 +106,11 @@ def chain(self,which): chain += "dsd -fd" elif which == "nxdn": chain += "dsd -fi" - chain += " -i - -o - -u 2 -g 10 | " + chain += " -i - -o - -u {unvoiced_quality} -g 10 | " elif which == "dmr": - chain += "rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer | " + chain += "rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer -u {unvoiced_quality} | " elif which == "ysf": - chain += "rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y | " + chain += "rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -u {unvoiced_quality} | " chain += "sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " elif which == "am": chain += "csdr amdemod_cf | csdr fastdcblock_ff | " @@ -347,6 +348,13 @@ def set_squelch_level(self, squelch_level): self.squelch_pipe_file.flush() self.modification_lock.release() + def set_unvoiced_quality(self, q): + self.unvoiced_quality = q + self.restart() + + def get_unvoiced_quality(self): + return self.unvoiced_quality + def mkfifo(self,path): try: os.unlink(path) @@ -393,7 +401,8 @@ def start(self): bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, - output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000) ) + output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000), + unvoiced_quality = self.get_unvoiced_quality()) logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) my_env=os.environ.copy() diff --git a/owrx/source.py b/owrx/source.py index b5264b00a..d75e650a7 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -355,7 +355,8 @@ def set_high_cut(cut): self.localProps.getProperty("squelch_level").wire(self.dsp.set_squelch_level), self.localProps.getProperty("low_cut").wire(set_low_cut), self.localProps.getProperty("high_cut").wire(set_high_cut), - self.localProps.getProperty("mod").wire(self.dsp.set_demodulator) + self.localProps.getProperty("mod").wire(self.dsp.set_demodulator), + self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality) ] self.dsp.set_offset_freq(0) From edadc383ffc50f6e95c8d8f98c3abd01eab6a461 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 18 May 2019 22:26:52 +0200 Subject: [PATCH 0130/2616] make unvoiced quality actually work --- owrx/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/source.py b/owrx/source.py index d75e650a7..f101f5bfc 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -329,7 +329,7 @@ def __init__(self, handler, sdrSource): self.localProps = self.sdrSource.getProps().collect( "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", - "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate" + "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality" ).defaults(PropertyManager.getSharedInstance()) self.dsp = csdr.dsp(self) From bb6b00a99811a9400a7c2ec095bb1f1a8023a9be Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 18 May 2019 22:27:19 +0200 Subject: [PATCH 0131/2616] fix meta pipe crashes caused by unknown unicode characters (looks ugly now at times, but at least works continuously) --- csdr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/csdr.py b/csdr.py index f2aa9df54..60811577b 100755 --- a/csdr.py +++ b/csdr.py @@ -450,7 +450,8 @@ def read_smeter(): return float(raw.rstrip("\n")) self.output.add_output("smeter", read_smeter) if self.meta_pipe != None: - self.meta_pipe_file=open(self.meta_pipe,"r") + # TODO make digiham output unicode and then change this here + self.meta_pipe_file=open(self.meta_pipe, "r", encoding="cp437") def read_meta(): raw = self.meta_pipe_file.readline() if len(raw) == 0: From eb758685a1e624cc46d3b8fb777ecd47f42ad394 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 19 May 2019 13:17:36 +0200 Subject: [PATCH 0132/2616] add antenna switching support for sdrplay --- config_webrx.py | 12 ++++++++---- owrx/config.py | 2 +- owrx/source.py | 11 +++-------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index 3bd92c60d..d16b94879 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -133,7 +133,8 @@ "rf_gain": 40, "samp_rate": 500000, "start_freq": 14070000, - "start_mod": "usb" + "start_mod": "usb", + "antenna": "Antenna A" }, "40m": { "name":"40m", @@ -141,7 +142,8 @@ "rf_gain": 40, "samp_rate": 500000, "start_freq": 7070000, - "start_mod": "usb" + "start_mod": "usb", + "antenna": "Antenna A" }, "80m": { "name":"80m", @@ -149,7 +151,8 @@ "rf_gain": 40, "samp_rate": 500000, "start_freq": 3570000, - "start_mod": "usb" + "start_mod": "usb", + "antenna": "Antenna A" }, "49m": { "name": "49m Broadcast", @@ -157,7 +160,8 @@ "rf_gain": 40, "samp_rate": 500000, "start_freq": 6070000, - "start_mod": "am" + "start_mod": "am", + "antenna": "Antenna A" } } }, diff --git a/owrx/config.py b/owrx/config.py index 8fb65132c..558e3438c 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -32,7 +32,7 @@ def getSharedInstance(): return PropertyManager.sharedInstance def collect(self, *props): - return PropertyManager(dict((name, self.getProperty(name) if self.hasProperty(name) else Property()) for name in props)) + return PropertyManager({name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props}) def __init__(self, properties = None): self.properties = {} diff --git a/owrx/source.py b/owrx/source.py index 3efc7d4a4..9c41318d9 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -76,7 +76,7 @@ def __init__(self, props, port): self.props = props self.activateProfile() self.rtlProps = self.props.collect( - "type", "samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp" + "samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna" ).defaults(PropertyManager.getSharedInstance()) def restart(name, value): @@ -128,12 +128,7 @@ def start(self): props = self.rtlProps start_sdr_command = self.command.format( - samp_rate = props["samp_rate"], - center_freq = props["center_freq"], - ppm = props["ppm"], - rf_gain = props["rf_gain"], - lna_gain = props["lna_gain"], - rf_amp = props["rf_amp"] + **props.collect("samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna").__dict__() ) if self.format_conversion is not None: @@ -243,7 +238,7 @@ def __init__(self, props, port): class SdrplaySource(SdrSource): def __init__(self, props, port): super().__init__(props, port) - self.command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -" + self.command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -a \"{antenna}\" -" self.format_conversion = None def sleepOnRestart(self): From 92abef71723298cf3546b19a35caf44aaea0b7fe Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 19 May 2019 13:36:05 +0200 Subject: [PATCH 0133/2616] pass antenna parameter only if set --- owrx/source.py | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/owrx/source.py b/owrx/source.py index 9c41318d9..f5a0f5fed 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -92,9 +92,13 @@ def restart(name, value): self.process = None self.modificationLock = threading.Lock() - # override these in subclasses as necessary - self.command = None - self.format_conversion = None + # override this in subclasses + def getCommand(self): + pass + + # override this in subclasses, if necessary + def getFormatConversion(self): + return None def activateProfile(self, id = None): profiles = self.props["profiles"] @@ -127,12 +131,13 @@ def start(self): props = self.rtlProps - start_sdr_command = self.command.format( + start_sdr_command = self.getCommand().format( **props.collect("samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna").__dict__() ) - if self.format_conversion is not None: - start_sdr_command += " | " + self.format_conversion + format_conversion = self.getFormatConversion() + if format_conversion is not None: + start_sdr_command += " | " + format_conversion nmux_bufcnt = nmux_bufsize = 0 while nmux_bufsize < props["samp_rate"]/4: nmux_bufsize += 4096 @@ -224,22 +229,26 @@ def writeSpectrumData(self, data): class RtlSdrSource(SdrSource): - def __init__(self, props, port): - super().__init__(props, port) - self.command = "rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -" - self.format_conversion = "csdr convert_u8_f" + def getCommand(self): + return "rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -" + + def getFormatConversion(self): + return "csdr convert_u8_f" class HackrfSource(SdrSource): - def __init__(self, props, port): - super().__init__(props, port) - self.command = "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-" - self.format_conversion = "csdr convert_s8_f" + def getCommand(self): + return "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-" + + def getFormatConversion(self): + return "csdr convert_s8_f" class SdrplaySource(SdrSource): - def __init__(self, props, port): - super().__init__(props, port) - self.command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -a \"{antenna}\" -" - self.format_conversion = None + def getCommand(self): + command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain}" + if self.rtlProps["antenna"] is not None: + command += " -a \"{antenna}\"" + command += " -" + return command def sleepOnRestart(self): time.sleep(1) From 3a669294d7c061dec612021b44cdc372e8e182d3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 19 May 2019 17:56:41 +0200 Subject: [PATCH 0134/2616] check for gfsk_demodulator, too --- owrx/feature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/feature.py b/owrx/feature.py index dfc790181..e119ffa5a 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -90,7 +90,7 @@ def check_with_stdin(command): return reduce(and_, map( check_with_stdin, - ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer"] + ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator"] ), True) From 8091831b1f1a3400da1241a720fe3f646fb79b9e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 19 May 2019 22:10:11 +0200 Subject: [PATCH 0135/2616] make both gains available for sdrplay --- config_webrx.py | 12 ++++++++---- owrx/connection.py | 2 +- owrx/source.py | 10 +++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index d16b94879..25053ebfa 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -130,7 +130,8 @@ "20m": { "name":"20m", "center_freq": 14150000, - "rf_gain": 40, + "rf_gain": 4, + "if_gain": 40, "samp_rate": 500000, "start_freq": 14070000, "start_mod": "usb", @@ -139,7 +140,8 @@ "40m": { "name":"40m", "center_freq": 7100000, - "rf_gain": 40, + "rf_gain": 4, + "if_gain": 40, "samp_rate": 500000, "start_freq": 7070000, "start_mod": "usb", @@ -148,7 +150,8 @@ "80m": { "name":"80m", "center_freq": 3650000, - "rf_gain": 40, + "rf_gain": 4, + "if_gain": 40, "samp_rate": 500000, "start_freq": 3570000, "start_mod": "usb", @@ -157,7 +160,8 @@ "49m": { "name": "49m Broadcast", "center_freq": 6000000, - "rf_gain": 40, + "rf_gain": 4, + "if_gain": 40, "samp_rate": 500000, "start_freq": 6070000, "start_mod": "am", diff --git a/owrx/connection.py b/owrx/connection.py index 95ce84f89..029b8d445 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -81,7 +81,7 @@ def stopDsp(self): def setParams(self, params): # only the keys in the protected property manager can be overridden from the web - protected = self.sdr.getProps().collect("samp_rate", "center_freq", "rf_gain", "type") \ + protected = self.sdr.getProps().collect("samp_rate", "center_freq", "rf_gain", "type", "if_gain") \ .defaults(PropertyManager.getSharedInstance()) for key, value in params.items(): protected[key] = value diff --git a/owrx/source.py b/owrx/source.py index f5a0f5fed..2820abcb0 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -76,7 +76,7 @@ def __init__(self, props, port): self.props = props self.activateProfile() self.rtlProps = self.props.collect( - "samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna" + "samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain" ).defaults(PropertyManager.getSharedInstance()) def restart(name, value): @@ -132,7 +132,7 @@ def start(self): props = self.rtlProps start_sdr_command = self.getCommand().format( - **props.collect("samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna").__dict__() + **props.collect("samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain").__dict__() ) format_conversion = self.getFormatConversion() @@ -244,7 +244,11 @@ def getFormatConversion(self): class SdrplaySource(SdrSource): def getCommand(self): - command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain}" + command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}" + gainMap = { "rf_gain" : "RFGR", "if_gain" : "IFGR"} + gains = [ "{0}={{{1}}}".format(gainMap[name], name) for name in self.rtlProps.collect("rf_gain", "if_gain").__dict__() ] + if gains: + command += " -g {gains}".format(gains = ",".join(gains)) if self.rtlProps["antenna"] is not None: command += " -a \"{antenna}\"" command += " -" From 7893216cce5181af87f63e918972ce3a0807e920 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 19 May 2019 22:12:17 +0200 Subject: [PATCH 0136/2616] 30m fix --- config_webrx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config_webrx.py b/config_webrx.py index aece843c8..75f61b1ec 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -144,7 +144,8 @@ "30m": { "name":"30m", "center_freq": 10125000, - "rf_gain": 40, + "rf_gain": 4, + "if_gain": 40, "samp_rate": 250000, "start_freq": 10142000, "start_mod": "usb" From 8a7aeca6b9c1358cc6612b30fd1e0b9637594da2 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 19 May 2019 22:23:35 +0200 Subject: [PATCH 0137/2616] if_gain is optional, default is agc --- config_webrx.py | 12 ++++++++---- owrx/source.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index 25053ebfa..e97ed6381 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -131,17 +131,23 @@ "name":"20m", "center_freq": 14150000, "rf_gain": 4, - "if_gain": 40, "samp_rate": 500000, "start_freq": 14070000, "start_mod": "usb", "antenna": "Antenna A" }, + "30m": { + "name":"30m", + "center_freq": 10125000, + "rf_gain": 4, + "samp_rate": 250000, + "start_freq": 10142000, + "start_mod": "usb" + }, "40m": { "name":"40m", "center_freq": 7100000, "rf_gain": 4, - "if_gain": 40, "samp_rate": 500000, "start_freq": 7070000, "start_mod": "usb", @@ -151,7 +157,6 @@ "name":"80m", "center_freq": 3650000, "rf_gain": 4, - "if_gain": 40, "samp_rate": 500000, "start_freq": 3570000, "start_mod": "usb", @@ -161,7 +166,6 @@ "name": "49m Broadcast", "center_freq": 6000000, "rf_gain": 4, - "if_gain": 40, "samp_rate": 500000, "start_freq": 6070000, "start_mod": "am", diff --git a/owrx/source.py b/owrx/source.py index 2820abcb0..eeed1a7f9 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -246,7 +246,7 @@ class SdrplaySource(SdrSource): def getCommand(self): command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}" gainMap = { "rf_gain" : "RFGR", "if_gain" : "IFGR"} - gains = [ "{0}={{{1}}}".format(gainMap[name], name) for name in self.rtlProps.collect("rf_gain", "if_gain").__dict__() ] + gains = [ "{0}={{{1}}}".format(gainMap[name], name) for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items() if value is not None ] if gains: command += " -g {gains}".format(gains = ",".join(gains)) if self.rtlProps["antenna"] is not None: From 1846605184167fa223fe7d8329b5ac355dad1f34 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 24 May 2019 18:48:08 +0200 Subject: [PATCH 0138/2616] use dc blocker and limiter to improve signal decoding --- csdr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/csdr.py b/csdr.py index 60811577b..ec2ac1281 100755 --- a/csdr.py +++ b/csdr.py @@ -92,13 +92,13 @@ def chain(self,which): chain += "csdr tee {iqtee_pipe} | " chain += "csdr tee {iqtee2_pipe} | " # safe some cpu cycles... no need to decimate if decimation factor is 1 - last_decimation_block = "csdr old_fractional_decimator_ff {last_decimation} | " if self.last_decimation != 1.0 else "" + last_decimation_block = "csdr fractional_decimator_ff {last_decimation} | " if self.last_decimation != 1.0 else "" if which == "nfm": chain += "csdr fmdemod_quadri_cf | csdr limit_ff | " chain += last_decimation_block chain += "csdr deemphasis_nfm_ff {output_rate} | csdr convert_f_s16" elif self.isDigitalVoice(which): - chain += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | " + chain += "csdr fmdemod_quadri_cf | dc_block | csdr limit_ff | " chain += last_decimation_block chain += "csdr convert_f_s16 | " if which in [ "dstar", "nxdn" ]: From 725615fbe573c333a1445bb3b306ad1b4d23d484 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 May 2019 01:45:05 +0200 Subject: [PATCH 0139/2616] display the mode from the metadata for ysf --- htdocs/openwebrx.js | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 38f098e8d..01aafe7b4 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1340,6 +1340,7 @@ function update_metadata(stringData) { break; case 'YSF': var strings = []; + if (meta.mode) strings.push("Mode: " + meta.mode); if (meta.source) strings.push("Source: " + meta.source); if (meta.target) strings.push("Destination: " + meta.target); if (meta.up) strings.push("Up: " + meta.up); From 05f6fff8f6fd7da940b31471f5944b8deccaf061 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 May 2019 01:46:16 +0200 Subject: [PATCH 0140/2616] feed rrc filter with floats; add digitalvoice_filter --- csdr.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/csdr.py b/csdr.py index ec2ac1281..5fd2a20a5 100755 --- a/csdr.py +++ b/csdr.py @@ -98,20 +98,24 @@ def chain(self,which): chain += last_decimation_block chain += "csdr deemphasis_nfm_ff {output_rate} | csdr convert_f_s16" elif self.isDigitalVoice(which): - chain += "csdr fmdemod_quadri_cf | dc_block | csdr limit_ff | " + chain += "csdr fmdemod_quadri_cf | dc_block | " chain += last_decimation_block - chain += "csdr convert_f_s16 | " + # dsd modes if which in [ "dstar", "nxdn" ]: + chain += "csdr limit_ff | csdr convert_f_s16 | " if which == "dstar": chain += "dsd -fd" elif which == "nxdn": chain += "dsd -fi" chain += " -i - -o - -u {unvoiced_quality} -g 10 | " - elif which == "dmr": - chain += "rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer -u {unvoiced_quality} | " - elif which == "ysf": - chain += "rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -u {unvoiced_quality} | " - chain += "sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + # digiham modes + else: + chain += "rrc_filter | csdr limit_ff | csdr convert_f_s16 | gfsk_demodulator | " + if which == "dmr": + chain += "dmr_decoder --fifo {meta_pipe} | mbe_synthesizer -u {unvoiced_quality} | " + elif which == "ysf": + chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -u {unvoiced_quality} | " + chain += "digitalvoice_filter | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " elif which == "am": chain += "csdr amdemod_cf | csdr fastdcblock_ff | " chain += last_decimation_block From 14f932eea848cff78c691eb13141cf5792e1f4c3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 30 May 2019 16:12:13 +0200 Subject: [PATCH 0141/2616] parse metadata on the server side --- htdocs/openwebrx.js | 8 +------- owrx/meta.py | 7 +++++++ owrx/source.py | 4 +++- 3 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 owrx/meta.py diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 01aafe7b4..7c9d2372b 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1309,13 +1309,7 @@ function on_ws_recv(evt) } } -function update_metadata(stringData) { - var meta = {}; - stringData.split(";").forEach(function(s) { - var item = s.split(":"); - meta[item[0]] = item[1]; - }); - +function update_metadata(meta) { var update = function(_, el) { el.innerHTML = ""; }; diff --git a/owrx/meta.py b/owrx/meta.py new file mode 100644 index 000000000..b99f6467e --- /dev/null +++ b/owrx/meta.py @@ -0,0 +1,7 @@ +class MetaParser(object): + def __init__(self, handler): + self.handler = handler + def parse(self, meta): + fields = meta.split(";") + dict = {v[0] : "".join(v[1:]) for v in map(lambda x: x.split(":"), fields)} + self.handler.write_metadata(dict) \ No newline at end of file diff --git a/owrx/source.py b/owrx/source.py index 66691cc3a..c390dbf6b 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -1,6 +1,7 @@ import subprocess from owrx.config import PropertyManager from owrx.feature import FeatureDetector, UnknownFeatureException +from owrx.meta import MetaParser import threading import csdr import time @@ -334,6 +335,7 @@ class DspManager(csdr.output): def __init__(self, handler, sdrSource): self.handler = handler self.sdrSource = sdrSource + self.metaParser = MetaParser(self.handler) self.localProps = self.sdrSource.getProps().collect( "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", @@ -403,7 +405,7 @@ def add_output(self, t, read_fn): "smeter": self.handler.write_s_meter_level, "secondary_fft": self.handler.write_secondary_fft, "secondary_demod": self.handler.write_secondary_demod, - "meta": self.handler.write_metadata + "meta": self.metaParser.parse } write = writers[t] From 7100d43d9ec3918685c4c05300666ec4d5422e17 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 30 May 2019 17:19:46 +0200 Subject: [PATCH 0142/2616] show callsigns for ham radio dmr ids --- config_webrx.py | 2 ++ htdocs/openwebrx.js | 10 ++++++++- owrx/meta.py | 54 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index 0a76aad51..98bcb0a55 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -80,6 +80,8 @@ # determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes # if you're running on a Raspi (up to 3B+) you'll want to leave this on 1 digital_voice_unvoiced_quality = 1 +# enables lookup of DMR ids using the radioid api +digital_voice_dmr_id_lookup = True """ Note: if you experience audio underruns while CPU usage is 100%, you can: diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 7c9d2372b..3808f9215 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1318,7 +1318,15 @@ function update_metadata(meta) { if (meta.slot) { var html = 'Timeslot: ' + meta.slot; if (meta.type) html += ' Typ: ' + meta.type; - if (meta.source && meta.target) html += ' Source: ' + meta.source + ' Target: ' + meta.target; + if (meta.additional && meta.additional.callsign) { + html += ' Source: ' + meta.additional.callsign; + if (meta.additional.fname) { + html += ' (' + meta.additional.fname + ')'; + } + } else if (meta.source) { + html += ' Source: ' + meta.source; + } + if (meta.target) html += ' Target: ' + meta.target; update = function(_, el) { var slotEl = el.getElementsByClassName('slot-' + meta.slot); if (!slotEl.length) { diff --git a/owrx/meta.py b/owrx/meta.py index b99f6467e..bdb1eab2b 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -1,7 +1,57 @@ +from owrx.config import PropertyManager +from urllib import request +import json +from datetime import datetime, timedelta +import logging + +logger = logging.getLogger(__name__) + +class DmrMetaEnricher(object): + def __init__(self): + self.cache = {} + self.cacheTimeout = timedelta(seconds = 86400) + def cacheEntryValid(self, id): + if not id in self.cache: return False + entry = self.cache[id] + return entry["timestamp"] + self.cacheTimeout > datetime.now() + def enrich(self, meta): + if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: return None + if not "source" in meta: return None + source = meta["source"] + if not self.cacheEntryValid(source): + try: + logger.debug("requesting DMR metadata for id=%s", source) + res = request.urlopen("https://www.radioid.net/api/dmr/user/?id={0}".format(source), timeout=5).read() + data = json.loads(res.decode("utf-8")) + self.cache[source] = { + "timestamp": datetime.now(), + "data": data + } + except json.JSONDecodeError: + self.cache[source] = { + "timestamp": datetime.now(), + "data": None + } + data = self.cache[source]["data"] + if "count" in data and data["count"] > 0 and "results" in data: + return data["results"][0] + return None + + class MetaParser(object): + enrichers = { + "DMR": DmrMetaEnricher() + } def __init__(self, handler): self.handler = handler def parse(self, meta): fields = meta.split(";") - dict = {v[0] : "".join(v[1:]) for v in map(lambda x: x.split(":"), fields)} - self.handler.write_metadata(dict) \ No newline at end of file + meta = {v[0] : "".join(v[1:]) for v in map(lambda x: x.split(":"), fields)} + + if "protocol" in meta: + protocol = meta["protocol"] + if protocol in MetaParser.enrichers: + additional_data = MetaParser.enrichers[protocol].enrich(meta) + if additional_data is not None: meta["additional"] = additional_data + self.handler.write_metadata(meta) + From f565b4dbcd658ce68b1e798d6174a6ec9d64c318 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 30 May 2019 18:32:08 +0200 Subject: [PATCH 0143/2616] download dmr ids asynchronously --- owrx/meta.py | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/owrx/meta.py b/owrx/meta.py index bdb1eab2b..d448b55fb 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -3,36 +3,44 @@ import json from datetime import datetime, timedelta import logging +import threading logger = logging.getLogger(__name__) class DmrMetaEnricher(object): def __init__(self): self.cache = {} + self.threads = {} self.cacheTimeout = timedelta(seconds = 86400) def cacheEntryValid(self, id): if not id in self.cache: return False entry = self.cache[id] return entry["timestamp"] + self.cacheTimeout > datetime.now() + def downloadRadioIdData(self, id): + try: + logger.debug("requesting DMR metadata for id=%s", id) + res = request.urlopen("https://www.radioid.net/api/dmr/user/?id={0}".format(id), timeout=5).read() + data = json.loads(res.decode("utf-8")) + self.cache[id] = { + "timestamp": datetime.now(), + "data": data + } + except json.JSONDecodeError: + self.cache[id] = { + "timestamp": datetime.now(), + "data": None + } + del self.threads[id] def enrich(self, meta): if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: return None if not "source" in meta: return None - source = meta["source"] - if not self.cacheEntryValid(source): - try: - logger.debug("requesting DMR metadata for id=%s", source) - res = request.urlopen("https://www.radioid.net/api/dmr/user/?id={0}".format(source), timeout=5).read() - data = json.loads(res.decode("utf-8")) - self.cache[source] = { - "timestamp": datetime.now(), - "data": data - } - except json.JSONDecodeError: - self.cache[source] = { - "timestamp": datetime.now(), - "data": None - } - data = self.cache[source]["data"] + id = meta["source"] + if not self.cacheEntryValid(id): + if not id in self.threads: + self.threads[id] = threading.Thread(target=self.downloadRadioIdData, args=[id]) + self.threads[id].start() + return None + data = self.cache[id]["data"] if "count" in data and data["count"] > 0 and "results" in data: return data["results"][0] return None From 908e3036e0058e01be26fbdb124ade8eb2dd7aa7 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 30 May 2019 18:35:58 +0200 Subject: [PATCH 0144/2616] digital pipeline tweaks (not sure if it's better that way) --- csdr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/csdr.py b/csdr.py index 5fd2a20a5..6e78adb1e 100755 --- a/csdr.py +++ b/csdr.py @@ -110,12 +110,12 @@ def chain(self,which): chain += " -i - -o - -u {unvoiced_quality} -g 10 | " # digiham modes else: - chain += "rrc_filter | csdr limit_ff | csdr convert_f_s16 | gfsk_demodulator | " + chain += "rrc_filter | csdr convert_f_s16 | gfsk_demodulator | " if which == "dmr": chain += "dmr_decoder --fifo {meta_pipe} | mbe_synthesizer -u {unvoiced_quality} | " elif which == "ysf": chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -u {unvoiced_quality} | " - chain += "digitalvoice_filter | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + chain += "digitalvoice_filter | sox -V -v 0.95 -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " elif which == "am": chain += "csdr amdemod_cf | csdr fastdcblock_ff | " chain += last_decimation_block From 21217399255378648ccfe8c2860964cdc960f13f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 30 May 2019 18:54:45 +0200 Subject: [PATCH 0145/2616] make the cache global --- owrx/meta.py | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/owrx/meta.py b/owrx/meta.py index d448b55fb..8613e6486 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -7,40 +7,54 @@ logger = logging.getLogger(__name__) -class DmrMetaEnricher(object): +class DmrCache(object): + sharedInstance = None + @staticmethod + def getSharedInstance(): + if DmrCache.sharedInstance is None: + DmrCache.sharedInstance = DmrCache() + return DmrCache.sharedInstance def __init__(self): self.cache = {} - self.threads = {} self.cacheTimeout = timedelta(seconds = 86400) - def cacheEntryValid(self, id): - if not id in self.cache: return False - entry = self.cache[id] + def isValid(self, key): + if not key in self.cache: return False + entry = self.cache[key] return entry["timestamp"] + self.cacheTimeout > datetime.now() + def put(self, key, value): + self.cache[key] = { + "timestamp": datetime.now(), + "data": value + } + def get(self, key): + if not self.isValid(key): return None + return self.cache[key]["data"] + + +class DmrMetaEnricher(object): + def __init__(self): + self.threads = {} def downloadRadioIdData(self, id): + cache = DmrCache.getSharedInstance() try: logger.debug("requesting DMR metadata for id=%s", id) res = request.urlopen("https://www.radioid.net/api/dmr/user/?id={0}".format(id), timeout=5).read() data = json.loads(res.decode("utf-8")) - self.cache[id] = { - "timestamp": datetime.now(), - "data": data - } + cache.put(id, data) except json.JSONDecodeError: - self.cache[id] = { - "timestamp": datetime.now(), - "data": None - } + cache.put(id, None) del self.threads[id] def enrich(self, meta): if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: return None if not "source" in meta: return None id = meta["source"] - if not self.cacheEntryValid(id): + cache = DmrCache.getSharedInstance() + if not cache.isValid(id): if not id in self.threads: self.threads[id] = threading.Thread(target=self.downloadRadioIdData, args=[id]) self.threads[id].start() return None - data = self.cache[id]["data"] + data = cache.get(id) if "count" in data and data["count"] > 0 and "results" in data: return data["results"][0] return None From b7fc6a9c87eac55c63762488237b599343a0dbc9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 4 Jun 2019 00:39:22 +0200 Subject: [PATCH 0146/2616] connection handling fix --- owrx/connection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/owrx/connection.py b/owrx/connection.py index abd61abf5..67ee96bd8 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -15,12 +15,12 @@ class OpenWebRxClient(object): def __init__(self, conn): self.conn = conn - ClientRegistry.getSharedInstance().addClient(self) - self.dsp = None self.sdr = None self.configSub = None + ClientRegistry.getSharedInstance().addClient(self) + pm = PropertyManager.getSharedInstance() self.setSdr() From 546249e95069c2444dd5809c02c1ae6f4214911c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 5 Jun 2019 00:08:56 +0200 Subject: [PATCH 0147/2616] detect presence of nc --- owrx/feature.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/owrx/feature.py b/owrx/feature.py index e119ffa5a..75ef97d9f 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -12,7 +12,7 @@ class UnknownFeatureException(Exception): class FeatureDetector(object): features = { - "core": [ "csdr", "nmux" ], + "core": [ "csdr", "nmux", "nc" ], "rtl_sdr": [ "rtl_sdr" ], "sdrplay": [ "rx_tools" ], "hackrf": [ "hackrf_transfer" ], @@ -51,6 +51,9 @@ def has_csdr(self): def has_nmux(self): return self.command_is_runnable("nmux --help") + def has_nc(self): + return self.command_is_runnable('nc --help') + def has_rtl_sdr(self): return self.command_is_runnable("rtl_sdr --help") From 4934e91e7486b624cf6ebe2917f8c33b8f447f54 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 5 Jun 2019 00:13:54 +0200 Subject: [PATCH 0148/2616] increase timeout (it's asynchronous, so we can wait) --- owrx/meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/meta.py b/owrx/meta.py index 8613e6486..e215d89c8 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -38,7 +38,7 @@ def downloadRadioIdData(self, id): cache = DmrCache.getSharedInstance() try: logger.debug("requesting DMR metadata for id=%s", id) - res = request.urlopen("https://www.radioid.net/api/dmr/user/?id={0}".format(id), timeout=5).read() + res = request.urlopen("https://www.radioid.net/api/dmr/user/?id={0}".format(id), timeout=30).read() data = json.loads(res.decode("utf-8")) cache.put(id, data) except json.JSONDecodeError: From 0c59caa2307aa632df5bcd21f5d24e1cd0ce1f7b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 5 Jun 2019 00:17:06 +0200 Subject: [PATCH 0149/2616] try to handle clipping problems with agc --- csdr.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/csdr.py b/csdr.py index 6e78adb1e..28443f32d 100755 --- a/csdr.py +++ b/csdr.py @@ -108,14 +108,15 @@ def chain(self,which): elif which == "nxdn": chain += "dsd -fi" chain += " -i - -o - -u {unvoiced_quality} -g 10 | " + chain += "digitalvoice_filter | sox -V -v 0.95 -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " # digiham modes else: chain += "rrc_filter | csdr convert_f_s16 | gfsk_demodulator | " if which == "dmr": - chain += "dmr_decoder --fifo {meta_pipe} | mbe_synthesizer -u {unvoiced_quality} | " + chain += "dmr_decoder --fifo {meta_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | " elif which == "ysf": - chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -u {unvoiced_quality} | " - chain += "digitalvoice_filter | sox -V -v 0.95 -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | " + chain += "digitalvoice_filter -f | csdr agc_ff 160000 0.8 1 0.0000001 0.0005 | csdr convert_f_s16 | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " elif which == "am": chain += "csdr amdemod_cf | csdr fastdcblock_ff | " chain += last_decimation_block From aa7212c64272de7ac4c9a820dd792b425a3df8fe Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 7 Jun 2019 01:14:09 +0200 Subject: [PATCH 0150/2616] handle OSErrors, too --- owrx/websocket.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/owrx/websocket.py b/owrx/websocket.py index d0385b885..b8ea3a72b 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -78,7 +78,9 @@ def close(self): self.handler.wfile.write(header) self.handler.wfile.flush() except ValueError: - logger.exception("while writing close frame:") + logger.exception("ValueError while writing close frame:") + except OSError: + logger.exception("OSError while writing close frame:") try: self.handler.finish() From f49086a527fbf015446d0e53647bd8e51808eac9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 7 Jun 2019 15:11:04 +0200 Subject: [PATCH 0151/2616] add first integration of direwolf for aprs --- csdr.py | 14 +- htdocs/index.html | 3 + htdocs/libmbe.js | 2322 +++++++++++++++++++++++++++++++++++++++++++ htdocs/libmbe.wasm | Bin 0 -> 31678 bytes htdocs/openwebrx.js | 5 + owrx/feature.py | 8 +- 6 files changed, 2348 insertions(+), 4 deletions(-) create mode 100644 htdocs/libmbe.js create mode 100644 htdocs/libmbe.wasm diff --git a/csdr.py b/csdr.py index 28443f32d..4825c6498 100755 --- a/csdr.py +++ b/csdr.py @@ -117,6 +117,11 @@ def chain(self,which): elif which == "ysf": chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | " chain += "digitalvoice_filter -f | csdr agc_ff 160000 0.8 1 0.0000001 0.0005 | csdr convert_f_s16 | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + elif which == "packet": + chain += "csdr fmdemod_quadri_cf | " + chain += last_decimation_block + chain += "csdr convert_f_s16 | " + chain += "direwolf -r {audio_rate} - 1>&2" elif which == "am": chain += "csdr amdemod_cf | csdr fastdcblock_ff | " chain += last_decimation_block @@ -285,7 +290,7 @@ def get_output_rate(self): return self.output_rate def get_audio_rate(self): - if self.isDigitalVoice(): + if self.isDigitalVoice() or self.isPacket(): return 48000 return self.get_output_rate() @@ -294,6 +299,11 @@ def isDigitalVoice(self, demodulator = None): demodulator = self.get_demodulator() return demodulator in ["dmr", "dstar", "nxdn", "ysf"] + def isPacket(self, demodulator = None): + if demodulator is None: + demodulator = self.get_demodulator() + return demodulator == "packet" + def set_output_rate(self,output_rate): self.output_rate=output_rate self.calculate_decimation() @@ -407,7 +417,7 @@ def start(self): flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000), - unvoiced_quality = self.get_unvoiced_quality()) + unvoiced_quality = self.get_unvoiced_quality(), audio_rate = self.get_audio_rate()) logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) my_env=os.environ.copy() diff --git a/htdocs/index.html b/htdocs/index.html index da3c800e5..7e7b1e53c 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -102,6 +102,9 @@ +
DIG
diff --git a/htdocs/libmbe.js b/htdocs/libmbe.js new file mode 100644 index 000000000..fb29253a0 --- /dev/null +++ b/htdocs/libmbe.js @@ -0,0 +1,2322 @@ +// Copyright 2010 The Emscripten Authors. All rights reserved. +// Emscripten is available under two separate licenses, the MIT license and the +// University of Illinois/NCSA Open Source License. Both these licenses can be +// found in the LICENSE file. + +// The Module object: Our interface to the outside world. We import +// and export values on it. There are various ways Module can be used: +// 1. Not defined. We create it here +// 2. A function parameter, function(Module) { ..generated code.. } +// 3. pre-run appended it, var Module = {}; ..generated code.. +// 4. External script tag defines var Module. +// We need to check if Module already exists (e.g. case 3 above). +// Substitution will be replaced with actual code on later stage of the build, +// this way Closure Compiler will not mangle it (e.g. case 4. above). +// Note that if you want to run closure, and also to use Module +// after the generated code, you will need to define var Module = {}; +// before the code. Then that object will be used in the code, and you +// can continue to use Module afterwards as well. +var Module = typeof Module !== 'undefined' ? Module : {}; + +// --pre-jses are emitted after the Module integration code, so that they can +// refer to Module (if they choose; they can also define Module) +// {{PRE_JSES}} + +// Sometimes an existing Module object exists with properties +// meant to overwrite the default module functionality. Here +// we collect those properties and reapply _after_ we configure +// the current environment's defaults to avoid having to be so +// defensive during initialization. +var moduleOverrides = {}; +var key; +for (key in Module) { + if (Module.hasOwnProperty(key)) { + moduleOverrides[key] = Module[key]; + } +} + +Module['arguments'] = []; +Module['thisProgram'] = './this.program'; +Module['quit'] = function(status, toThrow) { + throw toThrow; +}; +Module['preRun'] = []; +Module['postRun'] = []; + +// Determine the runtime environment we are in. You can customize this by +// setting the ENVIRONMENT setting at compile time (see settings.js). + +var ENVIRONMENT_IS_WEB = false; +var ENVIRONMENT_IS_WORKER = false; +var ENVIRONMENT_IS_NODE = false; +var ENVIRONMENT_IS_SHELL = false; +ENVIRONMENT_IS_WEB = typeof window === 'object'; +ENVIRONMENT_IS_WORKER = typeof importScripts === 'function'; +ENVIRONMENT_IS_NODE = typeof process === 'object' && typeof require === 'function' && !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_WORKER; +ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; + +if (Module['ENVIRONMENT']) { + throw new Error('Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -s ENVIRONMENT=web or -s ENVIRONMENT=node)'); +} + + +// Three configurations we can be running in: +// 1) We could be the application main() thread running in the main JS UI thread. (ENVIRONMENT_IS_WORKER == false and ENVIRONMENT_IS_PTHREAD == false) +// 2) We could be the application main() thread proxied to worker. (with Emscripten -s PROXY_TO_WORKER=1) (ENVIRONMENT_IS_WORKER == true, ENVIRONMENT_IS_PTHREAD == false) +// 3) We could be an application pthread running in a worker. (ENVIRONMENT_IS_WORKER == true and ENVIRONMENT_IS_PTHREAD == true) + + + + +// `/` should be present at the end if `scriptDirectory` is not empty +var scriptDirectory = ''; +function locateFile(path) { + if (Module['locateFile']) { + return Module['locateFile'](path, scriptDirectory); + } else { + return scriptDirectory + path; + } +} + +if (ENVIRONMENT_IS_NODE) { + scriptDirectory = __dirname + '/'; + + // Expose functionality in the same simple way that the shells work + // Note that we pollute the global namespace here, otherwise we break in node + var nodeFS; + var nodePath; + + Module['read'] = function shell_read(filename, binary) { + var ret; + if (!nodeFS) nodeFS = require('fs'); + if (!nodePath) nodePath = require('path'); + filename = nodePath['normalize'](filename); + ret = nodeFS['readFileSync'](filename); + return binary ? ret : ret.toString(); + }; + + Module['readBinary'] = function readBinary(filename) { + var ret = Module['read'](filename, true); + if (!ret.buffer) { + ret = new Uint8Array(ret); + } + assert(ret.buffer); + return ret; + }; + + if (process['argv'].length > 1) { + Module['thisProgram'] = process['argv'][1].replace(/\\/g, '/'); + } + + Module['arguments'] = process['argv'].slice(2); + + if (typeof module !== 'undefined') { + module['exports'] = Module; + } + + process['on']('uncaughtException', function(ex) { + // suppress ExitStatus exceptions from showing an error + if (!(ex instanceof ExitStatus)) { + throw ex; + } + }); + // Currently node will swallow unhandled rejections, but this behavior is + // deprecated, and in the future it will exit with error status. + process['on']('unhandledRejection', abort); + + Module['quit'] = function(status) { + process['exit'](status); + }; + + Module['inspect'] = function () { return '[Emscripten Module object]'; }; +} else +if (ENVIRONMENT_IS_SHELL) { + + + if (typeof read != 'undefined') { + Module['read'] = function shell_read(f) { + return read(f); + }; + } + + Module['readBinary'] = function readBinary(f) { + var data; + if (typeof readbuffer === 'function') { + return new Uint8Array(readbuffer(f)); + } + data = read(f, 'binary'); + assert(typeof data === 'object'); + return data; + }; + + if (typeof scriptArgs != 'undefined') { + Module['arguments'] = scriptArgs; + } else if (typeof arguments != 'undefined') { + Module['arguments'] = arguments; + } + + if (typeof quit === 'function') { + Module['quit'] = function(status) { + quit(status); + } + } +} else +if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { + if (ENVIRONMENT_IS_WORKER) { // Check worker, not web, since window could be polyfilled + scriptDirectory = self.location.href; + } else if (document.currentScript) { // web + scriptDirectory = document.currentScript.src; + } + // blob urls look like blob:http://site.com/etc/etc and we cannot infer anything from them. + // otherwise, slice off the final part of the url to find the script directory. + // if scriptDirectory does not contain a slash, lastIndexOf will return -1, + // and scriptDirectory will correctly be replaced with an empty string. + if (scriptDirectory.indexOf('blob:') !== 0) { + scriptDirectory = scriptDirectory.substr(0, scriptDirectory.lastIndexOf('/')+1); + } else { + scriptDirectory = ''; + } + + + Module['read'] = function shell_read(url) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); + xhr.send(null); + return xhr.responseText; + }; + + if (ENVIRONMENT_IS_WORKER) { + Module['readBinary'] = function readBinary(url) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); + xhr.responseType = 'arraybuffer'; + xhr.send(null); + return new Uint8Array(xhr.response); + }; + } + + Module['readAsync'] = function readAsync(url, onload, onerror) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.onload = function xhr_onload() { + if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0 + onload(xhr.response); + return; + } + onerror(); + }; + xhr.onerror = onerror; + xhr.send(null); + }; + + Module['setWindowTitle'] = function(title) { document.title = title }; +} else +{ + throw new Error('environment detection error'); +} + +// Set up the out() and err() hooks, which are how we can print to stdout or +// stderr, respectively. +// If the user provided Module.print or printErr, use that. Otherwise, +// console.log is checked first, as 'print' on the web will open a print dialogue +// printErr is preferable to console.warn (works better in shells) +// bind(console) is necessary to fix IE/Edge closed dev tools panel behavior. +var out = Module['print'] || (typeof console !== 'undefined' ? console.log.bind(console) : (typeof print !== 'undefined' ? print : null)); +var err = Module['printErr'] || (typeof printErr !== 'undefined' ? printErr : ((typeof console !== 'undefined' && console.warn.bind(console)) || out)); + +// Merge back in the overrides +for (key in moduleOverrides) { + if (moduleOverrides.hasOwnProperty(key)) { + Module[key] = moduleOverrides[key]; + } +} +// Free the object hierarchy contained in the overrides, this lets the GC +// reclaim data used e.g. in memoryInitializerRequest, which is a large typed array. +moduleOverrides = undefined; + +// perform assertions in shell.js after we set up out() and err(), as otherwise if an assertion fails it cannot print the message +assert(typeof Module['memoryInitializerPrefixURL'] === 'undefined', 'Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead'); +assert(typeof Module['pthreadMainPrefixURL'] === 'undefined', 'Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead'); +assert(typeof Module['cdInitializerPrefixURL'] === 'undefined', 'Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead'); +assert(typeof Module['filePackagePrefixURL'] === 'undefined', 'Module.filePackagePrefixURL option was removed, use Module.locateFile instead'); + + + +// Copyright 2017 The Emscripten Authors. All rights reserved. +// Emscripten is available under two separate licenses, the MIT license and the +// University of Illinois/NCSA Open Source License. Both these licenses can be +// found in the LICENSE file. + +// {{PREAMBLE_ADDITIONS}} + +var STACK_ALIGN = 16; + +// stack management, and other functionality that is provided by the compiled code, +// should not be used before it is ready +stackSave = stackRestore = stackAlloc = function() { + abort('cannot use the stack before compiled code is ready to run, and has provided stack access'); +}; + +function staticAlloc(size) { + abort('staticAlloc is no longer available at runtime; instead, perform static allocations at compile time (using makeStaticAlloc)'); +} + +function dynamicAlloc(size) { + assert(DYNAMICTOP_PTR); + var ret = HEAP32[DYNAMICTOP_PTR>>2]; + var end = (ret + size + 15) & -16; + if (end <= _emscripten_get_heap_size()) { + HEAP32[DYNAMICTOP_PTR>>2] = end; + } else { + return 0; + } + return ret; +} + +function alignMemory(size, factor) { + if (!factor) factor = STACK_ALIGN; // stack alignment (16-byte) by default + return Math.ceil(size / factor) * factor; +} + +function getNativeTypeSize(type) { + switch (type) { + case 'i1': case 'i8': return 1; + case 'i16': return 2; + case 'i32': return 4; + case 'i64': return 8; + case 'float': return 4; + case 'double': return 8; + default: { + if (type[type.length-1] === '*') { + return 4; // A pointer + } else if (type[0] === 'i') { + var bits = parseInt(type.substr(1)); + assert(bits % 8 === 0, 'getNativeTypeSize invalid bits ' + bits + ', type ' + type); + return bits / 8; + } else { + return 0; + } + } + } +} + +function warnOnce(text) { + if (!warnOnce.shown) warnOnce.shown = {}; + if (!warnOnce.shown[text]) { + warnOnce.shown[text] = 1; + err(text); + } +} + +var asm2wasmImports = { // special asm2wasm imports + "f64-rem": function(x, y) { + return x % y; + }, + "debugger": function() { + debugger; + } +}; + + + +var jsCallStartIndex = 1; +var functionPointers = new Array(0); + +// Wraps a JS function as a wasm function with a given signature. +// In the future, we may get a WebAssembly.Function constructor. Until then, +// we create a wasm module that takes the JS function as an import with a given +// signature, and re-exports that as a wasm function. +function convertJsFunctionToWasm(func, sig) { + // The module is static, with the exception of the type section, which is + // generated based on the signature passed in. + var typeSection = [ + 0x01, // id: section, + 0x00, // length: 0 (placeholder) + 0x01, // count: 1 + 0x60, // form: func + ]; + var sigRet = sig.slice(0, 1); + var sigParam = sig.slice(1); + var typeCodes = { + 'i': 0x7f, // i32 + 'j': 0x7e, // i64 + 'f': 0x7d, // f32 + 'd': 0x7c, // f64 + }; + + // Parameters, length + signatures + typeSection.push(sigParam.length); + for (var i = 0; i < sigParam.length; ++i) { + typeSection.push(typeCodes[sigParam[i]]); + } + + // Return values, length + signatures + // With no multi-return in MVP, either 0 (void) or 1 (anything else) + if (sigRet == 'v') { + typeSection.push(0x00); + } else { + typeSection = typeSection.concat([0x01, typeCodes[sigRet]]); + } + + // Write the overall length of the type section back into the section header + // (excepting the 2 bytes for the section id and length) + typeSection[1] = typeSection.length - 2; + + // Rest of the module is static + var bytes = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, // magic ("\0asm") + 0x01, 0x00, 0x00, 0x00, // version: 1 + ].concat(typeSection, [ + 0x02, 0x07, // import section + // (import "e" "f" (func 0 (type 0))) + 0x01, 0x01, 0x65, 0x01, 0x66, 0x00, 0x00, + 0x07, 0x05, // export section + // (export "f" (func 0 (type 0))) + 0x01, 0x01, 0x66, 0x00, 0x00, + ])); + + // We can compile this wasm module synchronously because it is very small. + // This accepts an import (at "e.f"), that it reroutes to an export (at "f") + var module = new WebAssembly.Module(bytes); + var instance = new WebAssembly.Instance(module, { + e: { + f: func + } + }); + var wrappedFunc = instance.exports.f; + return wrappedFunc; +} + +// Add a wasm function to the table. +function addFunctionWasm(func, sig) { + var table = wasmTable; + var ret = table.length; + + // Grow the table + try { + table.grow(1); + } catch (err) { + if (!err instanceof RangeError) { + throw err; + } + throw 'Unable to grow wasm table. Use a higher value for RESERVED_FUNCTION_POINTERS or set ALLOW_TABLE_GROWTH.'; + } + + // Insert new element + try { + // Attempting to call this with JS function will cause of table.set() to fail + table.set(ret, func); + } catch (err) { + if (!err instanceof TypeError) { + throw err; + } + assert(typeof sig !== 'undefined', 'Missing signature argument to addFunction'); + var wrapped = convertJsFunctionToWasm(func, sig); + table.set(ret, wrapped); + } + + return ret; +} + +function removeFunctionWasm(index) { + // TODO(sbc): Look into implementing this to allow re-using of table slots +} + +// 'sig' parameter is required for the llvm backend but only when func is not +// already a WebAssembly function. +function addFunction(func, sig) { + + + var base = 0; + for (var i = base; i < base + 0; i++) { + if (!functionPointers[i]) { + functionPointers[i] = func; + return jsCallStartIndex + i; + } + } + throw 'Finished up all reserved function pointers. Use a higher value for RESERVED_FUNCTION_POINTERS.'; + +} + +function removeFunction(index) { + + functionPointers[index-jsCallStartIndex] = null; +} + +var funcWrappers = {}; + +function getFuncWrapper(func, sig) { + if (!func) return; // on null pointer, return undefined + assert(sig); + if (!funcWrappers[sig]) { + funcWrappers[sig] = {}; + } + var sigCache = funcWrappers[sig]; + if (!sigCache[func]) { + // optimize away arguments usage in common cases + if (sig.length === 1) { + sigCache[func] = function dynCall_wrapper() { + return dynCall(sig, func); + }; + } else if (sig.length === 2) { + sigCache[func] = function dynCall_wrapper(arg) { + return dynCall(sig, func, [arg]); + }; + } else { + // general case + sigCache[func] = function dynCall_wrapper() { + return dynCall(sig, func, Array.prototype.slice.call(arguments)); + }; + } + } + return sigCache[func]; +} + + +function makeBigInt(low, high, unsigned) { + return unsigned ? ((+((low>>>0)))+((+((high>>>0)))*4294967296.0)) : ((+((low>>>0)))+((+((high|0)))*4294967296.0)); +} + +function dynCall(sig, ptr, args) { + if (args && args.length) { + assert(args.length == sig.length-1); + assert(('dynCall_' + sig) in Module, 'bad function pointer type - no table for sig \'' + sig + '\''); + return Module['dynCall_' + sig].apply(null, [ptr].concat(args)); + } else { + assert(sig.length == 1); + assert(('dynCall_' + sig) in Module, 'bad function pointer type - no table for sig \'' + sig + '\''); + return Module['dynCall_' + sig].call(null, ptr); + } +} + +var tempRet0 = 0; + +var setTempRet0 = function(value) { + tempRet0 = value; +} + +var getTempRet0 = function() { + return tempRet0; +} + +function getCompilerSetting(name) { + throw 'You must build with -s RETAIN_COMPILER_SETTINGS=1 for getCompilerSetting or emscripten_get_compiler_setting to work'; +} + +var Runtime = { + // helpful errors + getTempRet0: function() { abort('getTempRet0() is now a top-level function, after removing the Runtime object. Remove "Runtime."') }, + staticAlloc: function() { abort('staticAlloc() is now a top-level function, after removing the Runtime object. Remove "Runtime."') }, + stackAlloc: function() { abort('stackAlloc() is now a top-level function, after removing the Runtime object. Remove "Runtime."') }, +}; + +// The address globals begin at. Very low in memory, for code size and optimization opportunities. +// Above 0 is static memory, starting with globals. +// Then the stack. +// Then 'dynamic' memory for sbrk. +var GLOBAL_BASE = 1024; + + + + +// === Preamble library stuff === + +// Documentation for the public APIs defined in this file must be updated in: +// site/source/docs/api_reference/preamble.js.rst +// A prebuilt local version of the documentation is available at: +// site/build/text/docs/api_reference/preamble.js.txt +// You can also build docs locally as HTML or other formats in site/ +// An online HTML version (which may be of a different version of Emscripten) +// is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html + + +if (typeof WebAssembly !== 'object') { + abort('No WebAssembly support found. Build with -s WASM=0 to target JavaScript instead.'); +} + + +/** @type {function(number, string, boolean=)} */ +function getValue(ptr, type, noSafe) { + type = type || 'i8'; + if (type.charAt(type.length-1) === '*') type = 'i32'; // pointers are 32-bit + switch(type) { + case 'i1': return HEAP8[((ptr)>>0)]; + case 'i8': return HEAP8[((ptr)>>0)]; + case 'i16': return HEAP16[((ptr)>>1)]; + case 'i32': return HEAP32[((ptr)>>2)]; + case 'i64': return HEAP32[((ptr)>>2)]; + case 'float': return HEAPF32[((ptr)>>2)]; + case 'double': return HEAPF64[((ptr)>>3)]; + default: abort('invalid type for getValue: ' + type); + } + return null; +} + + + + +// Wasm globals + +var wasmMemory; + +// Potentially used for direct table calls. +var wasmTable; + + +//======================================== +// Runtime essentials +//======================================== + +// whether we are quitting the application. no code should run after this. +// set in exit() and abort() +var ABORT = false; + +// set by exit() and abort(). Passed to 'onExit' handler. +// NOTE: This is also used as the process return code code in shell environments +// but only when noExitRuntime is false. +var EXITSTATUS = 0; + +/** @type {function(*, string=)} */ +function assert(condition, text) { + if (!condition) { + abort('Assertion failed: ' + text); + } +} + +// Returns the C function with a specified identifier (for C++, you need to do manual name mangling) +function getCFunc(ident) { + var func = Module['_' + ident]; // closure exported function + assert(func, 'Cannot call unknown function ' + ident + ', make sure it is exported'); + return func; +} + +// C calling interface. +function ccall(ident, returnType, argTypes, args, opts) { + // For fast lookup of conversion functions + var toC = { + 'string': function(str) { + var ret = 0; + if (str !== null && str !== undefined && str !== 0) { // null string + // at most 4 bytes per UTF-8 code point, +1 for the trailing '\0' + var len = (str.length << 2) + 1; + ret = stackAlloc(len); + stringToUTF8(str, ret, len); + } + return ret; + }, + 'array': function(arr) { + var ret = stackAlloc(arr.length); + writeArrayToMemory(arr, ret); + return ret; + } + }; + + function convertReturnValue(ret) { + if (returnType === 'string') return UTF8ToString(ret); + if (returnType === 'boolean') return Boolean(ret); + return ret; + } + + var func = getCFunc(ident); + var cArgs = []; + var stack = 0; + assert(returnType !== 'array', 'Return type should not be "array".'); + if (args) { + for (var i = 0; i < args.length; i++) { + var converter = toC[argTypes[i]]; + if (converter) { + if (stack === 0) stack = stackSave(); + cArgs[i] = converter(args[i]); + } else { + cArgs[i] = args[i]; + } + } + } + var ret = func.apply(null, cArgs); + ret = convertReturnValue(ret); + if (stack !== 0) stackRestore(stack); + return ret; +} + +function cwrap(ident, returnType, argTypes, opts) { + return function() { + return ccall(ident, returnType, argTypes, arguments, opts); + } +} + +/** @type {function(number, number, string, boolean=)} */ +function setValue(ptr, value, type, noSafe) { + type = type || 'i8'; + if (type.charAt(type.length-1) === '*') type = 'i32'; // pointers are 32-bit + switch(type) { + case 'i1': HEAP8[((ptr)>>0)]=value; break; + case 'i8': HEAP8[((ptr)>>0)]=value; break; + case 'i16': HEAP16[((ptr)>>1)]=value; break; + case 'i32': HEAP32[((ptr)>>2)]=value; break; + case 'i64': (tempI64 = [value>>>0,(tempDouble=value,(+(Math_abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? ((Math_min((+(Math_floor((tempDouble)/4294967296.0))), 4294967295.0))|0)>>>0 : (~~((+(Math_ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)],HEAP32[((ptr)>>2)]=tempI64[0],HEAP32[(((ptr)+(4))>>2)]=tempI64[1]); break; + case 'float': HEAPF32[((ptr)>>2)]=value; break; + case 'double': HEAPF64[((ptr)>>3)]=value; break; + default: abort('invalid type for setValue: ' + type); + } +} + +var ALLOC_NORMAL = 0; // Tries to use _malloc() +var ALLOC_STACK = 1; // Lives for the duration of the current function call +var ALLOC_DYNAMIC = 2; // Cannot be freed except through sbrk +var ALLOC_NONE = 3; // Do not allocate + +// allocate(): This is for internal use. You can use it yourself as well, but the interface +// is a little tricky (see docs right below). The reason is that it is optimized +// for multiple syntaxes to save space in generated code. So you should +// normally not use allocate(), and instead allocate memory using _malloc(), +// initialize it with setValue(), and so forth. +// @slab: An array of data, or a number. If a number, then the size of the block to allocate, +// in *bytes* (note that this is sometimes confusing: the next parameter does not +// affect this!) +// @types: Either an array of types, one for each byte (or 0 if no type at that position), +// or a single type which is used for the entire block. This only matters if there +// is initial data - if @slab is a number, then this does not matter at all and is +// ignored. +// @allocator: How to allocate memory, see ALLOC_* +/** @type {function((TypedArray|Array|number), string, number, number=)} */ +function allocate(slab, types, allocator, ptr) { + var zeroinit, size; + if (typeof slab === 'number') { + zeroinit = true; + size = slab; + } else { + zeroinit = false; + size = slab.length; + } + + var singleType = typeof types === 'string' ? types : null; + + var ret; + if (allocator == ALLOC_NONE) { + ret = ptr; + } else { + ret = [_malloc, + stackAlloc, + dynamicAlloc][allocator](Math.max(size, singleType ? 1 : types.length)); + } + + if (zeroinit) { + var stop; + ptr = ret; + assert((ret & 3) == 0); + stop = ret + (size & ~3); + for (; ptr < stop; ptr += 4) { + HEAP32[((ptr)>>2)]=0; + } + stop = ret + size; + while (ptr < stop) { + HEAP8[((ptr++)>>0)]=0; + } + return ret; + } + + if (singleType === 'i8') { + if (slab.subarray || slab.slice) { + HEAPU8.set(/** @type {!Uint8Array} */ (slab), ret); + } else { + HEAPU8.set(new Uint8Array(slab), ret); + } + return ret; + } + + var i = 0, type, typeSize, previousType; + while (i < size) { + var curr = slab[i]; + + type = singleType || types[i]; + if (type === 0) { + i++; + continue; + } + assert(type, 'Must know what type to store in allocate!'); + + if (type == 'i64') type = 'i32'; // special case: we have one i32 here, and one i32 later + + setValue(ret+i, curr, type); + + // no need to look up size unless type changes, so cache it + if (previousType !== type) { + typeSize = getNativeTypeSize(type); + previousType = type; + } + i += typeSize; + } + + return ret; +} + +// Allocate memory during any stage of startup - static memory early on, dynamic memory later, malloc when ready +function getMemory(size) { + if (!runtimeInitialized) return dynamicAlloc(size); + return _malloc(size); +} + + + + +/** @type {function(number, number=)} */ +function Pointer_stringify(ptr, length) { + abort("this function has been removed - you should use UTF8ToString(ptr, maxBytesToRead) instead!"); +} + +// Given a pointer 'ptr' to a null-terminated ASCII-encoded string in the emscripten HEAP, returns +// a copy of that string as a Javascript String object. + +function AsciiToString(ptr) { + var str = ''; + while (1) { + var ch = HEAPU8[((ptr++)>>0)]; + if (!ch) return str; + str += String.fromCharCode(ch); + } +} + +// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', +// null-terminated and encoded in ASCII form. The copy will require at most str.length+1 bytes of space in the HEAP. + +function stringToAscii(str, outPtr) { + return writeAsciiToMemory(str, outPtr, false); +} + + +// Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the given array that contains uint8 values, returns +// a copy of that string as a Javascript String object. + +var UTF8Decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf8') : undefined; + +/** + * @param {number} idx + * @param {number=} maxBytesToRead + * @return {string} + */ +function UTF8ArrayToString(u8Array, idx, maxBytesToRead) { + var endIdx = idx + maxBytesToRead; + var endPtr = idx; + // TextDecoder needs to know the byte length in advance, it doesn't stop on null terminator by itself. + // Also, use the length info to avoid running tiny strings through TextDecoder, since .subarray() allocates garbage. + // (As a tiny code save trick, compare endPtr against endIdx using a negation, so that undefined means Infinity) + while (u8Array[endPtr] && !(endPtr >= endIdx)) ++endPtr; + + if (endPtr - idx > 16 && u8Array.subarray && UTF8Decoder) { + return UTF8Decoder.decode(u8Array.subarray(idx, endPtr)); + } else { + var str = ''; + // If building with TextDecoder, we have already computed the string length above, so test loop end condition against that + while (idx < endPtr) { + // For UTF8 byte structure, see: + // http://en.wikipedia.org/wiki/UTF-8#Description + // https://www.ietf.org/rfc/rfc2279.txt + // https://tools.ietf.org/html/rfc3629 + var u0 = u8Array[idx++]; + if (!(u0 & 0x80)) { str += String.fromCharCode(u0); continue; } + var u1 = u8Array[idx++] & 63; + if ((u0 & 0xE0) == 0xC0) { str += String.fromCharCode(((u0 & 31) << 6) | u1); continue; } + var u2 = u8Array[idx++] & 63; + if ((u0 & 0xF0) == 0xE0) { + u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; + } else { + if ((u0 & 0xF8) != 0xF0) warnOnce('Invalid UTF-8 leading byte 0x' + u0.toString(16) + ' encountered when deserializing a UTF-8 string on the asm.js/wasm heap to a JS string!'); + u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (u8Array[idx++] & 63); + } + + if (u0 < 0x10000) { + str += String.fromCharCode(u0); + } else { + var ch = u0 - 0x10000; + str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); + } + } + } + return str; +} + +// Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the emscripten HEAP, returns a +// copy of that string as a Javascript String object. +// maxBytesToRead: an optional length that specifies the maximum number of bytes to read. You can omit +// this parameter to scan the string until the first \0 byte. If maxBytesToRead is +// passed, and the string at [ptr, ptr+maxBytesToReadr[ contains a null byte in the +// middle, then the string will cut short at that byte index (i.e. maxBytesToRead will +// not produce a string of exact length [ptr, ptr+maxBytesToRead[) +// N.B. mixing frequent uses of UTF8ToString() with and without maxBytesToRead may +// throw JS JIT optimizations off, so it is worth to consider consistently using one +// style or the other. +/** + * @param {number} ptr + * @param {number=} maxBytesToRead + * @return {string} + */ +function UTF8ToString(ptr, maxBytesToRead) { + return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : ''; +} + +// Copies the given Javascript String object 'str' to the given byte array at address 'outIdx', +// encoded in UTF8 form and null-terminated. The copy will require at most str.length*4+1 bytes of space in the HEAP. +// Use the function lengthBytesUTF8 to compute the exact number of bytes (excluding null terminator) that this function will write. +// Parameters: +// str: the Javascript string to copy. +// outU8Array: the array to copy to. Each index in this array is assumed to be one 8-byte element. +// outIdx: The starting offset in the array to begin the copying. +// maxBytesToWrite: The maximum number of bytes this function can write to the array. +// This count should include the null terminator, +// i.e. if maxBytesToWrite=1, only the null terminator will be written and nothing else. +// maxBytesToWrite=0 does not write any bytes to the output, not even the null terminator. +// Returns the number of bytes written, EXCLUDING the null terminator. + +function stringToUTF8Array(str, outU8Array, outIdx, maxBytesToWrite) { + if (!(maxBytesToWrite > 0)) // Parameter maxBytesToWrite is not optional. Negative values, 0, null, undefined and false each don't write out any bytes. + return 0; + + var startIdx = outIdx; + var endIdx = outIdx + maxBytesToWrite - 1; // -1 for string null terminator. + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! So decode UTF16->UTF32->UTF8. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description and https://www.ietf.org/rfc/rfc2279.txt and https://tools.ietf.org/html/rfc3629 + var u = str.charCodeAt(i); // possibly a lead surrogate + if (u >= 0xD800 && u <= 0xDFFF) { + var u1 = str.charCodeAt(++i); + u = 0x10000 + ((u & 0x3FF) << 10) | (u1 & 0x3FF); + } + if (u <= 0x7F) { + if (outIdx >= endIdx) break; + outU8Array[outIdx++] = u; + } else if (u <= 0x7FF) { + if (outIdx + 1 >= endIdx) break; + outU8Array[outIdx++] = 0xC0 | (u >> 6); + outU8Array[outIdx++] = 0x80 | (u & 63); + } else if (u <= 0xFFFF) { + if (outIdx + 2 >= endIdx) break; + outU8Array[outIdx++] = 0xE0 | (u >> 12); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); + outU8Array[outIdx++] = 0x80 | (u & 63); + } else { + if (outIdx + 3 >= endIdx) break; + if (u >= 0x200000) warnOnce('Invalid Unicode code point 0x' + u.toString(16) + ' encountered when serializing a JS string to an UTF-8 string on the asm.js/wasm heap! (Valid unicode code points should be in range 0-0x1FFFFF).'); + outU8Array[outIdx++] = 0xF0 | (u >> 18); + outU8Array[outIdx++] = 0x80 | ((u >> 12) & 63); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); + outU8Array[outIdx++] = 0x80 | (u & 63); + } + } + // Null-terminate the pointer to the buffer. + outU8Array[outIdx] = 0; + return outIdx - startIdx; +} + +// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', +// null-terminated and encoded in UTF8 form. The copy will require at most str.length*4+1 bytes of space in the HEAP. +// Use the function lengthBytesUTF8 to compute the exact number of bytes (excluding null terminator) that this function will write. +// Returns the number of bytes written, EXCLUDING the null terminator. + +function stringToUTF8(str, outPtr, maxBytesToWrite) { + assert(typeof maxBytesToWrite == 'number', 'stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); + return stringToUTF8Array(str, HEAPU8,outPtr, maxBytesToWrite); +} + +// Returns the number of bytes the given Javascript string takes if encoded as a UTF8 byte array, EXCLUDING the null terminator byte. +function lengthBytesUTF8(str) { + var len = 0; + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! So decode UTF16->UTF32->UTF8. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + var u = str.charCodeAt(i); // possibly a lead surrogate + if (u >= 0xD800 && u <= 0xDFFF) u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); + if (u <= 0x7F) ++len; + else if (u <= 0x7FF) len += 2; + else if (u <= 0xFFFF) len += 3; + else len += 4; + } + return len; +} + + +// Given a pointer 'ptr' to a null-terminated UTF16LE-encoded string in the emscripten HEAP, returns +// a copy of that string as a Javascript String object. + +var UTF16Decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-16le') : undefined; +function UTF16ToString(ptr) { + assert(ptr % 2 == 0, 'Pointer passed to UTF16ToString must be aligned to two bytes!'); + var endPtr = ptr; + // TextDecoder needs to know the byte length in advance, it doesn't stop on null terminator by itself. + // Also, use the length info to avoid running tiny strings through TextDecoder, since .subarray() allocates garbage. + var idx = endPtr >> 1; + while (HEAP16[idx]) ++idx; + endPtr = idx << 1; + + if (endPtr - ptr > 32 && UTF16Decoder) { + return UTF16Decoder.decode(HEAPU8.subarray(ptr, endPtr)); + } else { + var i = 0; + + var str = ''; + while (1) { + var codeUnit = HEAP16[(((ptr)+(i*2))>>1)]; + if (codeUnit == 0) return str; + ++i; + // fromCharCode constructs a character from a UTF-16 code unit, so we can pass the UTF16 string right through. + str += String.fromCharCode(codeUnit); + } + } +} + +// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', +// null-terminated and encoded in UTF16 form. The copy will require at most str.length*4+2 bytes of space in the HEAP. +// Use the function lengthBytesUTF16() to compute the exact number of bytes (excluding null terminator) that this function will write. +// Parameters: +// str: the Javascript string to copy. +// outPtr: Byte address in Emscripten HEAP where to write the string to. +// maxBytesToWrite: The maximum number of bytes this function can write to the array. This count should include the null +// terminator, i.e. if maxBytesToWrite=2, only the null terminator will be written and nothing else. +// maxBytesToWrite<2 does not write any bytes to the output, not even the null terminator. +// Returns the number of bytes written, EXCLUDING the null terminator. + +function stringToUTF16(str, outPtr, maxBytesToWrite) { + assert(outPtr % 2 == 0, 'Pointer passed to stringToUTF16 must be aligned to two bytes!'); + assert(typeof maxBytesToWrite == 'number', 'stringToUTF16(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); + // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. + if (maxBytesToWrite === undefined) { + maxBytesToWrite = 0x7FFFFFFF; + } + if (maxBytesToWrite < 2) return 0; + maxBytesToWrite -= 2; // Null terminator. + var startPtr = outPtr; + var numCharsToWrite = (maxBytesToWrite < str.length*2) ? (maxBytesToWrite / 2) : str.length; + for (var i = 0; i < numCharsToWrite; ++i) { + // charCodeAt returns a UTF-16 encoded code unit, so it can be directly written to the HEAP. + var codeUnit = str.charCodeAt(i); // possibly a lead surrogate + HEAP16[((outPtr)>>1)]=codeUnit; + outPtr += 2; + } + // Null-terminate the pointer to the HEAP. + HEAP16[((outPtr)>>1)]=0; + return outPtr - startPtr; +} + +// Returns the number of bytes the given Javascript string takes if encoded as a UTF16 byte array, EXCLUDING the null terminator byte. + +function lengthBytesUTF16(str) { + return str.length*2; +} + +function UTF32ToString(ptr) { + assert(ptr % 4 == 0, 'Pointer passed to UTF32ToString must be aligned to four bytes!'); + var i = 0; + + var str = ''; + while (1) { + var utf32 = HEAP32[(((ptr)+(i*4))>>2)]; + if (utf32 == 0) + return str; + ++i; + // Gotcha: fromCharCode constructs a character from a UTF-16 encoded code (pair), not from a Unicode code point! So encode the code point to UTF-16 for constructing. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + if (utf32 >= 0x10000) { + var ch = utf32 - 0x10000; + str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); + } else { + str += String.fromCharCode(utf32); + } + } +} + +// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', +// null-terminated and encoded in UTF32 form. The copy will require at most str.length*4+4 bytes of space in the HEAP. +// Use the function lengthBytesUTF32() to compute the exact number of bytes (excluding null terminator) that this function will write. +// Parameters: +// str: the Javascript string to copy. +// outPtr: Byte address in Emscripten HEAP where to write the string to. +// maxBytesToWrite: The maximum number of bytes this function can write to the array. This count should include the null +// terminator, i.e. if maxBytesToWrite=4, only the null terminator will be written and nothing else. +// maxBytesToWrite<4 does not write any bytes to the output, not even the null terminator. +// Returns the number of bytes written, EXCLUDING the null terminator. + +function stringToUTF32(str, outPtr, maxBytesToWrite) { + assert(outPtr % 4 == 0, 'Pointer passed to stringToUTF32 must be aligned to four bytes!'); + assert(typeof maxBytesToWrite == 'number', 'stringToUTF32(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); + // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. + if (maxBytesToWrite === undefined) { + maxBytesToWrite = 0x7FFFFFFF; + } + if (maxBytesToWrite < 4) return 0; + var startPtr = outPtr; + var endPtr = startPtr + maxBytesToWrite - 4; + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! We must decode the string to UTF-32 to the heap. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + var codeUnit = str.charCodeAt(i); // possibly a lead surrogate + if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) { + var trailSurrogate = str.charCodeAt(++i); + codeUnit = 0x10000 + ((codeUnit & 0x3FF) << 10) | (trailSurrogate & 0x3FF); + } + HEAP32[((outPtr)>>2)]=codeUnit; + outPtr += 4; + if (outPtr + 4 > endPtr) break; + } + // Null-terminate the pointer to the HEAP. + HEAP32[((outPtr)>>2)]=0; + return outPtr - startPtr; +} + +// Returns the number of bytes the given Javascript string takes if encoded as a UTF16 byte array, EXCLUDING the null terminator byte. + +function lengthBytesUTF32(str) { + var len = 0; + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! We must decode the string to UTF-32 to the heap. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + var codeUnit = str.charCodeAt(i); + if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) ++i; // possibly a lead surrogate, so skip over the tail surrogate. + len += 4; + } + + return len; +} + +// Allocate heap space for a JS string, and write it there. +// It is the responsibility of the caller to free() that memory. +function allocateUTF8(str) { + var size = lengthBytesUTF8(str) + 1; + var ret = _malloc(size); + if (ret) stringToUTF8Array(str, HEAP8, ret, size); + return ret; +} + +// Allocate stack space for a JS string, and write it there. +function allocateUTF8OnStack(str) { + var size = lengthBytesUTF8(str) + 1; + var ret = stackAlloc(size); + stringToUTF8Array(str, HEAP8, ret, size); + return ret; +} + +// Deprecated: This function should not be called because it is unsafe and does not provide +// a maximum length limit of how many bytes it is allowed to write. Prefer calling the +// function stringToUTF8Array() instead, which takes in a maximum length that can be used +// to be secure from out of bounds writes. +/** @deprecated */ +function writeStringToMemory(string, buffer, dontAddNull) { + warnOnce('writeStringToMemory is deprecated and should not be called! Use stringToUTF8() instead!'); + + var /** @type {number} */ lastChar, /** @type {number} */ end; + if (dontAddNull) { + // stringToUTF8Array always appends null. If we don't want to do that, remember the + // character that existed at the location where the null will be placed, and restore + // that after the write (below). + end = buffer + lengthBytesUTF8(string); + lastChar = HEAP8[end]; + } + stringToUTF8(string, buffer, Infinity); + if (dontAddNull) HEAP8[end] = lastChar; // Restore the value under the null character. +} + +function writeArrayToMemory(array, buffer) { + assert(array.length >= 0, 'writeArrayToMemory array must have a length (should be an array or typed array)') + HEAP8.set(array, buffer); +} + +function writeAsciiToMemory(str, buffer, dontAddNull) { + for (var i = 0; i < str.length; ++i) { + assert(str.charCodeAt(i) === str.charCodeAt(i)&0xff); + HEAP8[((buffer++)>>0)]=str.charCodeAt(i); + } + // Null-terminate the pointer to the HEAP. + if (!dontAddNull) HEAP8[((buffer)>>0)]=0; +} + + + + + +function demangle(func) { + warnOnce('warning: build with -s DEMANGLE_SUPPORT=1 to link in libcxxabi demangling'); + return func; +} + +function demangleAll(text) { + var regex = + /__Z[\w\d_]+/g; + return text.replace(regex, + function(x) { + var y = demangle(x); + return x === y ? x : (y + ' [' + x + ']'); + }); +} + +function jsStackTrace() { + var err = new Error(); + if (!err.stack) { + // IE10+ special cases: It does have callstack info, but it is only populated if an Error object is thrown, + // so try that as a special-case. + try { + throw new Error(0); + } catch(e) { + err = e; + } + if (!err.stack) { + return '(no stack trace available)'; + } + } + return err.stack.toString(); +} + +function stackTrace() { + var js = jsStackTrace(); + if (Module['extraStackTrace']) js += '\n' + Module['extraStackTrace'](); + return demangleAll(js); +} + + + +// Memory management + +var PAGE_SIZE = 16384; +var WASM_PAGE_SIZE = 65536; +var ASMJS_PAGE_SIZE = 16777216; + +function alignUp(x, multiple) { + if (x % multiple > 0) { + x += multiple - (x % multiple); + } + return x; +} + +var HEAP, +/** @type {ArrayBuffer} */ + buffer, +/** @type {Int8Array} */ + HEAP8, +/** @type {Uint8Array} */ + HEAPU8, +/** @type {Int16Array} */ + HEAP16, +/** @type {Uint16Array} */ + HEAPU16, +/** @type {Int32Array} */ + HEAP32, +/** @type {Uint32Array} */ + HEAPU32, +/** @type {Float32Array} */ + HEAPF32, +/** @type {Float64Array} */ + HEAPF64; + +function updateGlobalBufferViews() { + Module['HEAP8'] = HEAP8 = new Int8Array(buffer); + Module['HEAP16'] = HEAP16 = new Int16Array(buffer); + Module['HEAP32'] = HEAP32 = new Int32Array(buffer); + Module['HEAPU8'] = HEAPU8 = new Uint8Array(buffer); + Module['HEAPU16'] = HEAPU16 = new Uint16Array(buffer); + Module['HEAPU32'] = HEAPU32 = new Uint32Array(buffer); + Module['HEAPF32'] = HEAPF32 = new Float32Array(buffer); + Module['HEAPF64'] = HEAPF64 = new Float64Array(buffer); +} + + +var STATIC_BASE = 1024, + STACK_BASE = 12240, + STACKTOP = STACK_BASE, + STACK_MAX = 5255120, + DYNAMIC_BASE = 5255120, + DYNAMICTOP_PTR = 12208; + +assert(STACK_BASE % 16 === 0, 'stack must start aligned'); +assert(DYNAMIC_BASE % 16 === 0, 'heap must start aligned'); + + + +var TOTAL_STACK = 5242880; +if (Module['TOTAL_STACK']) assert(TOTAL_STACK === Module['TOTAL_STACK'], 'the stack size can no longer be determined at runtime') + +var INITIAL_TOTAL_MEMORY = Module['TOTAL_MEMORY'] || 16777216; +if (INITIAL_TOTAL_MEMORY < TOTAL_STACK) err('TOTAL_MEMORY should be larger than TOTAL_STACK, was ' + INITIAL_TOTAL_MEMORY + '! (TOTAL_STACK=' + TOTAL_STACK + ')'); + +// Initialize the runtime's memory +// check for full engine support (use string 'subarray' to avoid closure compiler confusion) +assert(typeof Int32Array !== 'undefined' && typeof Float64Array !== 'undefined' && Int32Array.prototype.subarray !== undefined && Int32Array.prototype.set !== undefined, + 'JS engine does not provide full typed array support'); + + + + + + + +// Use a provided buffer, if there is one, or else allocate a new one +if (Module['buffer']) { + buffer = Module['buffer']; + assert(buffer.byteLength === INITIAL_TOTAL_MEMORY, 'provided buffer should be ' + INITIAL_TOTAL_MEMORY + ' bytes, but it is ' + buffer.byteLength); +} else { + // Use a WebAssembly memory where available + if (typeof WebAssembly === 'object' && typeof WebAssembly.Memory === 'function') { + assert(INITIAL_TOTAL_MEMORY % WASM_PAGE_SIZE === 0); + wasmMemory = new WebAssembly.Memory({ 'initial': INITIAL_TOTAL_MEMORY / WASM_PAGE_SIZE, 'maximum': INITIAL_TOTAL_MEMORY / WASM_PAGE_SIZE }); + buffer = wasmMemory.buffer; + } else + { + buffer = new ArrayBuffer(INITIAL_TOTAL_MEMORY); + } + assert(buffer.byteLength === INITIAL_TOTAL_MEMORY); +} +updateGlobalBufferViews(); + + +HEAP32[DYNAMICTOP_PTR>>2] = DYNAMIC_BASE; + + +// Initializes the stack cookie. Called at the startup of main and at the startup of each thread in pthreads mode. +function writeStackCookie() { + assert((STACK_MAX & 3) == 0); + HEAPU32[(STACK_MAX >> 2)-1] = 0x02135467; + HEAPU32[(STACK_MAX >> 2)-2] = 0x89BACDFE; +} + +function checkStackCookie() { + if (HEAPU32[(STACK_MAX >> 2)-1] != 0x02135467 || HEAPU32[(STACK_MAX >> 2)-2] != 0x89BACDFE) { + abort('Stack overflow! Stack cookie has been overwritten, expected hex dwords 0x89BACDFE and 0x02135467, but received 0x' + HEAPU32[(STACK_MAX >> 2)-2].toString(16) + ' ' + HEAPU32[(STACK_MAX >> 2)-1].toString(16)); + } + // Also test the global address 0 for integrity. + if (HEAP32[0] !== 0x63736d65 /* 'emsc' */) throw 'Runtime error: The application has corrupted its heap memory area (address zero)!'; +} + +function abortStackOverflow(allocSize) { + abort('Stack overflow! Attempted to allocate ' + allocSize + ' bytes on the stack, but stack has only ' + (STACK_MAX - stackSave() + allocSize) + ' bytes available!'); +} + + + HEAP32[0] = 0x63736d65; /* 'emsc' */ + + + +// Endianness check (note: assumes compiler arch was little-endian) +HEAP16[1] = 0x6373; +if (HEAPU8[2] !== 0x73 || HEAPU8[3] !== 0x63) throw 'Runtime error: expected the system to be little-endian!'; + +function callRuntimeCallbacks(callbacks) { + while(callbacks.length > 0) { + var callback = callbacks.shift(); + if (typeof callback == 'function') { + callback(); + continue; + } + var func = callback.func; + if (typeof func === 'number') { + if (callback.arg === undefined) { + Module['dynCall_v'](func); + } else { + Module['dynCall_vi'](func, callback.arg); + } + } else { + func(callback.arg === undefined ? null : callback.arg); + } + } +} + +var __ATPRERUN__ = []; // functions called before the runtime is initialized +var __ATINIT__ = []; // functions called during startup +var __ATMAIN__ = []; // functions called when main() is to be run +var __ATEXIT__ = []; // functions called during shutdown +var __ATPOSTRUN__ = []; // functions called after the main() is called + +var runtimeInitialized = false; +var runtimeExited = false; + + +function preRun() { + // compatibility - merge in anything from Module['preRun'] at this time + if (Module['preRun']) { + if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; + while (Module['preRun'].length) { + addOnPreRun(Module['preRun'].shift()); + } + } + callRuntimeCallbacks(__ATPRERUN__); +} + +function ensureInitRuntime() { + checkStackCookie(); + if (runtimeInitialized) return; + runtimeInitialized = true; + + callRuntimeCallbacks(__ATINIT__); +} + +function preMain() { + checkStackCookie(); + + callRuntimeCallbacks(__ATMAIN__); +} + +function exitRuntime() { + checkStackCookie(); + runtimeExited = true; +} + +function postRun() { + checkStackCookie(); + // compatibility - merge in anything from Module['postRun'] at this time + if (Module['postRun']) { + if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; + while (Module['postRun'].length) { + addOnPostRun(Module['postRun'].shift()); + } + } + callRuntimeCallbacks(__ATPOSTRUN__); +} + +function addOnPreRun(cb) { + __ATPRERUN__.unshift(cb); +} + +function addOnInit(cb) { + __ATINIT__.unshift(cb); +} + +function addOnPreMain(cb) { + __ATMAIN__.unshift(cb); +} + +function addOnExit(cb) { +} + +function addOnPostRun(cb) { + __ATPOSTRUN__.unshift(cb); +} + +function unSign(value, bits, ignore) { + if (value >= 0) { + return value; + } + return bits <= 32 ? 2*Math.abs(1 << (bits-1)) + value // Need some trickery, since if bits == 32, we are right at the limit of the bits JS uses in bitshifts + : Math.pow(2, bits) + value; +} +function reSign(value, bits, ignore) { + if (value <= 0) { + return value; + } + var half = bits <= 32 ? Math.abs(1 << (bits-1)) // abs is needed if bits == 32 + : Math.pow(2, bits-1); + if (value >= half && (bits <= 32 || value > half)) { // for huge values, we can hit the precision limit and always get true here. so don't do that + // but, in general there is no perfect solution here. With 64-bit ints, we get rounding and errors + // TODO: In i64 mode 1, resign the two parts separately and safely + value = -2*half + value; // Cannot bitshift half, as it may be at the limit of the bits JS uses in bitshifts + } + return value; +} + + +assert(Math.imul, 'This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); +assert(Math.fround, 'This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); +assert(Math.clz32, 'This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); +assert(Math.trunc, 'This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); + +var Math_abs = Math.abs; +var Math_cos = Math.cos; +var Math_sin = Math.sin; +var Math_tan = Math.tan; +var Math_acos = Math.acos; +var Math_asin = Math.asin; +var Math_atan = Math.atan; +var Math_atan2 = Math.atan2; +var Math_exp = Math.exp; +var Math_log = Math.log; +var Math_sqrt = Math.sqrt; +var Math_ceil = Math.ceil; +var Math_floor = Math.floor; +var Math_pow = Math.pow; +var Math_imul = Math.imul; +var Math_fround = Math.fround; +var Math_round = Math.round; +var Math_min = Math.min; +var Math_max = Math.max; +var Math_clz32 = Math.clz32; +var Math_trunc = Math.trunc; + + + +// A counter of dependencies for calling run(). If we need to +// do asynchronous work before running, increment this and +// decrement it. Incrementing must happen in a place like +// Module.preRun (used by emcc to add file preloading). +// Note that you can add dependencies in preRun, even though +// it happens right before run - run will be postponed until +// the dependencies are met. +var runDependencies = 0; +var runDependencyWatcher = null; +var dependenciesFulfilled = null; // overridden to take different actions when all run dependencies are fulfilled +var runDependencyTracking = {}; + +function getUniqueRunDependency(id) { + var orig = id; + while (1) { + if (!runDependencyTracking[id]) return id; + id = orig + Math.random(); + } + return id; +} + +function addRunDependency(id) { + runDependencies++; + if (Module['monitorRunDependencies']) { + Module['monitorRunDependencies'](runDependencies); + } + if (id) { + assert(!runDependencyTracking[id]); + runDependencyTracking[id] = 1; + if (runDependencyWatcher === null && typeof setInterval !== 'undefined') { + // Check for missing dependencies every few seconds + runDependencyWatcher = setInterval(function() { + if (ABORT) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + return; + } + var shown = false; + for (var dep in runDependencyTracking) { + if (!shown) { + shown = true; + err('still waiting on run dependencies:'); + } + err('dependency: ' + dep); + } + if (shown) { + err('(end of list)'); + } + }, 10000); + } + } else { + err('warning: run dependency added without ID'); + } +} + +function removeRunDependency(id) { + runDependencies--; + if (Module['monitorRunDependencies']) { + Module['monitorRunDependencies'](runDependencies); + } + if (id) { + assert(runDependencyTracking[id]); + delete runDependencyTracking[id]; + } else { + err('warning: run dependency removed without ID'); + } + if (runDependencies == 0) { + if (runDependencyWatcher !== null) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + } + if (dependenciesFulfilled) { + var callback = dependenciesFulfilled; + dependenciesFulfilled = null; + callback(); // can add another dependenciesFulfilled + } + } +} + +Module["preloadedImages"] = {}; // maps url to image data +Module["preloadedAudios"] = {}; // maps url to audio data + + +var memoryInitializer = null; + + + +// show errors on likely calls to FS when it was not included +var FS = { + error: function() { + abort('Filesystem support (FS) was not included. The problem is that you are using files from JS, but files were not used from C/C++, so filesystem support was not auto-included. You can force-include filesystem support with -s FORCE_FILESYSTEM=1'); + }, + init: function() { FS.error() }, + createDataFile: function() { FS.error() }, + createPreloadedFile: function() { FS.error() }, + createLazyFile: function() { FS.error() }, + open: function() { FS.error() }, + mkdev: function() { FS.error() }, + registerDevice: function() { FS.error() }, + analyzePath: function() { FS.error() }, + loadFilesFromDB: function() { FS.error() }, + + ErrnoError: function ErrnoError() { FS.error() }, +}; +Module['FS_createDataFile'] = FS.createDataFile; +Module['FS_createPreloadedFile'] = FS.createPreloadedFile; + + + +// Copyright 2017 The Emscripten Authors. All rights reserved. +// Emscripten is available under two separate licenses, the MIT license and the +// University of Illinois/NCSA Open Source License. Both these licenses can be +// found in the LICENSE file. + +// Prefix of data URIs emitted by SINGLE_FILE and related options. +var dataURIPrefix = 'data:application/octet-stream;base64,'; + +// Indicates whether filename is a base64 data URI. +function isDataURI(filename) { + return String.prototype.startsWith ? + filename.startsWith(dataURIPrefix) : + filename.indexOf(dataURIPrefix) === 0; +} + + + + +var wasmBinaryFile = 'libmbe.wasm'; +if (!isDataURI(wasmBinaryFile)) { + wasmBinaryFile = locateFile(wasmBinaryFile); +} + +function getBinary() { + try { + if (Module['wasmBinary']) { + return new Uint8Array(Module['wasmBinary']); + } + if (Module['readBinary']) { + return Module['readBinary'](wasmBinaryFile); + } else { + throw "both async and sync fetching of the wasm failed"; + } + } + catch (err) { + abort(err); + } +} + +function getBinaryPromise() { + // if we don't have the binary yet, and have the Fetch api, use that + // in some environments, like Electron's render process, Fetch api may be present, but have a different context than expected, let's only use it on the Web + if (!Module['wasmBinary'] && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) && typeof fetch === 'function') { + return fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function(response) { + if (!response['ok']) { + throw "failed to load wasm binary file at '" + wasmBinaryFile + "'"; + } + return response['arrayBuffer'](); + }).catch(function () { + return getBinary(); + }); + } + // Otherwise, getBinary should be able to get it synchronously + return new Promise(function(resolve, reject) { + resolve(getBinary()); + }); +} + +// Create the wasm instance. +// Receives the wasm imports, returns the exports. +function createWasm(env) { + // prepare imports + var info = { + 'env': env + , + 'global': { + 'NaN': NaN, + 'Infinity': Infinity + }, + 'global.Math': Math, + 'asm2wasm': asm2wasmImports + }; + // Load the wasm module and create an instance of using native support in the JS engine. + // handle a generated wasm instance, receiving its exports and + // performing other necessary setup + function receiveInstance(instance, module) { + var exports = instance.exports; + Module['asm'] = exports; + removeRunDependency('wasm-instantiate'); + } + addRunDependency('wasm-instantiate'); + + // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback + // to manually instantiate the Wasm module themselves. This allows pages to run the instantiation parallel + // to any other async startup actions they are performing. + if (Module['instantiateWasm']) { + try { + return Module['instantiateWasm'](info, receiveInstance); + } catch(e) { + err('Module.instantiateWasm callback failed with error: ' + e); + return false; + } + } + + // Async compilation can be confusing when an error on the page overwrites Module + // (for example, if the order of elements is wrong, and the one defining Module is + // later), so we save Module and check it later. + var trueModule = Module; + function receiveInstantiatedSource(output) { + // 'output' is a WebAssemblyInstantiatedSource object which has both the module and instance. + // receiveInstance() will swap in the exports (to Module.asm) so they can be called + assert(Module === trueModule, 'the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?'); + trueModule = null; + // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. + // When the regression is fixed, can restore the above USE_PTHREADS-enabled path. + receiveInstance(output['instance']); + } + function instantiateArrayBuffer(receiver) { + getBinaryPromise().then(function(binary) { + return WebAssembly.instantiate(binary, info); + }).then(receiver, function(reason) { + err('failed to asynchronously prepare wasm: ' + reason); + abort(reason); + }); + } + // Prefer streaming instantiation if available. + if (!Module['wasmBinary'] && + typeof WebAssembly.instantiateStreaming === 'function' && + !isDataURI(wasmBinaryFile) && + typeof fetch === 'function') { + WebAssembly.instantiateStreaming(fetch(wasmBinaryFile, { credentials: 'same-origin' }), info) + .then(receiveInstantiatedSource, function(reason) { + // We expect the most common failure cause to be a bad MIME type for the binary, + // in which case falling back to ArrayBuffer instantiation should work. + err('wasm streaming compile failed: ' + reason); + err('falling back to ArrayBuffer instantiation'); + instantiateArrayBuffer(receiveInstantiatedSource); + }); + } else { + instantiateArrayBuffer(receiveInstantiatedSource); + } + return {}; // no exports yet; we'll fill them in later +} + +// Provide an "asm.js function" for the application, called to "link" the asm.js module. We instantiate +// the wasm module at that time, and it receives imports and provides exports and so forth, the app +// doesn't need to care that it is wasm or asm.js. + +Module['asm'] = function(global, env, providedBuffer) { + // memory was already allocated (so js could use the buffer) + env['memory'] = wasmMemory + ; + // import table + env['table'] = wasmTable = new WebAssembly.Table({ + 'initial': 14, + 'maximum': 14, + 'element': 'anyfunc' + }); + // With the wasm backend __memory_base and __table_base and only needed for + // relocatable output. + env['__memory_base'] = 1024; // tell the memory segments where to place themselves + // table starts at 0 by default (even in dynamic linking, for the main module) + env['__table_base'] = 0; + + var exports = createWasm(env); + assert(exports, 'binaryen setup failed (no wasm support?)'); + return exports; +}; + +// === Body === + +var ASM_CONSTS = []; + + + + + +// STATICTOP = STATIC_BASE + 11216; +/* global initializers */ /*__ATINIT__.push();*/ + + + + + + + + +/* no memory initializer */ +var tempDoublePtr = 12224 +assert(tempDoublePtr % 8 == 0); + +function copyTempFloat(ptr) { // functions, because inlining this code increases code size too much + HEAP8[tempDoublePtr] = HEAP8[ptr]; + HEAP8[tempDoublePtr+1] = HEAP8[ptr+1]; + HEAP8[tempDoublePtr+2] = HEAP8[ptr+2]; + HEAP8[tempDoublePtr+3] = HEAP8[ptr+3]; +} + +function copyTempDouble(ptr) { + HEAP8[tempDoublePtr] = HEAP8[ptr]; + HEAP8[tempDoublePtr+1] = HEAP8[ptr+1]; + HEAP8[tempDoublePtr+2] = HEAP8[ptr+2]; + HEAP8[tempDoublePtr+3] = HEAP8[ptr+3]; + HEAP8[tempDoublePtr+4] = HEAP8[ptr+4]; + HEAP8[tempDoublePtr+5] = HEAP8[ptr+5]; + HEAP8[tempDoublePtr+6] = HEAP8[ptr+6]; + HEAP8[tempDoublePtr+7] = HEAP8[ptr+7]; +} + +// {{PRE_LIBRARY}} + + + function ___lock() {} + + + var SYSCALLS={buffers:[null,[],[]],printChar:function(stream, curr) { + var buffer = SYSCALLS.buffers[stream]; + assert(buffer); + if (curr === 0 || curr === 10) { + (stream === 1 ? out : err)(UTF8ArrayToString(buffer, 0)); + buffer.length = 0; + } else { + buffer.push(curr); + } + },varargs:0,get:function(varargs) { + SYSCALLS.varargs += 4; + var ret = HEAP32[(((SYSCALLS.varargs)-(4))>>2)]; + return ret; + },getStr:function() { + var ret = UTF8ToString(SYSCALLS.get()); + return ret; + },get64:function() { + var low = SYSCALLS.get(), high = SYSCALLS.get(); + if (low >= 0) assert(high === 0); + else assert(high === -1); + return low; + },getZero:function() { + assert(SYSCALLS.get() === 0); + }};function ___syscall140(which, varargs) {SYSCALLS.varargs = varargs; + try { + // llseek + var stream = SYSCALLS.getStreamFromFD(), offset_high = SYSCALLS.get(), offset_low = SYSCALLS.get(), result = SYSCALLS.get(), whence = SYSCALLS.get(); + abort('it should not be possible to operate on streams when !SYSCALLS_REQUIRE_FILESYSTEM'); + return 0; + } catch (e) { + if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); + return -e.errno; + } + } + + + function flush_NO_FILESYSTEM() { + // flush anything remaining in the buffers during shutdown + var fflush = Module["_fflush"]; + if (fflush) fflush(0); + var buffers = SYSCALLS.buffers; + if (buffers[1].length) SYSCALLS.printChar(1, 10); + if (buffers[2].length) SYSCALLS.printChar(2, 10); + }function ___syscall146(which, varargs) {SYSCALLS.varargs = varargs; + try { + // writev + // hack to support printf in SYSCALLS_REQUIRE_FILESYSTEM=0 + var stream = SYSCALLS.get(), iov = SYSCALLS.get(), iovcnt = SYSCALLS.get(); + var ret = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAP32[(((iov)+(i*8))>>2)]; + var len = HEAP32[(((iov)+(i*8 + 4))>>2)]; + for (var j = 0; j < len; j++) { + SYSCALLS.printChar(stream, HEAPU8[ptr+j]); + } + ret += len; + } + return ret; + } catch (e) { + if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); + return -e.errno; + } + } + + function ___syscall54(which, varargs) {SYSCALLS.varargs = varargs; + try { + // ioctl + return 0; + } catch (e) { + if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); + return -e.errno; + } + } + + function ___syscall6(which, varargs) {SYSCALLS.varargs = varargs; + try { + // close + var stream = SYSCALLS.getStreamFromFD(); + abort('it should not be possible to operate on streams when !SYSCALLS_REQUIRE_FILESYSTEM'); + return 0; + } catch (e) { + if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); + return -e.errno; + } + } + + function ___unlock() {} + + function _emscripten_get_heap_size() { + return HEAP8.length; + } + + + function abortOnCannotGrowMemory(requestedSize) { + abort('Cannot enlarge memory arrays to size ' + requestedSize + ' bytes (OOM). Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value ' + HEAP8.length + ', (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 '); + }function _emscripten_resize_heap(requestedSize) { + abortOnCannotGrowMemory(requestedSize); + } + + + function _emscripten_memcpy_big(dest, src, num) { + HEAPU8.set(HEAPU8.subarray(src, src+num), dest); + } + + + + + + + function ___setErrNo(value) { + if (Module['___errno_location']) HEAP32[((Module['___errno_location']())>>2)]=value; + else err('failed to set errno from JS'); + return value; + } +var ASSERTIONS = true; + +// Copyright 2017 The Emscripten Authors. All rights reserved. +// Emscripten is available under two separate licenses, the MIT license and the +// University of Illinois/NCSA Open Source License. Both these licenses can be +// found in the LICENSE file. + +/** @type {function(string, boolean=, number=)} */ +function intArrayFromString(stringy, dontAddNull, length) { + var len = length > 0 ? length : lengthBytesUTF8(stringy)+1; + var u8array = new Array(len); + var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); + if (dontAddNull) u8array.length = numBytesWritten; + return u8array; +} + +function intArrayToString(array) { + var ret = []; + for (var i = 0; i < array.length; i++) { + var chr = array[i]; + if (chr > 0xFF) { + if (ASSERTIONS) { + assert(false, 'Character code ' + chr + ' (' + String.fromCharCode(chr) + ') at offset ' + i + ' not in 0x00-0xFF.'); + } + chr &= 0xFF; + } + ret.push(String.fromCharCode(chr)); + } + return ret.join(''); +} + + +// ASM_LIBRARY EXTERN PRIMITIVES: Int8Array,Int32Array + + +function nullFunc_ii(x) { err("Invalid function pointer called with signature 'ii'. Perhaps this is an invalid value (e.g. caused by calling a virtual method on a NULL pointer)? Or calling a function with an incorrect type, which will fail? (it is worth building your source files with -Werror (warnings are errors), as warnings can indicate undefined behavior which can cause this)"); err("Build with ASSERTIONS=2 for more info.");abort(x) } + +function nullFunc_iiii(x) { err("Invalid function pointer called with signature 'iiii'. Perhaps this is an invalid value (e.g. caused by calling a virtual method on a NULL pointer)? Or calling a function with an incorrect type, which will fail? (it is worth building your source files with -Werror (warnings are errors), as warnings can indicate undefined behavior which can cause this)"); err("Build with ASSERTIONS=2 for more info.");abort(x) } + +function nullFunc_jiji(x) { err("Invalid function pointer called with signature 'jiji'. Perhaps this is an invalid value (e.g. caused by calling a virtual method on a NULL pointer)? Or calling a function with an incorrect type, which will fail? (it is worth building your source files with -Werror (warnings are errors), as warnings can indicate undefined behavior which can cause this)"); err("Build with ASSERTIONS=2 for more info.");abort(x) } + +var asmGlobalArg = {} + +var asmLibraryArg = { + "abort": abort, + "setTempRet0": setTempRet0, + "getTempRet0": getTempRet0, + "abortStackOverflow": abortStackOverflow, + "nullFunc_ii": nullFunc_ii, + "nullFunc_iiii": nullFunc_iiii, + "nullFunc_jiji": nullFunc_jiji, + "___lock": ___lock, + "___setErrNo": ___setErrNo, + "___syscall140": ___syscall140, + "___syscall146": ___syscall146, + "___syscall54": ___syscall54, + "___syscall6": ___syscall6, + "___unlock": ___unlock, + "_emscripten_get_heap_size": _emscripten_get_heap_size, + "_emscripten_memcpy_big": _emscripten_memcpy_big, + "_emscripten_resize_heap": _emscripten_resize_heap, + "abortOnCannotGrowMemory": abortOnCannotGrowMemory, + "flush_NO_FILESYSTEM": flush_NO_FILESYSTEM, + "tempDoublePtr": tempDoublePtr, + "DYNAMICTOP_PTR": DYNAMICTOP_PTR +} +// EMSCRIPTEN_START_ASM +var asm =Module["asm"]// EMSCRIPTEN_END_ASM +(asmGlobalArg, asmLibraryArg, buffer); + +var real____errno_location = asm["___errno_location"]; asm["___errno_location"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return real____errno_location.apply(null, arguments); +}; + +var real__fflush = asm["_fflush"]; asm["_fflush"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return real__fflush.apply(null, arguments); +}; + +var real__free = asm["_free"]; asm["_free"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return real__free.apply(null, arguments); +}; + +var real__malloc = asm["_malloc"]; asm["_malloc"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return real__malloc.apply(null, arguments); +}; + +var real__mbe_eccAmbe3600x2400C0 = asm["_mbe_eccAmbe3600x2400C0"]; asm["_mbe_eccAmbe3600x2400C0"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return real__mbe_eccAmbe3600x2400C0.apply(null, arguments); +}; + +var real__sbrk = asm["_sbrk"]; asm["_sbrk"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return real__sbrk.apply(null, arguments); +}; + +var real_establishStackSpace = asm["establishStackSpace"]; asm["establishStackSpace"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return real_establishStackSpace.apply(null, arguments); +}; + +var real_stackAlloc = asm["stackAlloc"]; asm["stackAlloc"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return real_stackAlloc.apply(null, arguments); +}; + +var real_stackRestore = asm["stackRestore"]; asm["stackRestore"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return real_stackRestore.apply(null, arguments); +}; + +var real_stackSave = asm["stackSave"]; asm["stackSave"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return real_stackSave.apply(null, arguments); +}; +Module["asm"] = asm; +var ___errno_location = Module["___errno_location"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["___errno_location"].apply(null, arguments) }; +var _fflush = Module["_fflush"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["_fflush"].apply(null, arguments) }; +var _free = Module["_free"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["_free"].apply(null, arguments) }; +var _malloc = Module["_malloc"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["_malloc"].apply(null, arguments) }; +var _mbe_eccAmbe3600x2400C0 = Module["_mbe_eccAmbe3600x2400C0"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["_mbe_eccAmbe3600x2400C0"].apply(null, arguments) }; +var _memcpy = Module["_memcpy"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["_memcpy"].apply(null, arguments) }; +var _memset = Module["_memset"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["_memset"].apply(null, arguments) }; +var _sbrk = Module["_sbrk"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["_sbrk"].apply(null, arguments) }; +var establishStackSpace = Module["establishStackSpace"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["establishStackSpace"].apply(null, arguments) }; +var stackAlloc = Module["stackAlloc"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["stackAlloc"].apply(null, arguments) }; +var stackRestore = Module["stackRestore"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["stackRestore"].apply(null, arguments) }; +var stackSave = Module["stackSave"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["stackSave"].apply(null, arguments) }; +var dynCall_ii = Module["dynCall_ii"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["dynCall_ii"].apply(null, arguments) }; +var dynCall_iiii = Module["dynCall_iiii"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["dynCall_iiii"].apply(null, arguments) }; +var dynCall_jiji = Module["dynCall_jiji"] = function() { + assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); + assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); + return Module["asm"]["dynCall_jiji"].apply(null, arguments) }; +; + + + +// === Auto-generated postamble setup entry stuff === + +Module['asm'] = asm; + +if (!Module["intArrayFromString"]) Module["intArrayFromString"] = function() { abort("'intArrayFromString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["intArrayToString"]) Module["intArrayToString"] = function() { abort("'intArrayToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["ccall"]) Module["ccall"] = function() { abort("'ccall' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["cwrap"]) Module["cwrap"] = function() { abort("'cwrap' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["setValue"]) Module["setValue"] = function() { abort("'setValue' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["getValue"]) Module["getValue"] = function() { abort("'getValue' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["allocate"]) Module["allocate"] = function() { abort("'allocate' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["getMemory"]) Module["getMemory"] = function() { abort("'getMemory' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; +if (!Module["AsciiToString"]) Module["AsciiToString"] = function() { abort("'AsciiToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["stringToAscii"]) Module["stringToAscii"] = function() { abort("'stringToAscii' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["UTF8ArrayToString"]) Module["UTF8ArrayToString"] = function() { abort("'UTF8ArrayToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["UTF8ToString"]) Module["UTF8ToString"] = function() { abort("'UTF8ToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["stringToUTF8Array"]) Module["stringToUTF8Array"] = function() { abort("'stringToUTF8Array' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["stringToUTF8"]) Module["stringToUTF8"] = function() { abort("'stringToUTF8' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["lengthBytesUTF8"]) Module["lengthBytesUTF8"] = function() { abort("'lengthBytesUTF8' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["UTF16ToString"]) Module["UTF16ToString"] = function() { abort("'UTF16ToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["stringToUTF16"]) Module["stringToUTF16"] = function() { abort("'stringToUTF16' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["lengthBytesUTF16"]) Module["lengthBytesUTF16"] = function() { abort("'lengthBytesUTF16' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["UTF32ToString"]) Module["UTF32ToString"] = function() { abort("'UTF32ToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["stringToUTF32"]) Module["stringToUTF32"] = function() { abort("'stringToUTF32' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["lengthBytesUTF32"]) Module["lengthBytesUTF32"] = function() { abort("'lengthBytesUTF32' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["allocateUTF8"]) Module["allocateUTF8"] = function() { abort("'allocateUTF8' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["stackTrace"]) Module["stackTrace"] = function() { abort("'stackTrace' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["addOnPreRun"]) Module["addOnPreRun"] = function() { abort("'addOnPreRun' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["addOnInit"]) Module["addOnInit"] = function() { abort("'addOnInit' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["addOnPreMain"]) Module["addOnPreMain"] = function() { abort("'addOnPreMain' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["addOnExit"]) Module["addOnExit"] = function() { abort("'addOnExit' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["addOnPostRun"]) Module["addOnPostRun"] = function() { abort("'addOnPostRun' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["writeStringToMemory"]) Module["writeStringToMemory"] = function() { abort("'writeStringToMemory' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["writeArrayToMemory"]) Module["writeArrayToMemory"] = function() { abort("'writeArrayToMemory' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["writeAsciiToMemory"]) Module["writeAsciiToMemory"] = function() { abort("'writeAsciiToMemory' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["addRunDependency"]) Module["addRunDependency"] = function() { abort("'addRunDependency' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; +if (!Module["removeRunDependency"]) Module["removeRunDependency"] = function() { abort("'removeRunDependency' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; +if (!Module["ENV"]) Module["ENV"] = function() { abort("'ENV' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["FS"]) Module["FS"] = function() { abort("'FS' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["FS_createFolder"]) Module["FS_createFolder"] = function() { abort("'FS_createFolder' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; +if (!Module["FS_createPath"]) Module["FS_createPath"] = function() { abort("'FS_createPath' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; +if (!Module["FS_createDataFile"]) Module["FS_createDataFile"] = function() { abort("'FS_createDataFile' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; +if (!Module["FS_createPreloadedFile"]) Module["FS_createPreloadedFile"] = function() { abort("'FS_createPreloadedFile' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; +if (!Module["FS_createLazyFile"]) Module["FS_createLazyFile"] = function() { abort("'FS_createLazyFile' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; +if (!Module["FS_createLink"]) Module["FS_createLink"] = function() { abort("'FS_createLink' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; +if (!Module["FS_createDevice"]) Module["FS_createDevice"] = function() { abort("'FS_createDevice' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; +if (!Module["FS_unlink"]) Module["FS_unlink"] = function() { abort("'FS_unlink' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; +if (!Module["GL"]) Module["GL"] = function() { abort("'GL' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["dynamicAlloc"]) Module["dynamicAlloc"] = function() { abort("'dynamicAlloc' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["warnOnce"]) Module["warnOnce"] = function() { abort("'warnOnce' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["loadDynamicLibrary"]) Module["loadDynamicLibrary"] = function() { abort("'loadDynamicLibrary' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["loadWebAssemblyModule"]) Module["loadWebAssemblyModule"] = function() { abort("'loadWebAssemblyModule' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["getLEB"]) Module["getLEB"] = function() { abort("'getLEB' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["getFunctionTables"]) Module["getFunctionTables"] = function() { abort("'getFunctionTables' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["alignFunctionTables"]) Module["alignFunctionTables"] = function() { abort("'alignFunctionTables' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["registerFunctions"]) Module["registerFunctions"] = function() { abort("'registerFunctions' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["addFunction"]) Module["addFunction"] = function() { abort("'addFunction' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["removeFunction"]) Module["removeFunction"] = function() { abort("'removeFunction' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["getFuncWrapper"]) Module["getFuncWrapper"] = function() { abort("'getFuncWrapper' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["prettyPrint"]) Module["prettyPrint"] = function() { abort("'prettyPrint' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["makeBigInt"]) Module["makeBigInt"] = function() { abort("'makeBigInt' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["dynCall"]) Module["dynCall"] = function() { abort("'dynCall' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["getCompilerSetting"]) Module["getCompilerSetting"] = function() { abort("'getCompilerSetting' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["stackSave"]) Module["stackSave"] = function() { abort("'stackSave' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["stackRestore"]) Module["stackRestore"] = function() { abort("'stackRestore' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["stackAlloc"]) Module["stackAlloc"] = function() { abort("'stackAlloc' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["establishStackSpace"]) Module["establishStackSpace"] = function() { abort("'establishStackSpace' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["print"]) Module["print"] = function() { abort("'print' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["printErr"]) Module["printErr"] = function() { abort("'printErr' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["getTempRet0"]) Module["getTempRet0"] = function() { abort("'getTempRet0' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["setTempRet0"]) Module["setTempRet0"] = function() { abort("'setTempRet0' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; +if (!Module["Pointer_stringify"]) Module["Pointer_stringify"] = function() { abort("'Pointer_stringify' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") };if (!Module["ALLOC_NORMAL"]) Object.defineProperty(Module, "ALLOC_NORMAL", { get: function() { abort("'ALLOC_NORMAL' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") } }); +if (!Module["ALLOC_STACK"]) Object.defineProperty(Module, "ALLOC_STACK", { get: function() { abort("'ALLOC_STACK' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") } }); +if (!Module["ALLOC_DYNAMIC"]) Object.defineProperty(Module, "ALLOC_DYNAMIC", { get: function() { abort("'ALLOC_DYNAMIC' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") } }); +if (!Module["ALLOC_NONE"]) Object.defineProperty(Module, "ALLOC_NONE", { get: function() { abort("'ALLOC_NONE' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") } }); + + + + +/** + * @constructor + * @extends {Error} + * @this {ExitStatus} + */ +function ExitStatus(status) { + this.name = "ExitStatus"; + this.message = "Program terminated with exit(" + status + ")"; + this.status = status; +}; +ExitStatus.prototype = new Error(); +ExitStatus.prototype.constructor = ExitStatus; + +var calledMain = false; + +dependenciesFulfilled = function runCaller() { + // If run has never been called, and we should call run (INVOKE_RUN is true, and Module.noInitialRun is not false) + if (!Module['calledRun']) run(); + if (!Module['calledRun']) dependenciesFulfilled = runCaller; // try this again later, after new deps are fulfilled +} + + + + + +/** @type {function(Array=)} */ +function run(args) { + args = args || Module['arguments']; + + if (runDependencies > 0) { + return; + } + + writeStackCookie(); + + preRun(); + + if (runDependencies > 0) return; // a preRun added a dependency, run will be called later + if (Module['calledRun']) return; // run may have just been called through dependencies being fulfilled just in this very frame + + function doRun() { + if (Module['calledRun']) return; // run may have just been called while the async setStatus time below was happening + Module['calledRun'] = true; + + if (ABORT) return; + + ensureInitRuntime(); + + preMain(); + + if (Module['onRuntimeInitialized']) Module['onRuntimeInitialized'](); + + assert(!Module['_main'], 'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]'); + + postRun(); + } + + if (Module['setStatus']) { + Module['setStatus']('Running...'); + setTimeout(function() { + setTimeout(function() { + Module['setStatus'](''); + }, 1); + doRun(); + }, 1); + } else { + doRun(); + } + checkStackCookie(); +} +Module['run'] = run; + +function checkUnflushedContent() { + // Compiler settings do not allow exiting the runtime, so flushing + // the streams is not possible. but in ASSERTIONS mode we check + // if there was something to flush, and if so tell the user they + // should request that the runtime be exitable. + // Normally we would not even include flush() at all, but in ASSERTIONS + // builds we do so just for this check, and here we see if there is any + // content to flush, that is, we check if there would have been + // something a non-ASSERTIONS build would have not seen. + // How we flush the streams depends on whether we are in SYSCALLS_REQUIRE_FILESYSTEM=0 + // mode (which has its own special function for this; otherwise, all + // the code is inside libc) + var print = out; + var printErr = err; + var has = false; + out = err = function(x) { + has = true; + } + try { // it doesn't matter if it fails + var flush = flush_NO_FILESYSTEM; + if (flush) flush(0); + } catch(e) {} + out = print; + err = printErr; + if (has) { + warnOnce('stdio streams had content in them that was not flushed. you should set EXIT_RUNTIME to 1 (see the FAQ), or make sure to emit a newline when you printf etc.'); + warnOnce('(this may also be due to not including full filesystem support - try building with -s FORCE_FILESYSTEM=1)'); + } +} + +function exit(status, implicit) { + checkUnflushedContent(); + + // if this is just main exit-ing implicitly, and the status is 0, then we + // don't need to do anything here and can just leave. if the status is + // non-zero, though, then we need to report it. + // (we may have warned about this earlier, if a situation justifies doing so) + if (implicit && Module['noExitRuntime'] && status === 0) { + return; + } + + if (Module['noExitRuntime']) { + // if exit() was called, we may warn the user if the runtime isn't actually being shut down + if (!implicit) { + err('exit(' + status + ') called, but EXIT_RUNTIME is not set, so halting execution but not exiting the runtime or preventing further async execution (build with EXIT_RUNTIME=1, if you want a true shutdown)'); + } + } else { + + ABORT = true; + EXITSTATUS = status; + + exitRuntime(); + + if (Module['onExit']) Module['onExit'](status); + } + + Module['quit'](status, new ExitStatus(status)); +} + +var abortDecorators = []; + +function abort(what) { + if (Module['onAbort']) { + Module['onAbort'](what); + } + + if (what !== undefined) { + out(what); + err(what); + what = JSON.stringify(what) + } else { + what = ''; + } + + ABORT = true; + EXITSTATUS = 1; + + var extra = ''; + var output = 'abort(' + what + ') at ' + stackTrace() + extra; + if (abortDecorators) { + abortDecorators.forEach(function(decorator) { + output = decorator(output, what); + }); + } + throw output; +} +Module['abort'] = abort; + +if (Module['preInit']) { + if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; + while (Module['preInit'].length > 0) { + Module['preInit'].pop()(); + } +} + + + Module["noExitRuntime"] = true; + +run(); + + + + + +// {{MODULE_ADDITIONS}} + + + diff --git a/htdocs/libmbe.wasm b/htdocs/libmbe.wasm new file mode 100644 index 0000000000000000000000000000000000000000..5903c97845d357f2b62280fcff4f5865935df742 GIT binary patch literal 31678 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 literal 0 HcmV?d00001 diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 3808f9215..08bc951a6 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -443,6 +443,11 @@ function demodulator_default_analog(offset_frequency,subtype) { this.low_cut=-3250; this.high_cut=3250; + } + else if(subtype=="packet") + { + this.low_cut=-4000; + this.high_cut=4000; } else if(subtype=="am") { diff --git a/owrx/feature.py b/owrx/feature.py index 75ef97d9f..8233cfbf8 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -17,7 +17,8 @@ class FeatureDetector(object): "sdrplay": [ "rx_tools" ], "hackrf": [ "hackrf_transfer" ], "digital_voice_digiham": [ "digiham", "sox" ], - "digital_voice_dsd": [ "dsd", "sox" ] + "digital_voice_dsd": [ "dsd", "sox" ], + "packet": [ "direwolf" ] } def feature_availability(self): @@ -101,4 +102,7 @@ def has_dsd(self): return self.command_is_runnable("dsd") def has_sox(self): - return self.command_is_runnable("sox") \ No newline at end of file + return self.command_is_runnable("sox") + + def has_direwolf(self): + return self.command_is_runnable("direwolf --help") \ No newline at end of file From e422ca4d9b09d5300cc1c9a03131b8bdd6c4c287 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 7 Jun 2019 15:44:11 +0200 Subject: [PATCH 0152/2616] add airspy support (untested for now) --- config_webrx.py | 2 +- owrx/feature.py | 6 +++++- owrx/source.py | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index 3bd92c60d..a3ea8c0a7 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -93,7 +93,7 @@ # Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support # ################################################################################################# -# Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf" +# Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf", "airspy" sdrs = { "rtlsdr": { diff --git a/owrx/feature.py b/owrx/feature.py index 83f9232f2..bdfcee2ab 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -12,7 +12,8 @@ class FeatureDetector(object): "core": [ "csdr", "nmux" ], "rtl_sdr": [ "rtl_sdr" ], "sdrplay": [ "rx_tools" ], - "hackrf": [ "hackrf_transfer" ] + "hackrf": [ "hackrf_transfer" ], + "airspy": [ "airspy_rx" ] } def is_available(self, feature): @@ -63,3 +64,6 @@ def has_hackrf_transfer(self): # TODO i don't have a hackrf, so somebody doublecheck this. # TODO also check if it has the stdout feature return os.system("hackrf_transfer --help 2> /dev/null") != 32512 + + def has_airspy_rx(self): + return os.system("airspy_rx --help 2> /dev/null") != 32512 diff --git a/owrx/source.py b/owrx/source.py index 3efc7d4a4..d331c71a1 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -130,6 +130,7 @@ def start(self): start_sdr_command = self.command.format( samp_rate = props["samp_rate"], center_freq = props["center_freq"], + center_freq_mhz = props["center_freq"]/1e6, ppm = props["ppm"], rf_gain = props["rf_gain"], lna_gain = props["lna_gain"], @@ -249,6 +250,12 @@ def __init__(self, props, port): def sleepOnRestart(self): time.sleep(1) +class AirspySource(SdrSource): + def __init__(self, props, port): + super().__init__(props, port) + self.command = "airspy_rx -f{center_freq_mhz} -r /dev/stdout -a{samp_rate} -g {rf_gain}" + self.format_conversion = "csdr convert_s16_f" + class SpectrumThread(threading.Thread): def __init__(self, sdrSource): self.doRun = True From e8a1a40dc0e191d648193b1547151ac720a4d2ce Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 7 Jun 2019 20:10:03 +0200 Subject: [PATCH 0153/2616] try to handle overflowing connections --- owrx/websocket.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/owrx/websocket.py b/owrx/websocket.py index b8ea3a72b..a247b2a1f 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -38,12 +38,16 @@ def send(self, data): # string-type messages are sent as text frames if (type(data) == str): header = self.get_header(len(data), 1) - self.handler.wfile.write(header + data.encode('utf-8')) - self.handler.wfile.flush() + data_to_send = header + data.encode('utf-8') # anything else as binary else: header = self.get_header(len(data), 2) - self.handler.wfile.write(header + data) + data_to_send = header + data + written = self.handler.wfile.write(data_to_send) + if (written != len(data_to_send)): + logger.error("incomplete write! closing socket!") + self.close() + else: self.handler.wfile.flush() def read_loop(self): From b6e59e9b11ea2eb4c56b6bd824a449b7da5d8162 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 7 Jun 2019 20:23:31 +0200 Subject: [PATCH 0154/2616] allow avatar to be downloaded on its old url --- owrx/http.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/owrx/http.py b/owrx/http.py index 7012f0e9e..ca7d357f3 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -18,7 +18,9 @@ class Router(object): {"route": "/status", "controller": StatusController}, {"regex": "/static/(.+)", "controller": AssetsController}, {"route": "/ws/", "controller": WebSocketController}, - {"regex": "(/favicon.ico)", "controller": AssetsController} + {"regex": "(/favicon.ico)", "controller": AssetsController}, + # backwards compatibility for the sdr.hu portal + {"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController} ] def find_controller(self, path): for m in Router.mappings: From a9d5fcf82a9827830608f97d3d5d5454569d8c1d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 7 Jun 2019 20:23:58 +0200 Subject: [PATCH 0155/2616] use fixed buf sizes to avoid cut-off audio --- csdr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csdr.py b/csdr.py index 28443f32d..f04bc6ea2 100755 --- a/csdr.py +++ b/csdr.py @@ -116,7 +116,7 @@ def chain(self,which): chain += "dmr_decoder --fifo {meta_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | " elif which == "ysf": chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | " - chain += "digitalvoice_filter -f | csdr agc_ff 160000 0.8 1 0.0000001 0.0005 | csdr convert_f_s16 | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + chain += "digitalvoice_filter -f | CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 0.0005 | CSDR_FIXED_BUFSIZE=32 csdr convert_f_s16 | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " elif which == "am": chain += "csdr amdemod_cf | csdr fastdcblock_ff | " chain += last_decimation_block From f9c14addcccf6af3f54c68a6a1b0422b5e78f9ee Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 8 Jun 2019 09:23:39 +0200 Subject: [PATCH 0156/2616] apply audio filtering and agc to dsd too --- csdr.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/csdr.py b/csdr.py index f04bc6ea2..01c43ecaa 100755 --- a/csdr.py +++ b/csdr.py @@ -107,8 +107,8 @@ def chain(self,which): chain += "dsd -fd" elif which == "nxdn": chain += "dsd -fi" - chain += " -i - -o - -u {unvoiced_quality} -g 10 | " - chain += "digitalvoice_filter | sox -V -v 0.95 -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + chain += " -i - -o - -u {unvoiced_quality} -g -1 | CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f | " + max_gain = 5 # digiham modes else: chain += "rrc_filter | csdr convert_f_s16 | gfsk_demodulator | " @@ -116,7 +116,11 @@ def chain(self,which): chain += "dmr_decoder --fifo {meta_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | " elif which == "ysf": chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | " - chain += "digitalvoice_filter -f | CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 0.0005 | CSDR_FIXED_BUFSIZE=32 csdr convert_f_s16 | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + max_gain = 0.0005 + chain += "digitalvoice_filter -f | " + chain += "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain} | ".format(max_gain=max_gain) + chain += "CSDR_FIXED_BUFSIZE=32 csdr convert_f_s16 | " + chain += "sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " elif which == "am": chain += "csdr amdemod_cf | csdr fastdcblock_ff | " chain += last_decimation_block From b852fcc167c166f959fa6a2fb2de7178722a5147 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 8 Jun 2019 18:17:04 +0200 Subject: [PATCH 0157/2616] sox can accept float input, no need to convert --- csdr.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/csdr.py b/csdr.py index 01c43ecaa..a4c932a1e 100755 --- a/csdr.py +++ b/csdr.py @@ -119,8 +119,7 @@ def chain(self,which): max_gain = 0.0005 chain += "digitalvoice_filter -f | " chain += "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain} | ".format(max_gain=max_gain) - chain += "CSDR_FIXED_BUFSIZE=32 csdr convert_f_s16 | " - chain += "sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + chain += "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " elif which == "am": chain += "csdr amdemod_cf | csdr fastdcblock_ff | " chain += last_decimation_block From cde3ff703a0ece27ddcbf90f2ee5c84f6363539f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 8 Jun 2019 18:47:17 +0200 Subject: [PATCH 0158/2616] gfsk decoder now supports floating point input, so we can stop converting --- csdr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csdr.py b/csdr.py index a4c932a1e..b7d1ebd86 100755 --- a/csdr.py +++ b/csdr.py @@ -111,7 +111,7 @@ def chain(self,which): max_gain = 5 # digiham modes else: - chain += "rrc_filter | csdr convert_f_s16 | gfsk_demodulator | " + chain += "rrc_filter | gfsk_demodulator | " if which == "dmr": chain += "dmr_decoder --fifo {meta_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | " elif which == "ysf": From 94516ef341807fccb4c999aef746b8968928ea98 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 8 Jun 2019 23:36:16 +0200 Subject: [PATCH 0159/2616] implement https detection (thanks Denys Vitali) --- htdocs/openwebrx.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 3808f9215..40f95c2ce 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1812,7 +1812,12 @@ String.prototype.startswith=function(str){ return this.indexOf(str) == 0; }; //h function open_websocket() { - ws_url="ws://"+(window.location.origin.split("://")[1])+"/ws/"; //guess automatically -> now default behaviour + var protocol = 'ws'; + if (window.location.toString().startsWith('https://')) { + protocol = 'wss'; + } + + ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; //guess automatically -> now default behaviour if (!("WebSocket" in window)) divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser."); ws = new WebSocket(ws_url); From 2010a384110e7a14dfe75230e91dd662856031c2 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 9 Jun 2019 15:15:27 +0200 Subject: [PATCH 0160/2616] add new nicer dmr status display --- htdocs/gfx/openwebrx-user.png | Bin 0 -> 2466 bytes htdocs/index.html | 18 +++++++++++++ htdocs/openwebrx.css | 48 ++++++++++++++++++++++++++++++++++ htdocs/openwebrx.js | 44 +++++++++++++++---------------- 4 files changed, 87 insertions(+), 23 deletions(-) create mode 100644 htdocs/gfx/openwebrx-user.png diff --git a/htdocs/gfx/openwebrx-user.png b/htdocs/gfx/openwebrx-user.png new file mode 100644 index 0000000000000000000000000000000000000000..4c2969742d49eea92b5a829900f596fdf1c82c5c GIT binary patch literal 2466 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 literal 0 HcmV?d00001 diff --git a/htdocs/index.html b/htdocs/index.html index da3c800e5..255d32599 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -170,6 +170,24 @@
+
+
+
+
Timeslot 1
+
+
+
+
+
+
+
Timeslot 2
+
+
+
+
+
+
+
diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 4736bf303..9c4e0d86f 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -928,3 +928,51 @@ img.openwebrx-mirror-img border-color: Red; } +#openwebrx-panel-metadata-dmr { + background: none; + padding: 0; +} + +.openwebrx-dmr-panel { + height: 200px; + + outline: 1px solid #111; + border-top: 1px solid #555; + padding: 10px; + background: #333; +} + +.openwebrx-dmr-timeslot-panel { + width: 133px; + height: 194px; + float: left; + margin-right: 10px; + + padding:2px 0; + color: #333; + background: #575757; + border: 1px solid #000; + border-right: 1px solid #353535; + border-bottom: 1px solid #353535; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 2px; +} + +.openwebrx-dmr-timeslot-panel.active { + background-color: #95bbdf; +} + +.openwebrx-dmr-timeslot-panel:last-child { + margin-right: 0; +} + +.openwebrx-dmr-timeslot-panel.active .openwebrx-dmr-user-image { + background-image: url("gfx/openwebrx-user.png"); + width:133px; + height:133px; +} + +.openwebrx-dmr-timeslot-panel { + text-align: center; +} diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 40f95c2ce..b1e47be27 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -620,6 +620,9 @@ function demodulator_analog_replace(subtype, for_digital) demodulator_add(new demodulator_default_analog(temp_offset,subtype)); demodulator_buttons_update(); clear_metadata(); + if (subtype == "dmr") { + toggle_panel("openwebrx-panel-metadata-dmr", true); + } } function demodulator_set_offset_frequency(which,to_what) @@ -1316,28 +1319,22 @@ function update_metadata(meta) { if (meta.protocol) switch (meta.protocol) { case 'DMR': if (meta.slot) { - var html = 'Timeslot: ' + meta.slot; - if (meta.type) html += ' Typ: ' + meta.type; - if (meta.additional && meta.additional.callsign) { - html += ' Source: ' + meta.additional.callsign; - if (meta.additional.fname) { - html += ' (' + meta.additional.fname + ')'; - } - } else if (meta.source) { - html += ' Source: ' + meta.source; + el = $(".openwebrx-dmr-panel .openwebrx-dmr-timeslot-panel").get(meta.slot); + var id = ""; + var name = ""; + var talkgroup = ""; + if (meta.type && meta.type != "data") { + id = (meta.additional && meta.additional.callsign) || meta.source || ""; + name = (meta.additional && meta.additional.fname) || ""; + talkgroup = meta.target || ""; + $(el).addClass("active"); + } else { + $(el).removeClass("active"); } - if (meta.target) html += ' Target: ' + meta.target; - update = function(_, el) { - var slotEl = el.getElementsByClassName('slot-' + meta.slot); - if (!slotEl.length) { - slotEl = document.createElement('div'); - slotEl.className = 'slot-' + meta.slot; - el.appendChild(slotEl); - } else { - slotEl = slotEl[0]; - } - slotEl.innerHTML = html; - }; + $(el).find(".openwebrx-dmr-id").text(id); + $(el).find(".openwebrx-dmr-name").text(name); + $(el).find(".openwebrx-dmr-talkgroup").text(talkgroup); + } break; case 'YSF': @@ -1351,15 +1348,16 @@ function update_metadata(meta) { update = function(_, el) { el.innerHTML = html; } + $('.openwebrx-panel[data-panel-name="metadata"]').each(update); + toggle_panel("openwebrx-panel-metadata", true); break; } - $('.openwebrx-panel[data-panel-name="metadata"]').each(update); - toggle_panel("openwebrx-panel-metadata", true); } function clear_metadata() { toggle_panel("openwebrx-panel-metadata", false); + toggle_panel("openwebrx-panel-metadata-dmr", false); } function add_problem(what) From 761ca1132df0ecf66fd7c53acb8b5bd577ceead4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 9 Jun 2019 17:39:15 +0200 Subject: [PATCH 0161/2616] nicer user display panel for YSF, too --- htdocs/index.html | 35 ++++++++++++++++---------- htdocs/openwebrx.css | 14 +++++------ htdocs/openwebrx.js | 58 ++++++++++++++++++++++++++------------------ 3 files changed, 63 insertions(+), 44 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index 255d32599..44b414d3b 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -168,23 +168,32 @@ -
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
Timeslot 1
-
-
-
-
+
+
+
+
-
+
Timeslot 2
-
-
-
-
+
+
+
+
diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 9c4e0d86f..0654e5235 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -928,12 +928,12 @@ img.openwebrx-mirror-img border-color: Red; } -#openwebrx-panel-metadata-dmr { +.openwebrx-meta-panel { background: none; padding: 0; } -.openwebrx-dmr-panel { +.openwebrx-meta-frame { height: 200px; outline: 1px solid #111; @@ -942,7 +942,7 @@ img.openwebrx-mirror-img background: #333; } -.openwebrx-dmr-timeslot-panel { +.openwebrx-meta-slot { width: 133px; height: 194px; float: left; @@ -959,20 +959,20 @@ img.openwebrx-mirror-img border-radius: 2px; } -.openwebrx-dmr-timeslot-panel.active { +.openwebrx-meta-slot.active { background-color: #95bbdf; } -.openwebrx-dmr-timeslot-panel:last-child { +.openwebrx-meta-slot:last-child { margin-right: 0; } -.openwebrx-dmr-timeslot-panel.active .openwebrx-dmr-user-image { +.openwebrx-meta-slot.active .openwebrx-meta-user-image { background-image: url("gfx/openwebrx-user.png"); width:133px; height:133px; } -.openwebrx-dmr-timeslot-panel { +.openwebrx-meta-slot { text-align: center; } diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index b1e47be27..83b2bc136 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -620,9 +620,7 @@ function demodulator_analog_replace(subtype, for_digital) demodulator_add(new demodulator_default_analog(temp_offset,subtype)); demodulator_buttons_update(); clear_metadata(); - if (subtype == "dmr") { - toggle_panel("openwebrx-panel-metadata-dmr", true); - } + toggle_panel("openwebrx-panel-metadata-" + subtype, true); } function demodulator_set_offset_frequency(which,to_what) @@ -1313,51 +1311,62 @@ function on_ws_recv(evt) } function update_metadata(meta) { - var update = function(_, el) { - el.innerHTML = ""; - }; if (meta.protocol) switch (meta.protocol) { case 'DMR': if (meta.slot) { - el = $(".openwebrx-dmr-panel .openwebrx-dmr-timeslot-panel").get(meta.slot); + var el = $("#openwebrx-panel-metadata-dmr .openwebrx-dmr-timeslot-panel").get(meta.slot); var id = ""; var name = ""; - var talkgroup = ""; + var target = ""; if (meta.type && meta.type != "data") { id = (meta.additional && meta.additional.callsign) || meta.source || ""; name = (meta.additional && meta.additional.fname) || ""; - talkgroup = meta.target || ""; + if (meta.type == "group") target = "Talkgroup: "; + if (meta.type == "direct") tareget = "Direct: "; + target += meta.target || ""; $(el).addClass("active"); } else { $(el).removeClass("active"); } $(el).find(".openwebrx-dmr-id").text(id); $(el).find(".openwebrx-dmr-name").text(name); - $(el).find(".openwebrx-dmr-talkgroup").text(talkgroup); + $(el).find(".openwebrx-dmr-target").text(target); } break; case 'YSF': - var strings = []; - if (meta.mode) strings.push("Mode: " + meta.mode); - if (meta.source) strings.push("Source: " + meta.source); - if (meta.target) strings.push("Destination: " + meta.target); - if (meta.up) strings.push("Up: " + meta.up); - if (meta.down) strings.push("Down: " + meta.down); - var html = strings.join(' '); - update = function(_, el) { - el.innerHTML = html; + var el = $("#openwebrx-panel-metadata-ysf"); + + var mode = " " + var source = ""; + var up = ""; + var down = ""; + if (meta.mode && meta.mode != "") { + mode = "Mode: " + meta.mode; + source = meta.source || ""; + up = meta.up ? "Up: " + meta.up : ""; + down = meta.down ? "Down: " + meta.down : ""; + $(el).find(".openwebrx-meta-slot").addClass("active"); + } else { + $(el).find(".openwebrx-meta-slot").removeClass("active"); } - $('.openwebrx-panel[data-panel-name="metadata"]').each(update); - toggle_panel("openwebrx-panel-metadata", true); + $(el).find(".openwebrx-ysf-mode").text(mode); + $(el).find(".openwebrx-ysf-source").text(source); + $(el).find(".openwebrx-ysf-up").text(up); + $(el).find(".openwebrx-ysf-down").text(down); + break; + } } function clear_metadata() { - toggle_panel("openwebrx-panel-metadata", false); - toggle_panel("openwebrx-panel-metadata-dmr", false); + $(".openwebrx-meta-panel").each(function(_, p){ + toggle_panel(p.id, false); + }); + $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text(""); + $(".openwebrx-meta-panel .active").removeClass("active"); } function add_problem(what) @@ -2407,6 +2416,7 @@ function pop_bottommost_panel(from) function toggle_panel(what, on) { var item=e(what); + if (!item) return; if(typeof on !== "undefined") { if(item.openwebrxHidden && !on) return; @@ -2470,7 +2480,7 @@ function place_panels(function_apply) for(i=0;i= 0) { if(c.openwebrxHidden) { From e1d54bdf1df7c9f1ee1b90cfd7c918d41a4d19bf Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 9 Jun 2019 17:49:14 +0200 Subject: [PATCH 0162/2616] fix typo --- htdocs/openwebrx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 83b2bc136..67893a2f5 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1322,7 +1322,7 @@ function update_metadata(meta) { id = (meta.additional && meta.additional.callsign) || meta.source || ""; name = (meta.additional && meta.additional.fname) || ""; if (meta.type == "group") target = "Talkgroup: "; - if (meta.type == "direct") tareget = "Direct: "; + if (meta.type == "direct") target = "Direct: "; target += meta.target || ""; $(el).addClass("active"); } else { From 2053a6b16b37358ee99bb980859a3215245a0680 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 9 Jun 2019 19:12:37 +0200 Subject: [PATCH 0163/2616] more clean-up stuff --- htdocs/openwebrx.js | 3 ++- owrx/meta.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 67893a2f5..e304950a6 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1356,7 +1356,8 @@ function update_metadata(meta) { $(el).find(".openwebrx-ysf-down").text(down); break; - + } else { + $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text(""); } } diff --git a/owrx/meta.py b/owrx/meta.py index e215d89c8..ec4966ae9 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -64,11 +64,13 @@ class MetaParser(object): enrichers = { "DMR": DmrMetaEnricher() } + def __init__(self, handler): self.handler = handler + def parse(self, meta): fields = meta.split(";") - meta = {v[0] : "".join(v[1:]) for v in map(lambda x: x.split(":"), fields)} + meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""} if "protocol" in meta: protocol = meta["protocol"] From c7d969c96e30fe51390c03cbd3d17a57c35ededd Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 9 Jun 2019 22:27:35 +0200 Subject: [PATCH 0164/2616] polishing up the imaging --- htdocs/gfx/openwebrx-directcall.png | Bin 0 -> 5445 bytes htdocs/gfx/openwebrx-groupcall.png | Bin 0 -> 8055 bytes htdocs/gfx/openwebrx-user.png | Bin 2466 -> 0 bytes htdocs/openwebrx.css | 6 +++++- htdocs/openwebrx.js | 8 ++++++-- 5 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 htdocs/gfx/openwebrx-directcall.png create mode 100644 htdocs/gfx/openwebrx-groupcall.png delete mode 100644 htdocs/gfx/openwebrx-user.png diff --git a/htdocs/gfx/openwebrx-directcall.png b/htdocs/gfx/openwebrx-directcall.png new file mode 100644 index 0000000000000000000000000000000000000000..2d74713b60003cb919a8253fd04994f2d1d446be GIT binary patch literal 5445 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
  • hz?&?Z2Wo-mLWr4J%MzcF^TvriPu9 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 V)UUa{ZiDBqfF@c`wO-je>VKC$;k5t& literal 0 HcmV?d00001 diff --git a/htdocs/gfx/openwebrx-user.png b/htdocs/gfx/openwebrx-user.png deleted file mode 100644 index 4c2969742d49eea92b5a829900f596fdf1c82c5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2466 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 diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 0654e5235..38ecf2b12 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -968,11 +968,15 @@ img.openwebrx-mirror-img } .openwebrx-meta-slot.active .openwebrx-meta-user-image { - background-image: url("gfx/openwebrx-user.png"); + background-image: url("gfx/openwebrx-directcall.png"); width:133px; height:133px; } +.openwebrx-meta-slot.active .openwebrx-meta-user-image.group { + background-image: url("gfx/openwebrx-groupcall.png"); +} + .openwebrx-meta-slot { text-align: center; } diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index e304950a6..f61b84598 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1318,10 +1318,14 @@ function update_metadata(meta) { var id = ""; var name = ""; var target = ""; + var group = false; if (meta.type && meta.type != "data") { id = (meta.additional && meta.additional.callsign) || meta.source || ""; name = (meta.additional && meta.additional.fname) || ""; - if (meta.type == "group") target = "Talkgroup: "; + if (meta.type == "group") { + target = "Talkgroup: "; + group = true; + } if (meta.type == "direct") target = "Direct: "; target += meta.target || ""; $(el).addClass("active"); @@ -1331,7 +1335,7 @@ function update_metadata(meta) { $(el).find(".openwebrx-dmr-id").text(id); $(el).find(".openwebrx-dmr-name").text(name); $(el).find(".openwebrx-dmr-target").text(target); - + $(el).find(".openwebrx-meta-user-image")[group ? "addClass" : "removeClass"]("group"); } break; case 'YSF': From 3a89f520286c280292353e723996836b213c2886 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 10 Jun 2019 21:30:46 +0200 Subject: [PATCH 0165/2616] better sync on the client side --- htdocs/openwebrx.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index f61b84598..486e3cefb 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1336,6 +1336,9 @@ function update_metadata(meta) { $(el).find(".openwebrx-dmr-name").text(name); $(el).find(".openwebrx-dmr-target").text(target); $(el).find(".openwebrx-meta-user-image")[group ? "addClass" : "removeClass"]("group"); + } else { + $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text(""); + $(".openwebrx-meta-panel").removeClass("active"); } break; case 'YSF': @@ -1362,6 +1365,7 @@ function update_metadata(meta) { break; } else { $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text(""); + $(".openwebrx-meta-panel").removeClass("active"); } } @@ -1371,7 +1375,7 @@ function clear_metadata() { toggle_panel(p.id, false); }); $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text(""); - $(".openwebrx-meta-panel .active").removeClass("active"); + $(".openwebrx-meta-panel").removeClass("active"); } function add_problem(what) From adf62bc2ca9cc2425e79d76a99eb8c49e15a7f85 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 15 Jun 2019 12:30:04 +0200 Subject: [PATCH 0166/2616] sync indicator --- htdocs/gfx/openwebrx-groupcall.png | Bin 8055 -> 8286 bytes htdocs/index.html | 4 ++-- htdocs/openwebrx.css | 11 +++++++++++ htdocs/openwebrx.js | 10 +++++----- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/htdocs/gfx/openwebrx-groupcall.png b/htdocs/gfx/openwebrx-groupcall.png index 88afbe9d91a904c4acbdeccba737302e0ac5e6a0..5d61a4c770458e4b381d2cc62e0ca594300b5ead 100644 GIT binary patch delta 7838 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 delta 7605 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 diff --git a/htdocs/index.html b/htdocs/index.html index 44b414d3b..8bf100182 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -182,14 +182,14 @@
    -
    Timeslot 1
    +
    Timeslot 1
    -
    Timeslot 2
    +
    Timeslot 2
    diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 38ecf2b12..92b9f379c 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -963,6 +963,17 @@ img.openwebrx-mirror-img background-color: #95bbdf; } +.openwebrx-meta-slot.sync .openwebrx-dmr-slot:before { + content:""; + display: inline-block; + margin: 0 5px; + width: 12px; + height: 12px; + background-color: #ABFF00; + border-radius: 50%; + box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px; +} + .openwebrx-meta-slot:last-child { margin-right: 0; } diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 486e3cefb..8a8345b6e 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1319,7 +1319,8 @@ function update_metadata(meta) { var name = ""; var target = ""; var group = false; - if (meta.type && meta.type != "data") { + $(el)[meta.sync ? "addClass" : "removeClass"]("sync"); + if (meta.sync && meta.sync == "voice") { id = (meta.additional && meta.additional.callsign) || meta.source || ""; name = (meta.additional && meta.additional.fname) || ""; if (meta.type == "group") { @@ -1338,7 +1339,7 @@ function update_metadata(meta) { $(el).find(".openwebrx-meta-user-image")[group ? "addClass" : "removeClass"]("group"); } else { $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text(""); - $(".openwebrx-meta-panel").removeClass("active"); + $(".openwebrx-meta-panel").removeClass("active").removeClass("sync"); } break; case 'YSF': @@ -1365,7 +1366,7 @@ function update_metadata(meta) { break; } else { $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text(""); - $(".openwebrx-meta-panel").removeClass("active"); + $(".openwebrx-meta-panel").removeClass("active").removeClass("sync"); } } @@ -1374,8 +1375,7 @@ function clear_metadata() { $(".openwebrx-meta-panel").each(function(_, p){ toggle_panel(p.id, false); }); - $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text(""); - $(".openwebrx-meta-panel").removeClass("active"); + update_metadata({}); } function add_problem(what) From efa0c060fe15804cdab60eb32bea0cda3ebae905 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 15 Jun 2019 13:29:59 +0200 Subject: [PATCH 0167/2616] implement digiham version check --- owrx/feature.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/owrx/feature.py b/owrx/feature.py index a430c0d02..38d3fa75b 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -2,6 +2,8 @@ import subprocess from functools import reduce from operator import and_ +import re +from distutils.version import LooseVersion import logging logger = logging.getLogger(__name__) @@ -18,7 +20,7 @@ class FeatureDetector(object): "hackrf": [ "hackrf_transfer" ], "airspy": [ "airspy_rx" ], "digital_voice_digiham": [ "digiham", "sox" ], - "digital_voice_dsd": [ "dsd", "sox" ] + "digital_voice_dsd": [ "dsd", "sox", "digiham" ] } def feature_availability(self): @@ -82,19 +84,31 @@ def has_hackrf_transfer(self): def command_exists(self, command): return os.system("which {0}".format(command)) == 0 + """ + To use DMR and YSF, the digiham package is required. You can find the package and installation instructions here: + https://github.com/jketterl/digiham + + Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work. + If you have an older verison of digiham installed, please update it along with openwebrx. + As of now, we require version 0.2 of digiham. + """ def has_digiham(self): - # the digiham tools expect to be fed via stdin, they will block until their stdin is closed. - def check_with_stdin(command): + required_version = LooseVersion("0.2") + + digiham_version_regex = re.compile('^digiham version (.*)$') + def check_digiham_version(command): try: - process = subprocess.Popen(command, stdin=subprocess.PIPE) - process.communicate("") - return process.wait() == 0 + process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE) + version = LooseVersion(digiham_version_regex.match(process.stdout.readline().decode())[1]) + process.wait(1) + return version >= required_version except FileNotFoundError: return False return reduce(and_, map( - check_with_stdin, - ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator"] + check_digiham_version, + ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator", + "digitalvoice_filter"] ), True) From 7362e48cf3fd8730ec4e102f7e8c9badf7595e9e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 15 Jun 2019 14:48:57 +0200 Subject: [PATCH 0168/2616] style more like openwebrx --- htdocs/index.html | 2 +- htdocs/openwebrx.css | 42 ++++++++++++++---------------------------- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index 8bf100182..90ac89c87 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -168,7 +168,7 @@
    -
    +
    diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 92b9f379c..ac90bc622 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -928,35 +928,20 @@ img.openwebrx-mirror-img border-color: Red; } -.openwebrx-meta-panel { - background: none; - padding: 0; -} - -.openwebrx-meta-frame { - height: 200px; - - outline: 1px solid #111; - border-top: 1px solid #555; - padding: 10px; - background: #333; -} - .openwebrx-meta-slot { - width: 133px; - height: 194px; + width: 145px; + height: 196px; float: left; margin-right: 10px; - padding:2px 0; + background-color: #676767; + padding: 2px 0; color: #333; - background: #575757; - border: 1px solid #000; - border-right: 1px solid #353535; - border-bottom: 1px solid #353535; -webkit-border-radius: 5px; -moz-border-radius: 5px; - border-radius: 2px; + border-radius: 5px; + + text-align: center; } .openwebrx-meta-slot.active { @@ -978,16 +963,17 @@ img.openwebrx-mirror-img margin-right: 0; } +.openwebrx-meta-slot .openwebrx-meta-user-image { + width:100%; + height:133px; + background-position: center; + background-repeat: no-repeat; +} + .openwebrx-meta-slot.active .openwebrx-meta-user-image { background-image: url("gfx/openwebrx-directcall.png"); - width:133px; - height:133px; } .openwebrx-meta-slot.active .openwebrx-meta-user-image.group { background-image: url("gfx/openwebrx-groupcall.png"); } - -.openwebrx-meta-slot { - text-align: center; -} From 8af8f93434b2b280adc6b7c0720eadd6252de4ba Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 15 Jun 2019 19:10:33 +0200 Subject: [PATCH 0169/2616] implement dmr timeslot muting --- csdr.py | 26 +++++++++++++++-------- htdocs/gfx/openwebrx-mute.png | Bin 0 -> 3002 bytes htdocs/openwebrx.css | 27 ++++++++++++++++++++++- htdocs/openwebrx.js | 39 +++++++++++++++++++++++++++------- 4 files changed, 74 insertions(+), 18 deletions(-) create mode 100644 htdocs/gfx/openwebrx-mute.png diff --git a/csdr.py b/csdr.py index b7d1ebd86..0ef0250d8 100755 --- a/csdr.py +++ b/csdr.py @@ -67,7 +67,8 @@ def __init__(self, output): self.secondary_fft_size = 1024 self.secondary_process_fft = None self.secondary_process_demod = None - self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", "iqtee2_pipe"] + self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", + "iqtee2_pipe", "dmr_control_pipe"] self.secondary_pipe_names=["secondary_shift_pipe"] self.secondary_offset_freq = 1000 self.unvoiced_quality = 1 @@ -113,7 +114,7 @@ def chain(self,which): else: chain += "rrc_filter | gfsk_demodulator | " if which == "dmr": - chain += "dmr_decoder --fifo {meta_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | " + chain += "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | " elif which == "ysf": chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | " max_gain = 0.0005 @@ -352,7 +353,7 @@ def set_squelch_level(self, squelch_level): actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level if self.running: self.modification_lock.acquire() - self.squelch_pipe_file.write( "%g\n"%(float(actual_squelch)) ) + self.squelch_pipe_file.write("%g\n"%(float(actual_squelch))) self.squelch_pipe_file.flush() self.modification_lock.release() @@ -363,6 +364,11 @@ def set_unvoiced_quality(self, q): def get_unvoiced_quality(self): return self.unvoiced_quality + def set_dmr_filter(self, filter): + if self.dmr_control_pipe_file: + self.dmr_control_pipe_file.write("{0}\n".format(filter)) + self.dmr_control_pipe_file.flush() + def mkfifo(self,path): try: os.unlink(path) @@ -410,7 +416,7 @@ def start(self): flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000), - unvoiced_quality = self.get_unvoiced_quality()) + unvoiced_quality = self.get_unvoiced_quality(), dmr_control_pipe = self.dmr_control_pipe) logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) my_env=os.environ.copy() @@ -430,13 +436,12 @@ def watch_thread(): self.output.add_output("audio", partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256)) # open control pipes for csdr - if self.bpf_pipe != None: - self.bpf_pipe_file=open(self.bpf_pipe,"w") + if self.bpf_pipe: + self.bpf_pipe_file = open(self.bpf_pipe, "w") if self.shift_pipe: - self.shift_pipe_file=open(self.shift_pipe,"w") + self.shift_pipe_file = open(self.shift_pipe, "w") if self.squelch_pipe: - self.squelch_pipe_file=open(self.squelch_pipe,"w") - + self.squelch_pipe_file = open(self.squelch_pipe, "w") self.start_secondary_demodulator() self.modification_lock.release() @@ -468,6 +473,9 @@ def read_meta(): return raw.rstrip("\n") self.output.add_output("meta", read_meta) + if self.dmr_control_pipe: + self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w") + def stop(self): self.modification_lock.acquire() self.running = False diff --git a/htdocs/gfx/openwebrx-mute.png b/htdocs/gfx/openwebrx-mute.png new file mode 100644 index 0000000000000000000000000000000000000000..23da7bbf190917a5797c682e86742e590d9a9143 GIT binary patch literal 3002 zcmaJ@i#O8`8{cBXHVh*nj3IIvQ_20BeN8Tt$o-O`$Tj9}n!A2wbDe9NkXu5zX0M78 z$r6#WsN5>V&!v)Af57{m=XTELJm+(s^E~G~=aXq?W62MbfPp|DegJPyJgBLEgBN;` zuTJafgFs+vpqZH+U}h#88XiIl4Dthkj^!rg8sHxrOFi{;Xjc*CfoEl0thEs{w~%Uw zS=1sgrJg@(t-hC{$PZ*?-{!c4uKBhsbLJZAuhq-)CMI`u=qh*NGxrHFH6H_=?}OcO zH@c&f?bNhJE=lrezdtF`Qd?xzoJDbptb`{l4&ED9^|hhR1)P5L{p0+KJJrA4aC>3V z9w){XFSo9Wvo0ZDBgeizsk17Q?(A_sb2M|%x=VIlep2+ttj>a)7@73UTJcM!*%P0p z%zQ;sjDvi+oIpLpX4iDcH-AISztNfWZ`_Z=P~6WUt;s+Ebee^#NeVB-4`oNa&fBP~ zEGFD06Z}yoa!EqLakf&!XYZv*`+2W<}lbCYxA>2GCm_Z%IM z4jT2vX;5G;;)s!>GG>0YpX7Zo!vU4{_8w_@x6KhWhUi;@Wex9<@Wtx?a2B~AWq?dV z#ZmG(_%ygTf?t!XxE82P_7KkL;PlS^d0XiNsrN4 z=wO)C|MMkd%=MdX9&CGH74M?Dq_0uDoOkOC+xBX>fYtIs^*MLbQn|p)@QuS7b<09k z=}c&9SYmq~;jAcpozIJFt(b-J??mORJu&t=x;DoB{E~atkf;x0Ev_=X&)6M?*+Y_r z7v~&jPhNGDGdqfv?08c@Q*BH<<)s}%#>4b`6Qkihdbyf`>!}T39P%>rj#M!TVaSv%WC-9j^1fvIw9~u4< z^Tx@UfDY7Ob2CK?fSkIKqKc^)y0$18v6v^jRL}ZM+Tv;c0gFw*FC&H_1B%vNeB$1u zID(GwPt{P}dyj|zFjDTX0Byy!(6>RPW~cs}t=-IatCw0p4b&|uiq)P5b@Ojv4v4TD zGbg=GE_WPT>vrhudDtnCp4t>@(BfXfIXGrbdR= zlfL*nW`Jp1ig_OmfLUvDg%7^vcG>1-g@j6~U$iAFsS9)2G^Zd^=N59+N}_zQ7e+HB zHnV;kpr2dWMLnKRziptn@MbJ;GtBGIt3r0LZ#@?j48TGKvBSd;m>rJ|1$OkXyr)8(eL^E|!0xqP>z7R~}2b7wxjI#6xq&O}(J?HN^ zMN{&u#}uR0L#lFRR@i&^b$GfMZ!S20|ihQ%&}TVH19ogmk7 zOhPU1_8tVQwf2x_{&`QrEk0-yPTg-;z5{oIp3{SA+|IZC!Msg0@0nJfki z)0>=6v)Q^-1fD^X%gk6ONDtms<7>nRlQmfc3ZM=u&#`?W6QZC9x>Ytt;+(;~W7sCT zXR$NKcoC2-9>v|+PzNI0d*veT#jj+CGAAeCDcOP@;B~^AeoaK-8_yLkQ>6}RY%34^ zdCfyq^+y z+@lNvQL6V19`VjB5HC7pL}boIr9qS2A=L8VP69hv-QodNr))96bCiWi!Z6l<$ELg+ zrfotB<@C%(tEm+|@4#;UB+HQ?b$iT5q%N4C@b6V+_Nc1tCSM|4{0gbD=20J@8IKc`;x-T^Izc7Z0%%3bKO*gqp1xxV7YDZW|cscx~9iq}#>Vp|%KU{aL0 zIE1QB-{z6Fl`yDzWxyy=I1%ktES;g}y1mfqXaF(Yl+OZ7|6Dt(*;*F;&-PzTcV5&BXJs}f{XMfaDY8CpzoM z!T<0eZGn(oWql&|%-J!i_%RwgynXXYRYb>!VGos0@5iBMwav zmdk|EX&@s4cC)9`*;5qTYG(dRe->dK4mnN2M*uCVTt1uNlX!`nDLubS*951}o@(_x zyzwRM;vXTZ{gk_%0mQXjY!^gl^>eqiB&K z(u^-4W=Q}c59o>halL=!3TI1`_qB@}&~^1p{|%(CV^8!g3{grZ<`e$iv8CU_h3-l~ zeZ;3a^vH(6x4lkXK=3nq;5^`;ZwI4C?gNJ0t*&f8zxdk3INJUXe?SJkf@er2Eg$3S zR!F#uB9^|A>9S-afcbgj<OiU;9U6_h(mU9ljd4+?uaKb||GWJ_`pt^;(&%vV;4%PETM??F{3=!Q+ke<3uk1U6&cCUw**q{?x^ zq{###zoY`JVx;a%=eCWcGeyS_=DpN#NL}ft)Wu_E6!>25UAqJJ2Tv2yMm&1eEiP+* zkex1W@2`Jb9iTSQM_Eap@)-FGGe*9;ns*Pn^mMsrO@t3~KjQv9cpwrEnuweYmPd0` zVO6_$Nru?#2XSw@QH@W@`2p&_jZS@pF_#=J^Vvm4h3p0T3K8Pw Z`p#&uy>eZzBMwv@2(Yj*Z!+~t`7bcFYg_;T literal 0 HcmV?d00001 diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index ac90bc622..3ed6deb27 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -937,11 +937,32 @@ img.openwebrx-mirror-img background-color: #676767; padding: 2px 0; color: #333; + + text-align: center; + position: relative; +} + +.openwebrx-meta-slot, .openwebrx-meta-slot.muted:before { -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; +} - text-align: center; +.openwebrx-meta-slot.muted:before { + display: block; + content: ""; + background-image: url("gfx/openwebrx-mute.png"); + width:100%; + height:133px; + background-position: center; + background-repeat: no-repeat; + + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: rgba(0,0,0,.3); } .openwebrx-meta-slot.active { @@ -977,3 +998,7 @@ img.openwebrx-mirror-img .openwebrx-meta-slot.active .openwebrx-meta-user-image.group { background-image: url("gfx/openwebrx-groupcall.png"); } + +.openwebrx-dmr-timeslot-panel * { + cursor: pointer; +} diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 8a8345b6e..debd3519c 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -619,7 +619,7 @@ function demodulator_analog_replace(subtype, for_digital) } demodulator_add(new demodulator_default_analog(temp_offset,subtype)); demodulator_buttons_update(); - clear_metadata(); + hide_digitalvoice_panels(); toggle_panel("openwebrx-panel-metadata-" + subtype, true); } @@ -1338,8 +1338,7 @@ function update_metadata(meta) { $(el).find(".openwebrx-dmr-target").text(target); $(el).find(".openwebrx-meta-user-image")[group ? "addClass" : "removeClass"]("group"); } else { - $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text(""); - $(".openwebrx-meta-panel").removeClass("active").removeClass("sync"); + clear_metadata(); } break; case 'YSF': @@ -1365,17 +1364,22 @@ function update_metadata(meta) { break; } else { - $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text(""); - $(".openwebrx-meta-panel").removeClass("active").removeClass("sync"); + clear_metadata(); } } -function clear_metadata() { +function hide_digitalvoice_panels() { $(".openwebrx-meta-panel").each(function(_, p){ toggle_panel(p.id, false); }); - update_metadata({}); + clear_metadata(); +} + +function clear_metadata() { + $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text(""); + $(".openwebrx-meta-slot").removeClass("active").removeClass("sync"); + $(".openwebrx-dmr-timeslot-panel").removeClass("muted"); } function add_problem(what) @@ -2327,7 +2331,7 @@ function openwebrx_init() init_rx_photo(); open_websocket(); secondary_demod_init(); - clear_metadata(); + digimodes_init(); place_panels(first_show_panel); window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); window.addEventListener("resize",openwebrx_resize); @@ -2338,6 +2342,25 @@ function openwebrx_init() } +function digimodes_init() { + hide_digitalvoice_panels(); + + // initialze DMR timeslot muting + $('.openwebrx-dmr-timeslot-panel').click(function(e) { + $(e.currentTarget).toggleClass("muted"); + update_dmr_timeslot_filtering(); + }); +} + +function update_dmr_timeslot_filtering() { + var filter = $('.openwebrx-dmr-timeslot-panel').map(function(index, el){ + return (!$(el).hasClass("muted")) << index; + }).toArray().reduce(function(acc, v){ + return acc | v; + }, 0); + webrx_set_param("dmr_filter", filter); +} + function iosPlayButtonClick() { //On iOS, we can only start audio from a click or touch event. From 4e9ef892766aeb0a48f24ee53edc581efdd1054a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 15 Jun 2019 19:26:59 +0200 Subject: [PATCH 0170/2616] use the old api for python < 3.6 --- owrx/feature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/feature.py b/owrx/feature.py index 38d3fa75b..38b8e27ed 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -99,7 +99,7 @@ def has_digiham(self): def check_digiham_version(command): try: process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE) - version = LooseVersion(digiham_version_regex.match(process.stdout.readline().decode())[1]) + version = LooseVersion(digiham_version_regex.match(process.stdout.readline().decode()).group(1)) process.wait(1) return version >= required_version except FileNotFoundError: From 3b04465106aa70def20890ce6dd349d629986429 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 15 Jun 2019 19:50:09 +0200 Subject: [PATCH 0171/2616] pointer on the overlay, too --- htdocs/openwebrx.css | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 3ed6deb27..13a1059f8 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -956,6 +956,7 @@ img.openwebrx-mirror-img height:133px; background-position: center; background-repeat: no-repeat; + cursor: pointer; position: absolute; width: 100%; From 231e4e72d9305565425c464dfb478485aebefeda Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 15 Jun 2019 21:47:28 +0200 Subject: [PATCH 0172/2616] add missing property binding --- owrx/source.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/owrx/source.py b/owrx/source.py index b02ef2860..7d6238878 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -349,7 +349,8 @@ def __init__(self, handler, sdrSource): self.localProps = self.sdrSource.getProps().collect( "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", - "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality" + "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality", + "dmr_filter" ).defaults(PropertyManager.getSharedInstance()) self.dsp = csdr.dsp(self) @@ -376,7 +377,8 @@ def set_high_cut(cut): self.localProps.getProperty("low_cut").wire(set_low_cut), self.localProps.getProperty("high_cut").wire(set_high_cut), self.localProps.getProperty("mod").wire(self.dsp.set_demodulator), - self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality) + self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality), + self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter) ] self.dsp.set_offset_freq(0) From 96468f9258d10e880b6b8d5a85ebde823bf4f806 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 19 Jun 2019 23:16:57 +0200 Subject: [PATCH 0173/2616] add a basic clickable pin that opens google maps for now --- htdocs/gfx/google_maps_pin.svg | 77 ++++++++++++++++++++++++++++++++++ htdocs/openwebrx.css | 10 +++++ htdocs/openwebrx.js | 5 ++- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 htdocs/gfx/google_maps_pin.svg diff --git a/htdocs/gfx/google_maps_pin.svg b/htdocs/gfx/google_maps_pin.svg new file mode 100644 index 000000000..2c54fe12d --- /dev/null +++ b/htdocs/gfx/google_maps_pin.svg @@ -0,0 +1,77 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 13a1059f8..5624d7978 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -1003,3 +1003,13 @@ img.openwebrx-mirror-img .openwebrx-dmr-timeslot-panel * { cursor: pointer; } + +.openwebrx-maps-pin { + background-image: url("gfx/google_maps_pin.svg"); + background-position: center; + background-repeat: no-repeat; + width: 15px; + height: 15px; + background-size: contain; + display: inline-block; +} diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index debd3519c..27cc8fceb 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1351,6 +1351,9 @@ function update_metadata(meta) { if (meta.mode && meta.mode != "") { mode = "Mode: " + meta.mode; source = meta.source || ""; + if (meta.lat && meta.lon) { + source = "" + source; + } up = meta.up ? "Up: " + meta.up : ""; down = meta.down ? "Down: " + meta.down : ""; $(el).find(".openwebrx-meta-slot").addClass("active"); @@ -1358,7 +1361,7 @@ function update_metadata(meta) { $(el).find(".openwebrx-meta-slot").removeClass("active"); } $(el).find(".openwebrx-ysf-mode").text(mode); - $(el).find(".openwebrx-ysf-source").text(source); + $(el).find(".openwebrx-ysf-source").html(source); $(el).find(".openwebrx-ysf-up").text(up); $(el).find(".openwebrx-ysf-down").text(down); From a8b2e21a5a7d9a4aee4c361b8b765d4646ba1f3f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 20 Jun 2019 14:46:04 +0200 Subject: [PATCH 0174/2616] update to python 3 --- docker/scripts/install-dependencies.sh | 2 +- docker/scripts/run.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index b3049d3be..48136b281 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -14,7 +14,7 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="sox fftw python2 netcat-openbsd libsndfile lapack" +STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack" BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers" apk add --no-cache $STATIC_PACKAGES diff --git a/docker/scripts/run.sh b/docker/scripts/run.sh index e878d2b85..a9034c591 100755 --- a/docker/scripts/run.sh +++ b/docker/scripts/run.sh @@ -16,7 +16,7 @@ _term() { trap _term SIGTERM SIGINT -python2.7 openwebrx.py $@ & +python3 openwebrx.py $@ & child=$! wait "$child" From a66b540254e108d92a79729eddd998e1f5fdbb6d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 20 Jun 2019 14:46:22 +0200 Subject: [PATCH 0175/2616] remove rtl-sdr as default (new full package coming up) --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 4a86a0399..f4c425e67 100755 --- a/build.sh +++ b/build.sh @@ -14,7 +14,7 @@ esac TAGS=$ARCH docker build --build-arg BASE_IMAGE=$BASE_IMAGE -t openwebrx-base:$ARCH -f docker/Dockerfiles/Dockerfile-base . -docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-rtlsdr:$ARCH -t jketterl/openwebrx:$ARCH -f docker/Dockerfiles/Dockerfile-rtlsdr . +docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-rtlsdr:$ARCH -f docker/Dockerfiles/Dockerfile-rtlsdr . docker build --build-arg ARCH=$ARCH -t openwebrx-soapysdr-base:$ARCH -f docker/Dockerfiles/Dockerfile-soapysdr . docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-sdrplay:$ARCH -f docker/Dockerfiles/Dockerfile-sdrplay . docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-hackrf:$ARCH -f docker/Dockerfiles/Dockerfile-hackrf . From f16a5f92e65a43fe14ec6dd6afb78601f3e22e7d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 20 Jun 2019 14:47:03 +0200 Subject: [PATCH 0176/2616] hackrf does not depend on soapy the way it's implemented now --- docker/Dockerfiles/Dockerfile-hackrf | 2 +- docker/scripts/install-dependencies-hackrf.sh | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docker/Dockerfiles/Dockerfile-hackrf b/docker/Dockerfiles/Dockerfile-hackrf index 55a753013..6afbc4a40 100644 --- a/docker/Dockerfiles/Dockerfile-hackrf +++ b/docker/Dockerfiles/Dockerfile-hackrf @@ -1,5 +1,5 @@ ARG ARCH -FROM openwebrx-soapysdr-base:$ARCH +FROM openwebrx-base:$ARCH ADD docker/scripts/install-dependencies-hackrf.sh / RUN /install-dependencies-hackrf.sh diff --git a/docker/scripts/install-dependencies-hackrf.sh b/docker/scripts/install-dependencies-hackrf.sh index 3ca1c75b0..1a460cc35 100755 --- a/docker/scripts/install-dependencies-hackrf.sh +++ b/docker/scripts/install-dependencies-hackrf.sh @@ -26,7 +26,4 @@ cmakebuild host cd .. rm -rf hackrf -git clone https://github.com/pothosware/SoapyHackRF.git -cmakebuild SoapyHackRF - apk del .build-deps From 84ddcbb74d98275d4b9dfaa921f9a892c278ceac Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 20 Jun 2019 14:56:52 +0200 Subject: [PATCH 0177/2616] add a full build for multi-sdr support --- build.sh | 1 + docker/Dockerfiles/Dockerfile-full | 10 ++++++++++ push.sh | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 docker/Dockerfiles/Dockerfile-full diff --git a/build.sh b/build.sh index f4c425e67..2f006e421 100755 --- a/build.sh +++ b/build.sh @@ -18,3 +18,4 @@ docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-rtlsdr:$ARCH -f docker docker build --build-arg ARCH=$ARCH -t openwebrx-soapysdr-base:$ARCH -f docker/Dockerfiles/Dockerfile-soapysdr . docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-sdrplay:$ARCH -f docker/Dockerfiles/Dockerfile-sdrplay . docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-hackrf:$ARCH -f docker/Dockerfiles/Dockerfile-hackrf . +docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-full:$ARCH -t jketterl/openwebrx:$ARCH -f docker/Dockerfiles/Dockerfile-full . diff --git a/docker/Dockerfiles/Dockerfile-full b/docker/Dockerfiles/Dockerfile-full new file mode 100644 index 000000000..a71a0a425 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-full @@ -0,0 +1,10 @@ +ARG ARCH +FROM openwebrx-base:$ARCH + +ADD docker/scripts/install-dependencies-*.sh / +ADD docker/scripts/install-lib.*.patch / + +RUN /install-dependencies-rtlsdr.sh +RUN /install-dependencies-hackrf.sh +RUN /install-dependencies-soapysdr.sh +RUN /install-dependencies-sdrplay.sh diff --git a/push.sh b/push.sh index 6844705e4..ac5d13250 100755 --- a/push.sh +++ b/push.sh @@ -3,6 +3,6 @@ set -euxo pipefail ARCH=$(uname -m) -for image in openwebrx openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf; do +for image in openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-full openwebrx; do docker push jketterl/$image:$ARCH done From 08edcd44ef43be64954bb14e752b6853a219b53e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 20 Jun 2019 15:37:21 +0200 Subject: [PATCH 0178/2616] add an airspy image --- build.sh | 1 + docker/Dockerfiles/Dockerfile-airspy | 6 +++++ docker/Dockerfiles/Dockerfile-full | 1 + docker/scripts/install-dependencies-airspy.sh | 26 +++++++++++++++++++ push.sh | 2 +- 5 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 docker/Dockerfiles/Dockerfile-airspy create mode 100755 docker/scripts/install-dependencies-airspy.sh diff --git a/build.sh b/build.sh index 2f006e421..1aa044a4d 100755 --- a/build.sh +++ b/build.sh @@ -18,4 +18,5 @@ docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-rtlsdr:$ARCH -f docker docker build --build-arg ARCH=$ARCH -t openwebrx-soapysdr-base:$ARCH -f docker/Dockerfiles/Dockerfile-soapysdr . docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-sdrplay:$ARCH -f docker/Dockerfiles/Dockerfile-sdrplay . docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-hackrf:$ARCH -f docker/Dockerfiles/Dockerfile-hackrf . +docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-airspy:$ARCH -f docker/Dockerfiles/Dockerfile-airspy . docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-full:$ARCH -t jketterl/openwebrx:$ARCH -f docker/Dockerfiles/Dockerfile-full . diff --git a/docker/Dockerfiles/Dockerfile-airspy b/docker/Dockerfiles/Dockerfile-airspy new file mode 100644 index 000000000..09425b68b --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-airspy @@ -0,0 +1,6 @@ +ARG ARCH +FROM openwebrx-base:$ARCH + +ADD docker/scripts/install-dependencies-airspy.sh / +RUN /install-dependencies-airspy.sh + diff --git a/docker/Dockerfiles/Dockerfile-full b/docker/Dockerfiles/Dockerfile-full index a71a0a425..e50251598 100644 --- a/docker/Dockerfiles/Dockerfile-full +++ b/docker/Dockerfiles/Dockerfile-full @@ -8,3 +8,4 @@ RUN /install-dependencies-rtlsdr.sh RUN /install-dependencies-hackrf.sh RUN /install-dependencies-soapysdr.sh RUN /install-dependencies-sdrplay.sh +RUN /install-dependencies-airspy.sh diff --git a/docker/scripts/install-dependencies-airspy.sh b/docker/scripts/install-dependencies-airspy.sh new file mode 100755 index 000000000..e4fcac6f9 --- /dev/null +++ b/docker/scripts/install-dependencies-airspy.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euxo pipefail + +function cmakebuild() { + cd $1 + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libusb" +BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" + +apk add --no-cache $STATIC_PACKAGES +apk add --no-cache --virtual .build-deps $BUILD_PACKAGES + +git clone https://github.com/airspy/airspyone_host.git +cmakebuild airspyone_host + +apk del .build-deps diff --git a/push.sh b/push.sh index ac5d13250..0288c810a 100755 --- a/push.sh +++ b/push.sh @@ -3,6 +3,6 @@ set -euxo pipefail ARCH=$(uname -m) -for image in openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-full openwebrx; do +for image in openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-full openwebrx; do docker push jketterl/$image:$ARCH done From 7e0591f0a6478b32619e1f5f129ee2d9199b04ca Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 22 Jun 2019 18:31:23 +0200 Subject: [PATCH 0179/2616] disable squelch for packet, too --- csdr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csdr.py b/csdr.py index 87114d3c0..93e05d010 100755 --- a/csdr.py +++ b/csdr.py @@ -360,7 +360,7 @@ def get_bpf(self): def set_squelch_level(self, squelch_level): self.squelch_level=squelch_level #no squelch required on digital voice modes - actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level + actual_squelch = 0 if self.isDigitalVoice() or self.isPacket() else self.squelch_level if self.running: self.modification_lock.acquire() self.squelch_pipe_file.write("%g\n"%(float(actual_squelch))) From c3411b88566d99693657529aaa5c945f11bdfc1a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 30 Jun 2019 15:57:32 +0200 Subject: [PATCH 0180/2616] update readme with recent stuff --- README.md | 81 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index d308c36f7..d505a7965 100644 --- a/README.md +++ b/README.md @@ -9,53 +9,66 @@ OpenWebRX is a multi-user SDR receiver software with a web interface. It has the following features: -- csdr based demodulators (AM/FM/SSB/CW/BPSK31), +- [csdr](https://github.com/simonyiszk/csdr) based demodulators (AM/FM/SSB/CW/BPSK31), - filter passband can be set from GUI, -- waterfall display can be shifted back in time, -- it extensively uses HTML5 features like WebSocket, Web Audio API, and <canvas>, -- it works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28), -- currently supports RTL-SDR, HackRF, SDRplay, AirSpy and many other devices, see the OpenWebRX Wiki, -- it has a 3D waterfall display: +- it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas +- it works in Google Chrome, Chromium and Mozilla Firefox +- currently supports RTL-SDR, HackRF, SDRplay, AirSpy +- Multiple SDR devices can be used simultaneously +- [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF) +- [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN) + +**News (2019-06-30 by DD5JFK)** +- I have done some major rework on the openwebrx core, and I am planning to continue adding more features in the near future. Please check this place for updates. +- My work has not been accepted into the upstream repository, so you will need to chose between my fork and the official version. +- I have enabled the issue tracker on this project, so feel free to file bugs or suggest enhancements there! +- This version sports the following new and amazing features: + - Support of multiple SDR devices simultaneously + - Support for multiple profiles per SDR that allow the user to listen to different frequencies + - Support for digital voice decoding + - Feature detection that will disable functionality when dependencies are not available (if you're missing the digital buttons, this is probably why) +- Raspbian SD Card Images and Docker builds available (see below) +- I am currently working on the feature set for a stable release, but you are more than welcome to test development versions! + +> When upgrading OpenWebRX, please make sure that you also upgrade *csdr* and *digiham*! -![OpenWebRX 3D waterfall](http://blog.sdr.hu/images/openwebrx/screenshot-3d.gif) +## OpenWebRX servers on SDR.hu -**News (2015-08-18)** -- My BSc. thesis written on OpenWebRX is available here. -- Several bugs were fixed to improve reliability and stability. -- OpenWebRX now supports compression of audio and waterfall stream, so the required network uplink bandwidth has been decreased from 2 Mbit/s to about 200 kbit/s per client! (Measured with the default settings. It is also dependent on `fft_size`.) -- OpenWebRX now uses sdr.js (*libcsdr* compiled to JavaScript) for some client-side DSP tasks. -- Receivers can now be listed on SDR.hu. -- License for OpenWebRX is now Affero GPL v3. +[SDR.hu](http://sdr.hu) is a site which lists the active, public OpenWebRX servers. Your receiver [can also be part of it](http://sdr.hu/openwebrx), if you want. -**News (2016-02-14)** -- The DDC in *csdr* has been manually optimized for ARM NEON, so it runs around 3 times faster on the Raspberry Pi 2 than before. -- Also we use *ncat* instead of *rtl_mus*, and it is 3 times faster in some cases. -- OpenWebRX now supports URLs like: `http://localhost:8073/#freq=145555000,mod=usb` -- UI improvements were made, thanks to John Seamons and Gnoxter. +![sdr.hu](http://blog.sdr.hu/images/openwebrx/screenshot-sdrhu.png) -**News (2017-04-04)** -- *ncat* has been replaced with a custom implementation called *nmux* due to a bug that caused regular crashes on some machines. The *nmux* tool is part of the *csdr* package. -- Most consumer SDR devices are supported via rx_tools, see the OpenWebRX Wiki on that. +## Setup -**News (2017-07-12)** -- OpenWebRX now has a BPSK31 demodulator and a 3D waterfall display. +### Raspberry Pi SD Card Images -> When upgrading OpenWebRX, please make sure that you also upgrade *csdr*! +Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-06-21-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+. -## OpenWebRX servers on SDR.hu +This is based off the Raspbian Lite distribution, so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply. -[SDR.hu](http://sdr.hu) is a site which lists the active, public OpenWebRX servers. Your receiver [can also be part of it](http://sdr.hu/openwebrx), if you want. +Please note: I have not updated this to include the Raspberry Pi 4 yet. (It seems to be impossible to build Rasbpian Buster images on x86 hardware right now. Stay tuned!) -![sdr.hu](http://blog.sdr.hu/images/openwebrx/screenshot-sdrhu.png) +Once you have booted a Raspberry with the SD Card, it will appear in your network with the hostname "openwebrx", which should make it available as http://openwebrx:8073/ on most networks. This may vary depending on your specific setup. -## Setup +For Digital voice, the minimum requirement right now seems to be a Rasbperry Pi 3B+. I would like to work on optimizing this for lower specs, but at this point I am not sure how much can be done. + +### Docker Images + +For those familiar with docker, I am providing [recent builds and Releases for both x86 and arm processors on the Docker hub](https://hub.docker.com/r/jketterl/openwebrx). You can find a short introduction there. + +### Manual Installation -OpenWebRX currently requires Linux and python 2.7 to run. +OpenWebRX currently requires Linux and python 3 to run. First you will need to install the dependencies: -- libcsdr -- rtl-sdr +- [csdr](https://github.com/simonyiszk/csdr) +- [rtl-sdr](http://sdr.osmocom.org/trac/wiki/rtl-sdr) + +Optional Dependencies if you want to be able to listen do digital voice: + +- [digiham](https://github.com/jketterl/digiham) +- [dsd](https://github.com/f4exb/dsdcc) After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server: @@ -65,7 +78,7 @@ You can now open the GUI at http://localhost:807 Please note that the server is also listening on the following ports (on localhost only): -- port 4951 for the multi-user I/Q server. +- ports 4950 to 4960 for the multi-user I/Q servers. Now the next step is to customize the parameters of your server in `config_webrx.py`. @@ -86,8 +99,6 @@ If you have any problems installing OpenWebRX, you should check out the summary). From 0e205ec1d98d8e2e7316560c3eaba5c2d9c3ffd9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jul 2019 11:16:05 +0200 Subject: [PATCH 0181/2616] remove unused html files --- htdocs/inactive.html | 85 --------------------------------------- htdocs/retry.html | 94 ------------------------------------------- htdocs/upgrade.html | 95 -------------------------------------------- 3 files changed, 274 deletions(-) delete mode 100644 htdocs/inactive.html delete mode 100644 htdocs/retry.html delete mode 100644 htdocs/upgrade.html diff --git a/htdocs/inactive.html b/htdocs/inactive.html deleted file mode 100644 index c7214c5c8..000000000 --- a/htdocs/inactive.html +++ /dev/null @@ -1,85 +0,0 @@ - - -OpenWebRX - - - - -
    - -
    - Sorry, the receiver is inactive due to internal error. -
    -
    - - - diff --git a/htdocs/retry.html b/htdocs/retry.html deleted file mode 100644 index 466c7eeb6..000000000 --- a/htdocs/retry.html +++ /dev/null @@ -1,94 +0,0 @@ - - -OpenWebRX - - - - - - -
    - -
    - There are no client slots left on this server. -
    - Please wait until a client disconnects.
    We will try to reconnect in 30 seconds... -
    -
    -
    - - - diff --git a/htdocs/upgrade.html b/htdocs/upgrade.html deleted file mode 100644 index 09b5aabbe..000000000 --- a/htdocs/upgrade.html +++ /dev/null @@ -1,95 +0,0 @@ - - -OpenWebRX - - - - - - -
    - -
    - Only the latest Google Chrome browser is supported at the moment.
    - Please download and install Google Chrome.
    -
    - Alternatively, you may proceed to OpenWebRX, but it's not supposed to work as expected.
    - Click here if you still want to try OpenWebRX. -
    -
    -
    - - - From f283a1ad68eb9e2faa05f13abe6c449cf9da15f4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jul 2019 11:47:07 +0200 Subject: [PATCH 0182/2616] prepare for different types of connections --- htdocs/openwebrx.js | 2 +- owrx/connection.py | 20 ++++++++++++++------ owrx/controllers.py | 1 - 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 27cc8fceb..529754914 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1431,7 +1431,7 @@ function waterfall_dequeue() function on_ws_opened() { - ws.send("SERVER DE CLIENT openwebrx.js"); + ws.send("SERVER DE CLIENT client=openwebrx.js type=receiver"); divlog("WebSocket opened to "+ws_url); } diff --git a/owrx/connection.py b/owrx/connection.py index 67ee96bd8..a6d268a33 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -1,12 +1,13 @@ from owrx.config import PropertyManager from owrx.source import DspManager, CpuUsageThread, SdrService, ClientRegistry from owrx.feature import FeatureDetector +from owrx.version import openwebrx_version import json import logging logger = logging.getLogger(__name__) -class OpenWebRxClient(object): +class OpenWebRxReceiverClient(object): config_keys = ["waterfall_colors", "waterfall_min_level", "waterfall_max_level", "waterfall_auto_level_margin", "lfo_offset", "samp_rate", "fft_size", "fft_fps", "audio_compression", "fft_compression", "max_clients", "start_mod", @@ -53,10 +54,10 @@ def setSdr(self, id = None): self.sdr = next # send initial config - configProps = self.sdr.getProps().collect(*OpenWebRxClient.config_keys).defaults(PropertyManager.getSharedInstance()) + configProps = self.sdr.getProps().collect(*OpenWebRxReceiverClient.config_keys).defaults(PropertyManager.getSharedInstance()) def sendConfig(key, value): - config = dict((key, configProps[key]) for key in OpenWebRxClient.config_keys) + config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys) # TODO mathematical properties? hmmmm config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"] self.write_config(config) @@ -143,11 +144,18 @@ def __init__(self): def handleTextMessage(self, conn, message): if (message[:16] == "SERVER DE CLIENT"): - # maybe put some more info in there? nothing to store yet. - self.handshake = "completed" + meta = message[17:].split(" ") + self.handshake = {v[0]: "=".join(v[1:]) for v in map(lambda x: x.split("="), meta)} + + conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version = openwebrx_version)) logger.debug("client connection intitialized") - self.client = OpenWebRxClient(conn) + if "type" in self.handshake: + if self.handshake["type"] == "receiver": + self.client = OpenWebRxReceiverClient(conn) + # backwards compatibility + else: + self.client = OpenWebRxReceiverClient(conn) return diff --git a/owrx/controllers.py b/owrx/controllers.py index 774ba9b4a..75c275888 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -82,6 +82,5 @@ def handle_request(self): class WebSocketController(Controller): def handle_request(self): conn = WebSocketConnection(self.handler, WebSocketMessageHandler()) - conn.send("CLIENT DE SERVER openwebrx.py") # enter read loop conn.read_loop() From a4a306374d4e904217cb0196bad96ddf38c6b064 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jul 2019 16:49:39 +0200 Subject: [PATCH 0183/2616] add some map basics --- htdocs/map.html | 12 ++++++++++++ htdocs/map.js | 30 ++++++++++++++++++++++++++++++ owrx/controllers.py | 4 ++++ owrx/http.py | 5 +++-- 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 htdocs/map.html create mode 100644 htdocs/map.js diff --git a/htdocs/map.html b/htdocs/map.html new file mode 100644 index 000000000..272207d83 --- /dev/null +++ b/htdocs/map.html @@ -0,0 +1,12 @@ + + + + OpenWebRX | Open Source SDR Web App for Everyone! + + + + + + + + diff --git a/htdocs/map.js b/htdocs/map.js new file mode 100644 index 000000000..c47935aa0 --- /dev/null +++ b/htdocs/map.js @@ -0,0 +1,30 @@ +(function(){ + var protocol = 'ws'; + if (window.location.toString().startsWith('https://')) { + protocol = 'wss'; + } + + var ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; + if (!("WebSocket" in window)) return; + + var ws = new WebSocket(ws_url); + ws.onopen = function(){ + console.info("onopen"); + ws.send("SERVER DE CLIENT client=map.js type=map"); + }; + ws.onmessage = function(){ + console.info("onmessage"); + }; + ws.onclose = function(){ + console.info("onclose"); + }; + + window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript + ws.onclose = function () {}; + ws.close(); + }; + ws.onerror = function(){ + console.info("onerror"); + }; + +})(); \ No newline at end of file diff --git a/owrx/controllers.py b/owrx/controllers.py index 75c275888..883a4032d 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -79,6 +79,10 @@ class IndexController(AssetsController): def handle_request(self): self.serve_file("index.html", content_type = "text/html") +class MapController(AssetsController): + def handle_request(self): + self.serve_file("map.html", content_type = "text/html") + class WebSocketController(Controller): def handle_request(self): conn = WebSocketConnection(self.handler, WebSocketMessageHandler()) diff --git a/owrx/http.py b/owrx/http.py index ca7d357f3..b449fab18 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -1,4 +1,4 @@ -from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController +from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController, MapController from http.server import BaseHTTPRequestHandler import re @@ -20,7 +20,8 @@ class Router(object): {"route": "/ws/", "controller": WebSocketController}, {"regex": "(/favicon.ico)", "controller": AssetsController}, # backwards compatibility for the sdr.hu portal - {"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController} + {"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController}, + {"route": "/map", "controller": MapController} ] def find_controller(self, path): for m in Router.mappings: From 893f69ad18ff7f90aff39a72f11a224f90f5d695 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 30 Jun 2019 16:24:56 +0200 Subject: [PATCH 0184/2616] chain as list as a first step to better flexibility --- csdr.py | 106 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 74 insertions(+), 32 deletions(-) diff --git a/csdr.py b/csdr.py index 0ef0250d8..31ee80a62 100755 --- a/csdr.py +++ b/csdr.py @@ -76,62 +76,103 @@ def __init__(self, output): self.output = output def chain(self,which): - chain ="nc -v 127.0.0.1 {nc_port} | " - if self.csdr_dynamic_bufsize: chain += "csdr setbuf {start_bufsize} | " - if self.csdr_through: chain +="csdr through | " + chain = ["nc -v 127.0.0.1 {nc_port}"] + if self.csdr_dynamic_bufsize: chain += ["csdr setbuf {start_bufsize}"] + if self.csdr_through: chain += ["csdr through"] if which == "fft": - chain += "csdr fft_cc {fft_size} {fft_block_size} | " + \ - ("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \ + chain += [ + "csdr fft_cc {fft_size} {fft_block_size}", + "csdr logpower_cf -70" if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages}", "csdr fft_exchange_sides_ff {fft_size}" - if self.fft_compression=="adpcm": - chain += " | csdr compress_fft_adpcm_f_u8 {fft_size}" + ] + if self.fft_compression == "adpcm": + chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"] return chain - chain += "csdr shift_addition_cc --fifo {shift_pipe} | " - chain += "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | " - chain += "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every} | " + chain += [ + "csdr shift_addition_cc --fifo {shift_pipe}", + "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING", + "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING", + "csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}" + ] if self.secondary_demodulator: - chain += "csdr tee {iqtee_pipe} | " - chain += "csdr tee {iqtee2_pipe} | " + chain += [ + "csdr tee {iqtee_pipe}", + "csdr tee {iqtee2_pipe}" + ] # safe some cpu cycles... no need to decimate if decimation factor is 1 - last_decimation_block = "csdr fractional_decimator_ff {last_decimation} | " if self.last_decimation != 1.0 else "" + last_decimation_block = ["csdr fractional_decimator_ff {last_decimation}"] if self.last_decimation != 1.0 else [] if which == "nfm": - chain += "csdr fmdemod_quadri_cf | csdr limit_ff | " + chain += [ + "csdr fmdemod_quadri_cf", + "csdr limit_ff" + ] chain += last_decimation_block - chain += "csdr deemphasis_nfm_ff {output_rate} | csdr convert_f_s16" + chain += [ + "csdr deemphasis_nfm_ff {output_rate}", + "csdr convert_f_s16" + ] elif self.isDigitalVoice(which): - chain += "csdr fmdemod_quadri_cf | dc_block | " + chain += [ + "csdr fmdemod_quadri_cf", + "dc_block " + ] chain += last_decimation_block # dsd modes if which in [ "dstar", "nxdn" ]: - chain += "csdr limit_ff | csdr convert_f_s16 | " + chain += [ + "csdr limit_ff", + "csdr convert_f_s16" + ] if which == "dstar": - chain += "dsd -fd" + chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "] elif which == "nxdn": - chain += "dsd -fi" - chain += " -i - -o - -u {unvoiced_quality} -g -1 | CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f | " + chain += ["dsd -fi -i - -o - -u {unvoiced_quality} -g -1 "] + chain += ["CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f"] max_gain = 5 # digiham modes else: - chain += "rrc_filter | gfsk_demodulator | " + chain += [ + "rrc_filter", + "gfsk_demodulator" + ] if which == "dmr": - chain += "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | " + chain += [ + "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe}", + "mbe_synthesizer -f -u {unvoiced_quality}" + ] elif which == "ysf": - chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | " + chain += [ + "ysf_decoder --fifo {meta_pipe}", + "mbe_synthesizer -y -f -u {unvoiced_quality}" + ] max_gain = 0.0005 - chain += "digitalvoice_filter -f | " - chain += "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain} | ".format(max_gain=max_gain) - chain += "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + chain += [ + "digitalvoice_filter -f", + "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain}".format(max_gain=max_gain), + "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + ] elif which == "am": - chain += "csdr amdemod_cf | csdr fastdcblock_ff | " + chain += [ + "csdr amdemod_cf", + "csdr fastdcblock_ff" + ] chain += last_decimation_block - chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" + chain += [ + "csdr agc_ff", + "csdr limit_ff", + "csdr convert_f_s16" + ] elif which == "ssb": - chain += "csdr realpart_cf | " + chain += ["csdr realpart_cf"] chain += last_decimation_block - chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" + chain += [ + "csdr agc_ff", + "csdr limit_ff", + "csdr convert_f_s16" + ] if self.audio_compression=="adpcm": - chain += " | csdr encode_ima_adpcm_i16_u8" + chain += ["csdr encode_ima_adpcm_i16_u8"] return chain def secondary_chain(self, which): @@ -402,7 +443,8 @@ def start(self): return self.running = True - command_base=self.chain(self.demodulator) + command_base = " | ".join(self.chain(self.demodulator)) + logger.debug(command_base) #create control pipes for csdr self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self)) From 2324a2c837fdc12bfe1190fef2682eb6b6881acc Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jul 2019 19:49:58 +0200 Subject: [PATCH 0185/2616] add google maps --- htdocs/map.css | 4 +++ htdocs/map.html | 2 +- htdocs/map.js | 33 +++++++++++++++++++++--- owrx/connection.py | 61 +++++++++++++++++++++++++++++++++++---------- owrx/controllers.py | 1 + 5 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 htdocs/map.css diff --git a/htdocs/map.css b/htdocs/map.css new file mode 100644 index 000000000..4b5fb0040 --- /dev/null +++ b/htdocs/map.css @@ -0,0 +1,4 @@ +html, body { + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/htdocs/map.html b/htdocs/map.html index 272207d83..fb8241e05 100644 --- a/htdocs/map.html +++ b/htdocs/map.html @@ -4,7 +4,7 @@ OpenWebRX | Open Source SDR Web App for Everyone! - + diff --git a/htdocs/map.js b/htdocs/map.js index c47935aa0..08e88096a 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -9,11 +9,38 @@ var ws = new WebSocket(ws_url); ws.onopen = function(){ - console.info("onopen"); ws.send("SERVER DE CLIENT client=map.js type=map"); }; - ws.onmessage = function(){ - console.info("onmessage"); + + ws.onmessage = function(e){ + if (typeof e.data != 'string') { + console.error("unsupported binary data on websocket; ignoring"); + return + } + if (e.data.substr(0, 16) == "CLIENT DE SERVER") { + console.log("Server acknowledged WebSocket connection."); + return + } + try { + json = JSON.parse(e.data); + switch (json.type) { + case "config": + var config = json.value; + $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ + var map = new google.maps.Map($('body')[0], { + center: { + lat: config.receiver_gps[0], + lng: config.receiver_gps[1] + }, + zoom: 8 + }); + }) + break + } + } catch (e) { + // don't lose exception + console.error(e); + } }; ws.onclose = function(){ console.info("onclose"); diff --git a/owrx/connection.py b/owrx/connection.py index a6d268a33..95fcf7626 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -7,14 +7,33 @@ import logging logger = logging.getLogger(__name__) -class OpenWebRxReceiverClient(object): +class Client(object): + def __init__(self, conn): + self.conn = conn + + def protected_send(self, data): + try: + self.conn.send(data) + # these exception happen when the socket is closed + except OSError: + self.close() + except ValueError: + self.close() + + def close(self): + self.conn.close() + logger.debug("connection closed") + + +class OpenWebRxReceiverClient(Client): config_keys = ["waterfall_colors", "waterfall_min_level", "waterfall_max_level", "waterfall_auto_level_margin", "lfo_offset", "samp_rate", "fft_size", "fft_fps", "audio_compression", "fft_compression", "max_clients", "start_mod", "client_audio_buffer_size", "start_freq", "center_freq", "mathbox_waterfall_colors", "mathbox_waterfall_history_length", "mathbox_waterfall_frequency_resolution"] + def __init__(self, conn): - self.conn = conn + super().__init__(conn) self.dsp = None self.sdr = None @@ -79,8 +98,7 @@ def close(self): if self.configSub is not None: self.configSub.cancel() self.configSub = None - self.conn.close() - logger.debug("connection closed") + super().close() def stopDsp(self): if self.dsp is not None: @@ -100,42 +118,57 @@ def setDspProperties(self, params): for key, value in params.items(): self.dsp.setProperty(key, value) - def protected_send(self, data): - try: - self.conn.send(data) - # these exception happen when the socket is closed - except OSError: - self.close() - except ValueError: - self.close() - def write_spectrum_data(self, data): self.protected_send(bytes([0x01]) + data) + def write_dsp_data(self, data): self.protected_send(bytes([0x02]) + data) + def write_s_meter_level(self, level): self.protected_send({"type":"smeter","value":level}) + def write_cpu_usage(self, usage): self.protected_send({"type":"cpuusage","value":usage}) + def write_clients(self, clients): self.protected_send({"type":"clients","value":clients}) + def write_secondary_fft(self, data): self.protected_send(bytes([0x03]) + data) + def write_secondary_demod(self, data): self.protected_send(bytes([0x04]) + data) + def write_secondary_dsp_config(self, cfg): self.protected_send({"type":"secondary_config", "value":cfg}) + def write_config(self, cfg): self.protected_send({"type":"config","value":cfg}) + def write_receiver_details(self, details): self.protected_send({"type":"receiver_details","value":details}) + def write_profiles(self, profiles): self.protected_send({"type":"profiles","value":profiles}) + def write_features(self, features): self.protected_send({"type":"features","value":features}) + def write_metadata(self, metadata): self.protected_send({"type":"metadata","value":metadata}) + +class MapConnection(Client): + def __init__(self, conn): + super().__init__(conn) + + pm = PropertyManager.getSharedInstance() + self.write_config(pm.collect("google_maps_api_key", "receiver_gps").__dict__()) + + def write_config(self, cfg): + self.protected_send({"type":"config","value":cfg}) + + class WebSocketMessageHandler(object): def __init__(self): self.handshake = None @@ -153,6 +186,8 @@ def handleTextMessage(self, conn, message): if "type" in self.handshake: if self.handshake["type"] == "receiver": self.client = OpenWebRxReceiverClient(conn) + if self.handshake["type"] == "map": + self.client = MapConnection(conn) # backwards compatibility else: self.client = OpenWebRxReceiverClient(conn) diff --git a/owrx/controllers.py b/owrx/controllers.py index 883a4032d..5a915fa57 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -81,6 +81,7 @@ def handle_request(self): class MapController(AssetsController): def handle_request(self): + #TODO check if we have a google maps api key first? self.serve_file("map.html", content_type = "text/html") class WebSocketController(Controller): From 272caa71007ee0f31c89dc8285aec879c1055e65 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jul 2019 19:51:31 +0200 Subject: [PATCH 0186/2616] rename title --- htdocs/map.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/map.html b/htdocs/map.html index fb8241e05..ee8908cd8 100644 --- a/htdocs/map.html +++ b/htdocs/map.html @@ -1,7 +1,7 @@ - OpenWebRX | Open Source SDR Web App for Everyone! + OpenWebRX Map From 3b2b51f07c612283b2a8373bc9e4de5c10a6af0f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jul 2019 21:20:53 +0200 Subject: [PATCH 0187/2616] display locations parsed from ysf on map --- htdocs/map.js | 33 +++++++++++++++++++++++- owrx/connection.py | 10 ++++++++ owrx/map.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++ owrx/meta.py | 18 +++++++++++++- 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 owrx/map.py diff --git a/htdocs/map.js b/htdocs/map.js index 08e88096a..9dfb24d37 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -12,6 +12,33 @@ ws.send("SERVER DE CLIENT client=map.js type=map"); }; + var map; + var markers = {}; + var updateQueue = []; + + var processUpdates = function(updates) { + if (!map) { + updateQueue = updateQueue.concat(updates); + return; + } + updates.forEach(function(update){ + // TODO maidenhead locator implementation + if (update.location.type != 'latlon') return; + var pos = new google.maps.LatLng(update.location.lat, update.location.lon) + if (markers[update.callsign]) { + console.info("updating"); + markers[update.callsign].setPosition(pos); + } else { + console.info("initializing"); + markers[update.callsign] = new google.maps.Marker({ + position: pos, + map: map, + title: update.callsign + }); + } + }); + } + ws.onmessage = function(e){ if (typeof e.data != 'string') { console.error("unsupported binary data on websocket; ignoring"); @@ -27,15 +54,19 @@ case "config": var config = json.value; $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ - var map = new google.maps.Map($('body')[0], { + map = new google.maps.Map($('body')[0], { center: { lat: config.receiver_gps[0], lng: config.receiver_gps[1] }, zoom: 8 }); + processUpdates(updateQueue); }) break + case "update": + processUpdates(json.value); + break } } catch (e) { // don't lose exception diff --git a/owrx/connection.py b/owrx/connection.py index 95fcf7626..328697545 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -3,10 +3,12 @@ from owrx.feature import FeatureDetector from owrx.version import openwebrx_version import json +from owrx.map import Map import logging logger = logging.getLogger(__name__) + class Client(object): def __init__(self, conn): self.conn = conn @@ -165,9 +167,17 @@ def __init__(self, conn): pm = PropertyManager.getSharedInstance() self.write_config(pm.collect("google_maps_api_key", "receiver_gps").__dict__()) + Map.getSharedInstance().addClient(self) + + def close(self): + Map.getSharedInstance().removeClient(self) + super().close() + def write_config(self, cfg): self.protected_send({"type":"config","value":cfg}) + def write_update(self, update): + self.protected_send({"type":"update","value":update}) class WebSocketMessageHandler(object): def __init__(self): diff --git a/owrx/map.py b/owrx/map.py new file mode 100644 index 000000000..a799c92fc --- /dev/null +++ b/owrx/map.py @@ -0,0 +1,62 @@ +from datetime import datetime + + +class Location(object): + def __dict__(self): + return {} + + +class Map(object): + sharedInstance = None + @staticmethod + def getSharedInstance(): + if Map.sharedInstance is None: + Map.sharedInstance = Map() + return Map.sharedInstance + + def __init__(self): + self.clients = [] + self.positions = {} + super().__init__() + + def broadcast(self, update): + for c in self.clients: + c.write_update(update) + + def addClient(self, client): + self.clients.append(client) + client.write_update([{"callsign": callsign, "location": record["loc"].__dict__()} for (callsign, record) in self.positions.items()]) + + def removeClient(self, client): + try: + self.clients.remove(client) + except ValueError: + pass + + def updateLocation(self, callsign, loc: Location): + self.positions[callsign] = {"loc": loc, "updated": datetime.now()} + self.broadcast([{"callsign": callsign, "location": loc.__dict__()}]) + + +class LatLngLocation(Location): + def __init__(self, lat: float, lon: float): + self.lat = lat + self.lon = lon + + def __dict__(self): + return { + "type":"latlon", + "lat":self.lat, + "lon":self.lon + } + + +class LocatorLocation(Location): + def __init__(self, locator: str): + self.locator = locator + + def __dict__(self): + return { + "type":"locator", + "locator":self.locator + } diff --git a/owrx/meta.py b/owrx/meta.py index ec4966ae9..ad3f0d35d 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import logging import threading +from owrx.map import Map, LatLngLocation logger = logging.getLogger(__name__) @@ -14,18 +15,22 @@ def getSharedInstance(): if DmrCache.sharedInstance is None: DmrCache.sharedInstance = DmrCache() return DmrCache.sharedInstance + def __init__(self): self.cache = {} self.cacheTimeout = timedelta(seconds = 86400) + def isValid(self, key): if not key in self.cache: return False entry = self.cache[key] return entry["timestamp"] + self.cacheTimeout > datetime.now() + def put(self, key, value): self.cache[key] = { "timestamp": datetime.now(), "data": value } + def get(self, key): if not self.isValid(key): return None return self.cache[key]["data"] @@ -34,6 +39,7 @@ def get(self, key): class DmrMetaEnricher(object): def __init__(self): self.threads = {} + def downloadRadioIdData(self, id): cache = DmrCache.getSharedInstance() try: @@ -44,6 +50,7 @@ def downloadRadioIdData(self, id): except json.JSONDecodeError: cache.put(id, None) del self.threads[id] + def enrich(self, meta): if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: return None if not "source" in meta: return None @@ -60,9 +67,18 @@ def enrich(self, meta): return None +class YsfMetaEnricher(object): + def enrich(self, meta): + if "source" in meta and "lat" in meta and "lon" in meta: + # TODO parsing the float values should probably happen earlier + Map.getSharedInstance().updateLocation(meta["source"], LatLngLocation(float(meta["lat"]), float(meta["lon"]))) + return None + + class MetaParser(object): enrichers = { - "DMR": DmrMetaEnricher() + "DMR": DmrMetaEnricher(), + "YSF": YsfMetaEnricher() } def __init__(self, handler): From f5f23e6fbca52a35a84adc0d4bff8628f5899a6d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jul 2019 21:21:26 +0200 Subject: [PATCH 0188/2616] remove debugging --- htdocs/map.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/htdocs/map.js b/htdocs/map.js index 9dfb24d37..1751089fd 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -26,10 +26,8 @@ if (update.location.type != 'latlon') return; var pos = new google.maps.LatLng(update.location.lat, update.location.lon) if (markers[update.callsign]) { - console.info("updating"); markers[update.callsign].setPosition(pos); } else { - console.info("initializing"); markers[update.callsign] = new google.maps.Marker({ position: pos, map: map, From e61c0dcc12d218f944b3fde3727f22f5e888ef27 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 5 Jul 2019 19:30:24 +0200 Subject: [PATCH 0189/2616] add some basic framework for the featurereport --- htdocs/features.html | 6 ++++++ htdocs/features.js | 5 +++++ owrx/controllers.py | 20 +++++++++++++------- owrx/feature.py | 5 +++++ owrx/http.py | 6 ++++-- 5 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 htdocs/features.html create mode 100644 htdocs/features.js diff --git a/htdocs/features.html b/htdocs/features.html new file mode 100644 index 000000000..f4ed3b446 --- /dev/null +++ b/htdocs/features.html @@ -0,0 +1,6 @@ + + OpenWebRX Feature report + + + + \ No newline at end of file diff --git a/htdocs/features.js b/htdocs/features.js new file mode 100644 index 000000000..cb96f09f6 --- /dev/null +++ b/htdocs/features.js @@ -0,0 +1,5 @@ +$(function(){ + $.ajax('/api/features').done(function(data){ + $('body').html(JSON.stringify(data)); + }); +}); \ No newline at end of file diff --git a/owrx/controllers.py b/owrx/controllers.py index 5a915fa57..c4f917a05 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -1,11 +1,13 @@ import os import mimetypes +import json from datetime import datetime from owrx.websocket import WebSocketConnection from owrx.config import PropertyManager from owrx.source import ClientRegistry from owrx.connection import WebSocketMessageHandler from owrx.version import openwebrx_version +from owrx.feature import FeatureDetector import logging logger = logging.getLogger(__name__) @@ -26,12 +28,7 @@ def send_response(self, content, code = 200, content_type = "text/html", last_mo if (type(content) == str): content = content.encode() self.handler.wfile.write(content) - def render_template(self, template, **variables): - f = open('htdocs/' + template) - data = f.read() - f.close() - self.send_response(data) class StatusController(Controller): def handle_request(self): @@ -77,12 +74,21 @@ def handle_request(self): class IndexController(AssetsController): def handle_request(self): - self.serve_file("index.html", content_type = "text/html") + self.serve_file("index.html") class MapController(AssetsController): def handle_request(self): #TODO check if we have a google maps api key first? - self.serve_file("map.html", content_type = "text/html") + self.serve_file("map.html") + +class FeatureController(AssetsController): + def handle_request(self): + self.serve_file("features.html") + +class ApiController(Controller): + def handle_request(self): + data = json.dumps(FeatureDetector().feature_report()) + self.send_response(data, content_type = "application/json") class WebSocketController(Controller): def handle_request(self): diff --git a/owrx/feature.py b/owrx/feature.py index 38b8e27ed..8a45b1079 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -26,6 +26,11 @@ class FeatureDetector(object): def feature_availability(self): return {name: self.is_available(name) for name in FeatureDetector.features} + def feature_report(self): + def feature_details(name): + return self.get_requirements(name) + return {name: feature_details(name) for name in FeatureDetector.features} + def is_available(self, feature): return self.has_requirements(self.get_requirements(feature)) diff --git a/owrx/http.py b/owrx/http.py index b449fab18..7e1f5782c 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -1,4 +1,4 @@ -from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController, MapController +from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController, MapController, FeatureController, ApiController from http.server import BaseHTTPRequestHandler import re @@ -21,7 +21,9 @@ class Router(object): {"regex": "(/favicon.ico)", "controller": AssetsController}, # backwards compatibility for the sdr.hu portal {"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController}, - {"route": "/map", "controller": MapController} + {"route": "/map", "controller": MapController}, + {"route": "/features", "controller": FeatureController}, + {"route": "/api/features", "controller": ApiController} ] def find_controller(self, path): for m in Router.mappings: From 823a4a35f0ffea95e00afa1230956050593a9840 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 5 Jul 2019 22:31:46 +0200 Subject: [PATCH 0190/2616] implement feature and requirement details --- htdocs/features.html | 11 ++++ htdocs/features.js | 21 +++++++- owrx/feature.py | 122 +++++++++++++++++++++++++++++++++---------- 3 files changed, 126 insertions(+), 28 deletions(-) diff --git a/htdocs/features.html b/htdocs/features.html index f4ed3b446..bcba73cbb 100644 --- a/htdocs/features.html +++ b/htdocs/features.html @@ -1,6 +1,17 @@ OpenWebRX Feature report + + +

    OpenWebRX Feature Report

    + + + + + + + +
    FeatureRequirementDescriptionAvailable
    \ No newline at end of file diff --git a/htdocs/features.js b/htdocs/features.js index cb96f09f6..e534bcbc1 100644 --- a/htdocs/features.js +++ b/htdocs/features.js @@ -1,5 +1,24 @@ $(function(){ + var converter = new showdown.Converter(); $.ajax('/api/features').done(function(data){ - $('body').html(JSON.stringify(data)); + $table = $('table.features'); + $.each(data, function(name, details) { + requirements = $.map(details.requirements, function(r, name){ + return '' + + '' + + '' + name + '' + + '' + converter.makeHtml(r.description) + '' + + '' + (r.available ? 'YES' : 'NO') + '' + + ''; + }); + $table.append( + '' + + '' + name + '' + + '' + converter.makeHtml(details.description) + '' + + '' + (details.available ? 'YES' : 'NO') + '' + + '' + + requirements.join("") + ); + }) }); }); \ No newline at end of file diff --git a/owrx/feature.py b/owrx/feature.py index 8a45b1079..d8bcdca0c 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -4,6 +4,7 @@ from operator import and_ import re from distutils.version import LooseVersion +import inspect import logging logger = logging.getLogger(__name__) @@ -12,6 +13,7 @@ class UnknownFeatureException(Exception): pass + class FeatureDetector(object): features = { "core": [ "csdr", "nmux", "nc" ], @@ -27,8 +29,22 @@ def feature_availability(self): return {name: self.is_available(name) for name in FeatureDetector.features} def feature_report(self): + def requirement_details(name): + available = self.has_requirement(name) + return { + "available": available, + # as of now, features are always enabled as soon as they are available. this may change in the future. + "enabled": available, + "description": self.get_requirement_description(name) + } + def feature_details(name): - return self.get_requirements(name) + return { + "description": "", + "available": self.is_available(name), + "requirements": {name: requirement_details(name) for name in self.get_requirements(name)} + } + return {name: feature_details(name) for name in FeatureDetector.features} def is_available(self, feature): @@ -43,45 +59,84 @@ def get_requirements(self, feature): def has_requirements(self, requirements): passed = True for requirement in requirements: - methodname = "has_" + requirement - if hasattr(self, methodname) and callable(getattr(self, methodname)): - passed = passed and getattr(self, methodname)() - else: - logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement)) + passed = passed and self.has_requirement(requirement) return passed + def _get_requirement_method(self, requirement): + methodname = "has_" + requirement + if hasattr(self, methodname) and callable(getattr(self, methodname)): + return getattr(self, methodname) + return None + + def has_requirement(self, requirement): + method = self._get_requirement_method(requirement) + if method is not None: + return method() + else: + logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement)) + return False + + def get_requirement_description(self, requirement): + return inspect.getdoc(self._get_requirement_method(requirement)) + def command_is_runnable(self, command): return os.system("{0} 2>/dev/null >/dev/null".format(command)) != 32512 def has_csdr(self): + """ + OpenWebRX uses the demodulator and pipeline tools provided by the csdr project. Please check out [the project + page on github](https://github.com/simonyiszk/csdr) for further details and installation instructions. + """ return self.command_is_runnable("csdr") def has_nmux(self): + """ + Nmux is another tool provided by the csdr project. It is used for internal multiplexing of the IQ data streams. + If you're missing nmux even though you have csdr installed, please update your csdr version. + """ return self.command_is_runnable("nmux --help") def has_nc(self): + """ + Nc is the client used to connect to the nmux multiplexer. It is provided by either the BSD netcat (recommended + for better performance) or GNU netcat packages. Please check your distribution package manager for options. + """ return self.command_is_runnable('nc --help') def has_rtl_sdr(self): + """ + The rtl-sdr command is required to read I/Q data from an RTL SDR USB-Stick. It is available in most + distribution package managers. + """ return self.command_is_runnable("rtl_sdr --help") def has_rx_tools(self): + """ + The rx_tools package can be used to interface with SDR devices compatible with SoapySDR. It is currently used + to connect to SDRPlay devices. Please check the following pages for more details: + + * [rx_tools GitHub page](https://github.com/rxseger/rx_tools) + * [SoapySDR Project wiki](https://github.com/pothosware/SoapySDR/wiki) + * [SDRPlay homepage](https://www.sdrplay.com/) + """ return self.command_is_runnable("rx_sdr --help") - """ - To use a HackRF, compile the HackRF host tools from its "stdout" branch: - git clone https://github.com/mossmann/hackrf/ - cd hackrf - git fetch - git checkout origin/stdout - cd host - mkdir build - cd build - cmake .. -DINSTALL_UDEV_RULES=ON - make - sudo make install - """ def has_hackrf_transfer(self): + """ + To use a HackRF, compile the HackRF host tools from its "stdout" branch: + ``` + git clone https://github.com/mossmann/hackrf/ + cd hackrf + git fetch + git checkout origin/stdout + cd host + mkdir build + cd build + cmake .. -DINSTALL_UDEV_RULES=ON + make + sudo make install + ``` + """ # TODO i don't have a hackrf, so somebody doublecheck this. # TODO also check if it has the stdout feature return self.command_is_runnable("hackrf_transfer --help") @@ -89,15 +144,15 @@ def has_hackrf_transfer(self): def command_exists(self, command): return os.system("which {0}".format(command)) == 0 - """ - To use DMR and YSF, the digiham package is required. You can find the package and installation instructions here: - https://github.com/jketterl/digiham - - Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work. - If you have an older verison of digiham installed, please update it along with openwebrx. - As of now, we require version 0.2 of digiham. - """ def has_digiham(self): + """ + To use digital voice modes, the digiham package is required. You can find the package and installation + instructions [here](https://github.com/jketterl/digiham). + + Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work. + If you have an older verison of digiham installed, please update it along with openwebrx. + As of now, we require version 0.2 of digiham. + """ required_version = LooseVersion("0.2") digiham_version_regex = re.compile('^digiham version (.*)$') @@ -118,10 +173,23 @@ def check_digiham_version(command): True) def has_dsd(self): + """ + The digital voice modes NXDN and D-Star can be decoded by the dsd project. Please note that you need the version + modified by F4EXB that provides stdin/stdout support. You can find it [here](https://github.com/f4exb/dsd). + """ return self.command_is_runnable("dsd") def has_sox(self): + """ + The sox audio library is used to convert between the typical 8 kHz audio sampling rate used by digital modes and + the audio sampling rate requested by the client. + + It is available for most distributions through the respective package manager. + """ return self.command_is_runnable("sox") def has_airspy_rx(self): + """ + In order to use an Airspy Receiver, you need to install the airspy_rx receiver software. + """ return self.command_is_runnable("airspy_rx --help 2> /dev/null") From d0d5dffe7968a9657b8916b16451a9a653b7a3f6 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 5 Jul 2019 22:46:43 +0200 Subject: [PATCH 0191/2616] add some styling --- htdocs/features.css | 4 ++++ htdocs/features.html | 21 ++++++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 htdocs/features.css diff --git a/htdocs/features.css b/htdocs/features.css new file mode 100644 index 000000000..cc821b122 --- /dev/null +++ b/htdocs/features.css @@ -0,0 +1,4 @@ +h1 { + text-align: center; + margin: 50px 0; +} \ No newline at end of file diff --git a/htdocs/features.html b/htdocs/features.html index bcba73cbb..560256793 100644 --- a/htdocs/features.html +++ b/htdocs/features.html @@ -1,17 +1,20 @@ OpenWebRX Feature report + -

    OpenWebRX Feature Report

    - - - - - - - -
    FeatureRequirementDescriptionAvailable
    +
    +

    OpenWebRX Feature Report

    + + + + + + + +
    FeatureRequirementDescriptionAvailable
    +
    \ No newline at end of file From 892c92eb1d4d463faaf9329896f83c747c969be1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 12:41:30 +0200 Subject: [PATCH 0192/2616] add a link for the map in the top bar --- htdocs/gfx/openwebrx-panel-map.png | Bin 0 -> 3218 bytes htdocs/index.html | 1 + htdocs/openwebrx.css | 5 +++++ 3 files changed, 6 insertions(+) create mode 100644 htdocs/gfx/openwebrx-panel-map.png diff --git a/htdocs/gfx/openwebrx-panel-map.png b/htdocs/gfx/openwebrx-panel-map.png new file mode 100644 index 0000000000000000000000000000000000000000..36cb90e1fe39c49667abaf735aa749995cba5f11 GIT binary patch literal 3218 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
    Status

  • Log

  • Receiver
  • +

  • Map
  • diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 5624d7978..9c6db0e69 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -656,6 +656,11 @@ img.openwebrx-mirror-img cursor:pointer; } +#openwebrx-main-buttons a { + color: inherit; + text-decoration: inherit; +} + #openwebrx-main-buttons li:hover { background-color: rgba(255, 255, 255, 0.3); From 31b8dd4fd59223f5b0ab788923643b866a24ca58 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 12:53:11 +0200 Subject: [PATCH 0193/2616] send ysf pins to the map --- htdocs/openwebrx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 529754914..814144dae 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1352,7 +1352,7 @@ function update_metadata(meta) { mode = "Mode: " + meta.mode; source = meta.source || ""; if (meta.lat && meta.lon) { - source = "" + source; + source = "" + source; } up = meta.up ? "Up: " + meta.up : ""; down = meta.down ? "Down: " + meta.down : ""; From 089964a5ebdcb82f5adee26d15c7a307890c345f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 13:03:49 +0200 Subject: [PATCH 0194/2616] query parameter support for the http module --- owrx/controllers.py | 6 +++--- owrx/http.py | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/owrx/controllers.py b/owrx/controllers.py index c4f917a05..f979891d5 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -13,9 +13,9 @@ logger = logging.getLogger(__name__) class Controller(object): - def __init__(self, handler, matches): + def __init__(self, handler, request): self.handler = handler - self.matches = matches + self.request = request def send_response(self, content, code = 200, content_type = "text/html", last_modified: datetime = None, max_age = None): self.handler.send_response(code) if content_type is not None: @@ -69,7 +69,7 @@ def serve_file(self, file, content_type = None): except FileNotFoundError: self.send_response("file not found", code = 404) def handle_request(self): - filename = self.matches.group(1) + filename = self.request.matches.group(1) self.serve_file(filename) class IndexController(AssetsController): diff --git a/owrx/http.py b/owrx/http.py index 7e1f5782c..ce821b9b5 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -1,6 +1,7 @@ from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController, MapController, FeatureController, ApiController from http.server import BaseHTTPRequestHandler import re +from urllib.parse import urlparse, parse_qs import logging logger = logging.getLogger(__name__) @@ -12,6 +13,11 @@ def __init__(self, request, client_address, server): def do_GET(self): self.router.route(self) +class Request(object): + def __init__(self, query = None, matches = None): + self.query = query + self.matches = matches + class Router(object): mappings = [ {"route": "/", "controller": IndexController}, @@ -36,10 +42,13 @@ def find_controller(self, path): if matches: return (m["controller"], matches) def route(self, handler): - res = self.find_controller(handler.path) + url = urlparse(handler.path) + res = self.find_controller(url.path) if res is not None: (controller, matches) = res - logger.debug("path: {0}, controller: {1}, matches: {2}".format(handler.path, controller, matches)) - controller(handler, matches).handle_request() + query = parse_qs(url.query) + logger.debug("path: {0}, controller: {1}, query: {2}, matches: {3}".format(handler.path, controller, query, matches)) + request = Request(query, matches) + controller(handler, request).handle_request() else: handler.send_error(404, "Not Found", "The page you requested could not be found.") From 3f05565b7b70f14db42b49fbe7c874ab6f104d0f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 15:04:39 +0200 Subject: [PATCH 0195/2616] show selected callsign on the map --- htdocs/map.js | 18 ++++++++++++++++++ htdocs/openwebrx.js | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/htdocs/map.js b/htdocs/map.js index 1751089fd..902c554bb 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -4,6 +4,18 @@ protocol = 'wss'; } + var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){ + var s = v.split('='); + r = {} + r[s[0]] = s.slice(1).join('=') + return r; + }).reduce(function(a, b){ + return a.assign(b); + }); + + var expectedCallsign; + if (query.callsign) expectedCallsign = query.callsign; + var ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; if (!("WebSocket" in window)) return; @@ -34,6 +46,12 @@ title: update.callsign }); } + + // TODO the trim should happen on the server side + if (expectedCallsign && expectedCallsign == update.callsign.trim()) { + map.panTo(pos); + delete(expectedCallsign); + } }); } diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 814144dae..399a4357c 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1351,8 +1351,8 @@ function update_metadata(meta) { if (meta.mode && meta.mode != "") { mode = "Mode: " + meta.mode; source = meta.source || ""; - if (meta.lat && meta.lon) { - source = "" + source; + if (meta.lat && meta.lon && meta.source) { + source = "" + source; } up = meta.up ? "Up: " + meta.up : ""; down = meta.down ? "Down: " + meta.down : ""; From 284646ee6cfb3a4b3aed998c6d5274f90d086942 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 18:21:43 +0200 Subject: [PATCH 0196/2616] first stab at ft8 decoding: chop up audio, call jt9 binary to decode --- csdr.py | 21 +++++++++--- htdocs/index.html | 1 + htdocs/openwebrx.js | 4 +++ owrx/wsjt.py | 79 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 owrx/wsjt.py diff --git a/csdr.py b/csdr.py index 31ee80a62..066d2de1b 100755 --- a/csdr.py +++ b/csdr.py @@ -21,11 +21,11 @@ """ import subprocess -import time import os import signal import threading from functools import partial +from owrx.wsjt import Ft8Chopper import logging logger = logging.getLogger(__name__) @@ -186,6 +186,12 @@ def secondary_chain(self, which): "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" + elif which == "ft8": + chain = secondary_chain_base + "csdr realpart_cf | " + if self.last_decimation != 1.0 : + chain += "csdr fractional_decimator_ff {last_decimation} | " + chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" + return chain def set_secondary_demodulator(self, what): if self.get_secondary_demodulator() == what: @@ -238,7 +244,8 @@ def start_secondary_demodulator(self): secondary_samples_per_bits=self.secondary_samples_per_bits(), secondary_bpf_cutoff=self.secondary_bpf_cutoff(), secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), - if_samp_rate=self.if_samp_rate() + if_samp_rate=self.if_samp_rate(), + last_decimation=self.last_decimation ) logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft) @@ -253,7 +260,11 @@ def start_secondary_demodulator(self): self.secondary_processes_running = True self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read()))) - self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) + if self.get_secondary_demodulator() == "ft8": + chopper = Ft8Chopper(self.secondary_process_demod.stdout) + chopper.start() + else: + self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) #open control pipes for csdr and send initialization data if self.secondary_shift_pipe != None: #TODO digimodes @@ -262,7 +273,7 @@ def start_secondary_demodulator(self): def set_secondary_offset_freq(self, value): self.secondary_offset_freq=value - if self.secondary_processes_running: + if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"): self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate())) self.secondary_shift_pipe_file.flush() @@ -332,6 +343,8 @@ def get_output_rate(self): def get_audio_rate(self): if self.isDigitalVoice(): return 48000 + elif self.secondary_demodulator == "ft8": + return 12000 return self.get_output_rate() def isDigitalVoice(self, demodulator = None): diff --git a/htdocs/index.html b/htdocs/index.html index 07b219e30..5373f498e 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -109,6 +109,7 @@
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 399a4357c..280b38037 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -2633,6 +2633,7 @@ function demodulator_digital_replace(subtype) { case "bpsk31": case "rtty": + case "ft8": secondary_demod_start(subtype); demodulator_analog_replace('usb', true); demodulator_buttons_update(); @@ -2809,6 +2810,9 @@ function secondary_demod_listbox_changed() case "rtty": demodulator_digital_replace('rtty'); break; + case "ft8": + demodulator_digital_replace('ft8'); + break; } } diff --git a/owrx/wsjt.py b/owrx/wsjt.py new file mode 100644 index 000000000..c9fc3196b --- /dev/null +++ b/owrx/wsjt.py @@ -0,0 +1,79 @@ +import threading +import wave +from datetime import datetime, timedelta +import time +import sched +import subprocess + +import logging +logger = logging.getLogger(__name__) + + +class Ft8Chopper(threading.Thread): + def __init__(self, source): + self.source = source + (self.wavefilename, self.wavefile) = self.getWaveFile() + self.scheduler = sched.scheduler(time.time, time.sleep) + self.queue = [] + self.doRun = True + super().__init__() + + def getWaveFile(self): + filename = "/tmp/openwebrx-ft8chopper-{0}.wav".format(datetime.now().strftime("%Y%m%d-%H%M%S")) + wavefile = wave.open(filename, "wb") + wavefile.setnchannels(1) + wavefile.setsampwidth(2) + wavefile.setframerate(12000) + return (filename, wavefile) + + def getNextDecodingTime(self): + t = datetime.now() + seconds = (int(t.second / 15) + 1) * 15 + if seconds >= 60: + t = t + timedelta(minutes = 1) + seconds = 0 + t = t.replace(second = seconds, microsecond = 0) + logger.debug("scheduling: {0}".format(t)) + return t.timestamp() + + def startScheduler(self): + self._scheduleNextSwitch() + threading.Thread(target = self.scheduler.run).start() + + def emptyScheduler(self): + for event in self.scheduler.queue: + self.scheduler.cancel(event) + + def _scheduleNextSwitch(self): + self.scheduler.enterabs(self.getNextDecodingTime(), 1, self.switchFiles) + + def switchFiles(self): + file = self.wavefile + filename = self.wavefilename + (self.wavefilename, self.wavefile) = self.getWaveFile() + + file.close() + self.queue.append(filename) + self._scheduleNextSwitch() + + def decode(self): + if self.queue: + file = self.queue.pop() + logger.debug("processing file {0}".format(file)) + #TODO expose decoding quality parameters through config + self.decoder = subprocess.Popen(["jt9", "--ft8", "-d", "3", file]) + + def run(self) -> None: + logger.debug("FT8 chopper starting up") + self.startScheduler() + while self.doRun: + data = self.source.read(256) + if data is None or (isinstance(data, bytes) and len(data) == 0): + logger.warning("zero read on ft8 chopper") + self.doRun = False + else: + self.wavefile.writeframes(data) + + self.decode() + logger.debug("FT8 chopper shutting down") + self.emptyScheduler() From fa2d82ac130d9cc952aeda354195b40d5ce5dee5 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 20:03:17 +0200 Subject: [PATCH 0197/2616] ft8 message parsing --- csdr.py | 1 + owrx/connection.py | 3 +++ owrx/wsjt.py | 64 +++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/csdr.py b/csdr.py index 066d2de1b..c9d17c76d 100755 --- a/csdr.py +++ b/csdr.py @@ -263,6 +263,7 @@ def start_secondary_demodulator(self): if self.get_secondary_demodulator() == "ft8": chopper = Ft8Chopper(self.secondary_process_demod.stdout) chopper.start() + self.output.add_output("wsjt_demod", chopper.read) else: self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) diff --git a/owrx/connection.py b/owrx/connection.py index 328697545..a782cfc50 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -159,6 +159,9 @@ def write_features(self, features): def write_metadata(self, metadata): self.protected_send({"type":"metadata","value":metadata}) + def write_wsjt_message(self, message): + self.protected_send({"type": "wsjt_message", "value": message}) + class MapConnection(Client): def __init__(self, conn): diff --git a/owrx/wsjt.py b/owrx/wsjt.py index c9fc3196b..cc8a7f80b 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -1,9 +1,11 @@ import threading import wave -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date import time import sched import subprocess +import os +from multiprocessing.connection import Pipe import logging logger = logging.getLogger(__name__) @@ -14,7 +16,8 @@ def __init__(self, source): self.source = source (self.wavefilename, self.wavefile) = self.getWaveFile() self.scheduler = sched.scheduler(time.time, time.sleep) - self.queue = [] + self.fileQueue = [] + (self.outputReader, self.outputWriter) = Pipe() self.doRun = True super().__init__() @@ -53,15 +56,28 @@ def switchFiles(self): (self.wavefilename, self.wavefile) = self.getWaveFile() file.close() - self.queue.append(filename) + self.fileQueue.append(filename) self._scheduleNextSwitch() def decode(self): - if self.queue: - file = self.queue.pop() - logger.debug("processing file {0}".format(file)) + def decode_and_unlink(file): #TODO expose decoding quality parameters through config - self.decoder = subprocess.Popen(["jt9", "--ft8", "-d", "3", file]) + decoder = subprocess.Popen(["jt9", "--ft8", "-d", "3", file], stdout=subprocess.PIPE) + while True: + line = decoder.stdout.readline() + if line is None or (isinstance(line, bytes) and len(line) == 0): + break + self.outputWriter.send(line) + rc = decoder.wait() + logger.debug("decoder return code: %i", rc) + os.unlink(file) + + self.decoder = decoder + + if self.fileQueue: + file = self.fileQueue.pop() + logger.debug("processing file {0}".format(file)) + threading.Thread(target=decode_and_unlink, args=[file]).start() def run(self) -> None: logger.debug("FT8 chopper starting up") @@ -76,4 +92,38 @@ def run(self) -> None: self.decode() logger.debug("FT8 chopper shutting down") + self.outputReader.close() + self.outputWriter.close() self.emptyScheduler() + + def read(self): + try: + return self.outputReader.recv() + except EOFError: + return None + + +class WsjtParser(object): + def __init__(self, handler): + self.handler = handler + + def parse(self, data): + try: + msg = data.decode().rstrip() + # known debug messages we know to skip + if msg.startswith(""): + return + if msg.startswith(" EOF on input file"): + return + + out = {} + time = datetime.strptime(msg[0:6], "%H%M%S") + out["timestamp"] = datetime.combine(date.today(), time.time()).timestamp() + out["db"] = float(msg[7:10]) + out["dt"] = float(msg[11:15]) + out["freq"] = int(msg[16:20]) + out["msg"] = msg[24:] + + self.handler.write_wsjt_message(out) + except ValueError: + logger.exception("error while parsing wsjt message") From d8a7dfbdbd68401f4cc9dc2ffaba2e0591016dcf Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 21:04:18 +0200 Subject: [PATCH 0198/2616] ft8 messages panel --- htdocs/index.html | 12 +++++++++++- htdocs/openwebrx.css | 27 +++++++++++++++++++++++++++ htdocs/openwebrx.js | 16 ++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/htdocs/index.html b/htdocs/index.html index 5373f498e..3e8ca708c 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -159,7 +159,7 @@ Under construction
    We're working on the code right now, so the application might fail.
    -
    +
    @@ -170,6 +170,16 @@
    + + + + + + + + + +
    UTCdBDTFreqMessage
    diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 9c6db0e69..f9135aeb4 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -1018,3 +1018,30 @@ img.openwebrx-mirror-img background-size: contain; display: inline-block; } + +#openwebrx-panel-wsjt-message { + height: 180px; +} + +#openwebrx-panel-wsjt-message tbody { + display: block; + overflow: auto; + height: 150px; + width: 100%; +} + +#openwebrx-panel-wsjt-message thead tr { + display: block; +} + +#openwebrx-panel-wsjt-message th, +#openwebrx-panel-wsjt-message td { + width: 50px; + text-align: left; +} + +#openwebrx-panel-wsjt-message .message { + width: 400px; +} + + diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 280b38037..66a004532 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1245,6 +1245,22 @@ function on_ws_recv(evt) case "metadata": update_metadata(json.value); break; + case "wsjt_message": + var msg = json.value; + var $b = $('#openwebrx-panel-wsjt-message tbody'); + var t = new Date(msg['timestamp'] * 1000); + var pad = function(i) { return ('' + i).padStart(2, "0"); } + $b.append($( + '' + + '' + pad(t.getHours()) + pad(t.getMinutes()) + pad(t.getSeconds()) + '' + + '' + msg['db'] + '' + + '' + msg['dt'] + '' + + '' + msg['freq'] + '' + + '' + msg['msg'] + '' + + '' + )); + $b.scrollTop($b[0].scrollHeight); + break; default: console.warn('received message of unknown type: ' + json.type); } From eb1b1ba22fc937605fa69c7e62b8aa53ee4d43e0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 21:26:35 +0200 Subject: [PATCH 0199/2616] fix utc timestamps --- htdocs/openwebrx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 66a004532..5103c85f7 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1252,7 +1252,7 @@ function on_ws_recv(evt) var pad = function(i) { return ('' + i).padStart(2, "0"); } $b.append($( '' + - '' + pad(t.getHours()) + pad(t.getMinutes()) + pad(t.getSeconds()) + '' + + '' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '' + '' + msg['db'] + '' + '' + msg['dt'] + '' + '' + msg['freq'] + '' + From a6d7209a4537030a5e0b423dbbbf860b20ab78a3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 21:29:49 +0200 Subject: [PATCH 0200/2616] explicit timezone information --- owrx/wsjt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index cc8a7f80b..5197f7d09 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -117,8 +117,8 @@ def parse(self, data): return out = {} - time = datetime.strptime(msg[0:6], "%H%M%S") - out["timestamp"] = datetime.combine(date.today(), time.time()).timestamp() + ts = datetime.strptime(msg[0:6], "%H%M%S") + out["timestamp"] = datetime.combine(date.today(), ts.time(), datetime.now().tzinfo).timestamp() out["db"] = float(msg[7:10]) out["dt"] = float(msg[11:15]) out["freq"] = int(msg[16:20]) From 48baea33046c8d4599d8a39747b24ee345b6f490 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 22:21:47 +0200 Subject: [PATCH 0201/2616] parse locators and send to map --- owrx/wsjt.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 5197f7d09..0081aed55 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -6,6 +6,8 @@ import subprocess import os from multiprocessing.connection import Pipe +from owrx.map import Map, LocatorLocation +import re import logging logger = logging.getLogger(__name__) @@ -106,6 +108,7 @@ def read(self): class WsjtParser(object): def __init__(self, handler): self.handler = handler + self.locator_pattern = re.compile(".*\s([A-Z0-9]+)\s([A-R]{2}[0-9]{2})$") def parse(self, data): try: @@ -122,8 +125,20 @@ def parse(self, data): out["db"] = float(msg[7:10]) out["dt"] = float(msg[11:15]) out["freq"] = int(msg[16:20]) - out["msg"] = msg[24:] + wsjt_msg = msg[24:] + self.getLocator(wsjt_msg) + out["msg"] = wsjt_msg self.handler.write_wsjt_message(out) except ValueError: logger.exception("error while parsing wsjt message") + + def getLocator(self, msg): + m = self.locator_pattern.match(msg) + if m is None: + return + # this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very + # likely this just means roger roger goodbye. + if m.group(2) == "RR73": + return + Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2))) From 25bc78859521e778220945e2845a5f13ea83fd91 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 22:43:36 +0200 Subject: [PATCH 0202/2616] parse and show locators on the map --- htdocs/map.js | 62 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/htdocs/map.js b/htdocs/map.js index 902c554bb..f5f9a71f3 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -26,6 +26,7 @@ var map; var markers = {}; + var rectangles = {}; var updateQueue = []; var processUpdates = function(updates) { @@ -34,23 +35,52 @@ return; } updates.forEach(function(update){ - // TODO maidenhead locator implementation - if (update.location.type != 'latlon') return; - var pos = new google.maps.LatLng(update.location.lat, update.location.lon) - if (markers[update.callsign]) { - markers[update.callsign].setPosition(pos); - } else { - markers[update.callsign] = new google.maps.Marker({ - position: pos, - map: map, - title: update.callsign - }); - } - // TODO the trim should happen on the server side - if (expectedCallsign && expectedCallsign == update.callsign.trim()) { - map.panTo(pos); - delete(expectedCallsign); + switch (update.location.type) { + case 'latlon': + var pos = new google.maps.LatLng(update.location.lat, update.location.lon) + if (markers[update.callsign]) { + markers[update.callsign].setPosition(pos); + } else { + markers[update.callsign] = new google.maps.Marker({ + position: pos, + map: map, + title: update.callsign + }); + } + + // TODO the trim should happen on the server side + if (expectedCallsign && expectedCallsign == update.callsign.trim()) { + map.panTo(pos); + delete(expectedCallsign); + } + break; + case 'locator': + var loc = update.location.locator; + var lat = (loc.charCodeAt(1) - 65 - 9) * 10 + Number(loc[3]); + var lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]); + var rectangle; + if (rectangles[update.callsign]) { + rectangle = rectangles[update.callsign]; + } else { + rectangle = new google.maps.Rectangle(); + rectangles[update.callsign] = rectangle; + } + rectangle.setOptions({ + strokeColor: '#FF0000', + strokeOpacity: 0.8, + strokeWeight: 2, + fillColor: '#FF0000', + fillOpacity: 0.35, + map: map, + bounds:{ + north: lat, + south: lat + 1, + west: lon, + east: lon + 1 + } + }); + break; } }); } From 849337c55de5e6f038d1d0daafaafa882f3097c8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 23:15:33 +0200 Subject: [PATCH 0203/2616] fix locator calculation --- htdocs/map.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/map.js b/htdocs/map.js index f5f9a71f3..e26894670 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -58,7 +58,7 @@ case 'locator': var loc = update.location.locator; var lat = (loc.charCodeAt(1) - 65 - 9) * 10 + Number(loc[3]); - var lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]); + var lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]) * 2; var rectangle; if (rectangles[update.callsign]) { rectangle = rectangles[update.callsign]; @@ -77,7 +77,7 @@ north: lat, south: lat + 1, west: lon, - east: lon + 1 + east: lon + 2 } }); break; From c22d10d0de4edb814bab7a724d6a1ac370a51b9e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 7 Jul 2019 00:52:11 +0200 Subject: [PATCH 0204/2616] add day/night overlay --- htdocs/map.js | 4 ++ htdocs/nite-overlay.js | 143 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 htdocs/nite-overlay.js diff --git a/htdocs/map.js b/htdocs/map.js index e26894670..a94629191 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -108,6 +108,10 @@ zoom: 8 }); processUpdates(updateQueue); + $.getScript("/static/nite-overlay.js").done(function(){ + nite.init(map); + setInterval(function() { nite.refresh() }, 10000); // every 10s + }); }) break case "update": diff --git a/htdocs/nite-overlay.js b/htdocs/nite-overlay.js new file mode 100644 index 000000000..4a8bdd2cf --- /dev/null +++ b/htdocs/nite-overlay.js @@ -0,0 +1,143 @@ +/* Nite v1.7 + * A tiny library to create a night overlay over the map + * Author: Rossen Georgiev @ https://github.com/rossengeorgiev + * Requires: GMaps API 3 + */ + + +var nite = { + map: null, + date: null, + sun_position: null, + earth_radius_meters: 6371008, + marker_twilight_civil: null, + marker_twilight_nautical: null, + marker_twilight_astronomical: null, + marker_night: null, + + init: function(map) { + if(typeof google === 'undefined' + || typeof google.maps === 'undefined') throw "Nite Overlay: no google.maps detected"; + + this.map = map; + this.sun_position = this.calculatePositionOfSun(); + + this.marker_twilight_civil = new google.maps.Circle({ + map: this.map, + center: this.getShadowPosition(), + radius: this.getShadowRadiusFromAngle(0.566666), + fillColor: "#000", + fillOpacity: 0.1, + strokeOpacity: 0, + clickable: false, + editable: false + }); + this.marker_twilight_nautical = new google.maps.Circle({ + map: this.map, + center: this.getShadowPosition(), + radius: this.getShadowRadiusFromAngle(6), + fillColor: "#000", + fillOpacity: 0.1, + strokeOpacity: 0, + clickable: false, + editable: false + }); + this.marker_twilight_astronomical = new google.maps.Circle({ + map: this.map, + center: this.getShadowPosition(), + radius: this.getShadowRadiusFromAngle(12), + fillColor: "#000", + fillOpacity: 0.1, + strokeOpacity: 0, + clickable: false, + editable: false + }); + this.marker_night = new google.maps.Circle({ + map: this.map, + center: this.getShadowPosition(), + radius: this.getShadowRadiusFromAngle(18), + fillColor: "#000", + fillOpacity: 0.1, + strokeOpacity: 0, + clickable: false, + editable: false + }); + }, + getShadowRadiusFromAngle: function(angle) { + var shadow_radius = this.earth_radius_meters * Math.PI * 0.5; + var twilight_dist = ((this.earth_radius_meters * 2 * Math.PI) / 360) * angle; + return shadow_radius - twilight_dist; + }, + getSunPosition: function() { + return this.sun_position; + }, + getShadowPosition: function() { + return (this.sun_position) ? new google.maps.LatLng(-this.sun_position.lat(), this.sun_position.lng() + 180) : null; + }, + refresh: function() { + if(!this.isVisible()) return; + this.sun_position = this.calculatePositionOfSun(this.date); + var shadow_position = this.getShadowPosition(); + this.marker_twilight_civil.setCenter(shadow_position); + this.marker_twilight_nautical.setCenter(shadow_position); + this.marker_twilight_astronomical.setCenter(shadow_position); + this.marker_night.setCenter(shadow_position); + }, + jday: function(date) { + return (date.getTime() / 86400000.0) + 2440587.5; + }, + calculatePositionOfSun: function(date) { + date = (date instanceof Date) ? date : new Date(); + + var rad = 0.017453292519943295; + + // based on NOAA solar calculations + var ms_past_midnight = ((date.getUTCHours() * 60 + date.getUTCMinutes()) * 60 + date.getUTCSeconds()) * 1000 + date.getUTCMilliseconds(); + var jc = (this.jday(date) - 2451545)/36525; + var mean_long_sun = (280.46646+jc*(36000.76983+jc*0.0003032)) % 360; + var mean_anom_sun = 357.52911+jc*(35999.05029-0.0001537*jc); + var sun_eq = Math.sin(rad*mean_anom_sun)*(1.914602-jc*(0.004817+0.000014*jc))+Math.sin(rad*2*mean_anom_sun)*(0.019993-0.000101*jc)+Math.sin(rad*3*mean_anom_sun)*0.000289; + var sun_true_long = mean_long_sun + sun_eq; + var sun_app_long = sun_true_long - 0.00569 - 0.00478*Math.sin(rad*125.04-1934.136*jc); + var mean_obliq_ecliptic = 23+(26+((21.448-jc*(46.815+jc*(0.00059-jc*0.001813))))/60)/60; + var obliq_corr = mean_obliq_ecliptic + 0.00256*Math.cos(rad*125.04-1934.136*jc); + + var lat = Math.asin(Math.sin(rad*obliq_corr)*Math.sin(rad*sun_app_long)) / rad; + + var eccent = 0.016708634-jc*(0.000042037+0.0000001267*jc); + var y = Math.tan(rad*(obliq_corr/2))*Math.tan(rad*(obliq_corr/2)); + var rq_of_time = 4*((y*Math.sin(2*rad*mean_long_sun)-2*eccent*Math.sin(rad*mean_anom_sun)+4*eccent*y*Math.sin(rad*mean_anom_sun)*Math.cos(2*rad*mean_long_sun)-0.5*y*y*Math.sin(4*rad*mean_long_sun)-1.25*eccent*eccent*Math.sin(2*rad*mean_anom_sun))/rad); + var true_solar_time_in_deg = ((ms_past_midnight+rq_of_time*60000) % 86400000) / 240000; + + var lng = -((true_solar_time_in_deg < 0) ? true_solar_time_in_deg + 180 : true_solar_time_in_deg - 180); + + return new google.maps.LatLng(lat, lng); + }, + setDate: function(date) { + this.date = date; + this.refresh(); + }, + setMap: function(map) { + this.map = map; + this.marker_twilight_civil.setMap(this.map); + this.marker_twilight_nautical.setMap(this.map); + this.marker_twilight_astronomical.setMap(this.map); + this.marker_night.setMap(this.map); + }, + show: function() { + this.marker_twilight_civil.setVisible(true); + this.marker_twilight_nautical.setVisible(true); + this.marker_twilight_astronomical.setVisible(true); + this.marker_night.setVisible(true); + this.refresh(); + }, + hide: function() { + this.marker_twilight_civil.setVisible(false); + this.marker_twilight_nautical.setVisible(false); + this.marker_twilight_astronomical.setVisible(false); + this.marker_night.setVisible(false); + }, + isVisible: function() { + return this.marker_night.getVisible(); + } +} From ceea2475a11d351e53553702528cbcb1f5bf65a3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 7 Jul 2019 00:52:28 +0200 Subject: [PATCH 0205/2616] get rid of the extra flags at the end --- owrx/wsjt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 0081aed55..b16b5e60a 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -125,7 +125,7 @@ def parse(self, data): out["db"] = float(msg[7:10]) out["dt"] = float(msg[11:15]) out["freq"] = int(msg[16:20]) - wsjt_msg = msg[24:] + wsjt_msg = msg[24:61].strip() self.getLocator(wsjt_msg) out["msg"] = wsjt_msg From af315e1671571b54fe95be2976eb8333be0ebd3d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 7 Jul 2019 01:22:45 +0200 Subject: [PATCH 0206/2616] let's zoom out a little, seems appropriate for now --- htdocs/map.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/map.js b/htdocs/map.js index a94629191..7435f393e 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -105,7 +105,7 @@ lat: config.receiver_gps[0], lng: config.receiver_gps[1] }, - zoom: 8 + zoom: 5 }); processUpdates(updateQueue); $.getScript("/static/nite-overlay.js").done(function(){ From 182a8af57f9864f3a43211d7e490a9d9970b03c9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 7 Jul 2019 14:09:24 +0200 Subject: [PATCH 0207/2616] deliver better timestamps --- htdocs/openwebrx.js | 2 +- owrx/wsjt.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 5103c85f7..2caf7e1e7 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1248,7 +1248,7 @@ function on_ws_recv(evt) case "wsjt_message": var msg = json.value; var $b = $('#openwebrx-panel-wsjt-message tbody'); - var t = new Date(msg['timestamp'] * 1000); + var t = new Date(msg['timestamp']); var pad = function(i) { return ('' + i).padStart(2, "0"); } $b.append($( '' + diff --git a/owrx/wsjt.py b/owrx/wsjt.py index b16b5e60a..8c5a7af27 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -121,7 +121,7 @@ def parse(self, data): out = {} ts = datetime.strptime(msg[0:6], "%H%M%S") - out["timestamp"] = datetime.combine(date.today(), ts.time(), datetime.now().tzinfo).timestamp() + out["timestamp"] = int(datetime.combine(date.today(), ts.time(), datetime.now().tzinfo).timestamp() * 1000) out["db"] = float(msg[7:10]) out["dt"] = float(msg[11:15]) out["freq"] = int(msg[16:20]) From 1a257064f7ffea581b25e801f3b1210de5695a5a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 7 Jul 2019 14:10:03 +0200 Subject: [PATCH 0208/2616] add missing parser integration --- owrx/source.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/owrx/source.py b/owrx/source.py index 7d6238878..3f1b2accc 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -2,6 +2,7 @@ from owrx.config import PropertyManager from owrx.feature import FeatureDetector, UnknownFeatureException from owrx.meta import MetaParser +from owrx.wsjt import WsjtParser import threading import csdr import time @@ -346,6 +347,7 @@ def __init__(self, handler, sdrSource): self.handler = handler self.sdrSource = sdrSource self.metaParser = MetaParser(self.handler) + self.wsjtParser = WsjtParser(self.handler) self.localProps = self.sdrSource.getProps().collect( "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", @@ -417,7 +419,8 @@ def add_output(self, t, read_fn): "smeter": self.handler.write_s_meter_level, "secondary_fft": self.handler.write_secondary_fft, "secondary_demod": self.handler.write_secondary_demod, - "meta": self.metaParser.parse + "meta": self.metaParser.parse, + "wsjt_demod": self.wsjtParser.parse } write = writers[t] From d0cecbdfd78e1a5cc0ba9424d21942f081b366a2 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 7 Jul 2019 14:31:12 +0200 Subject: [PATCH 0209/2616] implement removal of old messages in the gui --- htdocs/openwebrx.js | 46 +++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 2caf7e1e7..7a1619c44 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1246,20 +1246,7 @@ function on_ws_recv(evt) update_metadata(json.value); break; case "wsjt_message": - var msg = json.value; - var $b = $('#openwebrx-panel-wsjt-message tbody'); - var t = new Date(msg['timestamp']); - var pad = function(i) { return ('' + i).padStart(2, "0"); } - $b.append($( - '' + - '' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '' + - '' + msg['db'] + '' + - '' + msg['dt'] + '' + - '' + msg['freq'] + '' + - '' + msg['msg'] + '' + - '' - )); - $b.scrollTop($b[0].scrollHeight); + update_wsjt_panel(json.value); break; default: console.warn('received message of unknown type: ' + json.type); @@ -1388,6 +1375,36 @@ function update_metadata(meta) { } +function update_wsjt_panel(msg) { + var $b = $('#openwebrx-panel-wsjt-message tbody'); + var t = new Date(msg['timestamp']); + var pad = function(i) { return ('' + i).padStart(2, "0"); } + $b.append($( + '' + + '' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '' + + '' + msg['db'] + '' + + '' + msg['dt'] + '' + + '' + msg['freq'] + '' + + '' + msg['msg'] + '' + + '' + )); + $b.scrollTop($b[0].scrollHeight); +} + +var wsjt_removal_interval; + +// remove old wsjt messages in fixed intervals +function init_wsjt_removal_timer() { + if (wsjt_removal_interval) clearInterval(wsjt_removal_interval); + setInterval(function(){ + // let's keep 2 hours that should be plenty for most users + var cutoff = new Date().getTime()- 2 * 60 * 60 * 1000; + $('#openwebrx-panel-wsjt-message tbody tr').filter(function(_, e){ + return $(e).data('timestamp') < cutoff; + }).remove(); + }, 15000); +} + function hide_digitalvoice_panels() { $(".openwebrx-meta-panel").each(function(_, p){ toggle_panel(p.id, false); @@ -2717,6 +2734,7 @@ function secondary_demod_init() .mousedown(secondary_demod_canvas_container_mousedown) .mouseenter(secondary_demod_canvas_container_mousein) .mouseleave(secondary_demod_canvas_container_mouseout); + init_wsjt_removal_timer(); } function secondary_demod_start(subtype) From d1f46c8f55836ae987e0fecc408446bbc0927786 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 7 Jul 2019 15:52:24 +0200 Subject: [PATCH 0210/2616] server-side removal of map positions --- config_webrx.py | 7 +++++++ owrx/map.py | 49 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index 0613278b6..12f9c1479 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -230,3 +230,10 @@ csdr_through = False # Setting this True will print out how much data is going into the DSP chains. nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux. + +google_maps_api_key = "" + +# how long should positions be visible on the map? +# they will start fading out after half of that +# in seconds; default: 2 hours +map_position_retention_time = 2 * 60 * 60 \ No newline at end of file diff --git a/owrx/map.py b/owrx/map.py index a799c92fc..2819a3ee3 100644 --- a/owrx/map.py +++ b/owrx/map.py @@ -1,4 +1,9 @@ -from datetime import datetime +from datetime import datetime, timedelta +import threading, time +from owrx.config import PropertyManager + +import logging +logger = logging.getLogger(__name__) class Location(object): @@ -17,6 +22,16 @@ def getSharedInstance(): def __init__(self): self.clients = [] self.positions = {} + + def removeLoop(): + while True: + try: + self.removeOldPositions() + except Exception: + logger.exception("error while removing old map positions") + time.sleep(60) + + threading.Thread(target=removeLoop, daemon=True).start() super().__init__() def broadcast(self, update): @@ -25,7 +40,14 @@ def broadcast(self, update): def addClient(self, client): self.clients.append(client) - client.write_update([{"callsign": callsign, "location": record["loc"].__dict__()} for (callsign, record) in self.positions.items()]) + client.write_update([ + { + "callsign": callsign, + "location": record["location"].__dict__(), + "lastseen": record["updated"].timestamp() * 1000 + } + for (callsign, record) in self.positions.items() + ]) def removeClient(self, client): try: @@ -34,9 +56,28 @@ def removeClient(self, client): pass def updateLocation(self, callsign, loc: Location): - self.positions[callsign] = {"loc": loc, "updated": datetime.now()} - self.broadcast([{"callsign": callsign, "location": loc.__dict__()}]) + ts = datetime.now() + self.positions[callsign] = {"location": loc, "updated": ts} + self.broadcast([ + { + "callsign": callsign, + "location": loc.__dict__(), + "lastseen": ts.timestamp() * 1000 + } + ]) + + def removeLocation(self, callsign): + self.positions.pop(callsign, None) + # TODO broadcast removal to clients + + def removeOldPositions(self): + pm = PropertyManager.getSharedInstance() + retention = timedelta(seconds=pm["map_position_retention_time"]) + cutoff = datetime.now() - retention + to_be_removed = [callsign for (callsign, pos) in self.positions.items() if pos["updated"] < cutoff] + for callsign in to_be_removed: + self.removeLocation(callsign) class LatLngLocation(Location): def __init__(self, lat: float, lon: float): From 8b5dc8b3ad2ef7ab1ea3309ed7f16f730d2f1e48 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 7 Jul 2019 20:46:12 +0200 Subject: [PATCH 0211/2616] fade out markers on the map over time --- htdocs/gfx/openwebrx-top-photo.jpg | Bin 127786 -> 70244 bytes htdocs/map.js | 78 +++++++++++++++++++++++++---- owrx/connection.py | 2 +- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/htdocs/gfx/openwebrx-top-photo.jpg b/htdocs/gfx/openwebrx-top-photo.jpg index cf521c75251ca17ab939a00643212f61358c662d..afc8e7e02166621e9f91bf56faa542584cc72db5 100644 GIT binary patch literal 70244 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 literal 127786 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 bj3C80$+^hSuPWmrA+u(Yd$JCpVs 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%0D?Fj}AxQ(pz80c|9I#45>jf|#DElLQ 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 l=&-tnE52p1?}}Jfmqrj-rV5LnI3@`c<`R= retention_time / 2) { + scale = (retention_time - age) / (retention_time / 2); + } + return Math.max(0, Math.min(1, scale)); + } + + var getRectangleOpacityOptions = function(lastseen) { + var scale = getScale(lastseen); + return { + strokeOpacity: strokeOpacity * scale, + fillOpacity: fillOpacity * scale + }; + } + + var getMarkerOpacityOptions = function(lastseen) { + var scale = getScale(lastseen); + return { + opacity: scale + }; + } + + // fade out / remove positions after time + setInterval(function(){ + var now = new Date().getTime(); + $.each(rectangles, function(callsign, m) { + var age = now - m.lastseen; + if (age > retention_time) { + delete rectangles[callsign]; + m.setMap(); + return; + } + m.setOptions(getRectangleOpacityOptions(m.lastseen)); + }); + $.each(markers, function(callsign, m) { + var age = now - m.lastseen; + if (age > retention_time) { + delete markers[callsign]; + m.setMap(); + return; + } + m.setOptions(getMarkerOpacityOptions(m.lastseen)); + }); + }, 1000); + })(); \ No newline at end of file diff --git a/owrx/connection.py b/owrx/connection.py index a782cfc50..d83dacd1b 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -168,7 +168,7 @@ def __init__(self, conn): super().__init__(conn) pm = PropertyManager.getSharedInstance() - self.write_config(pm.collect("google_maps_api_key", "receiver_gps").__dict__()) + self.write_config(pm.collect("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__()) Map.getSharedInstance().addClient(self) From 30b56c553e1d9f403c1e973cb7600c4453dadacb Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 7 Jul 2019 20:46:31 +0200 Subject: [PATCH 0212/2616] strip one more character; seen weird stuff at the end. --- owrx/wsjt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 8c5a7af27..b05a7c17e 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -125,7 +125,7 @@ def parse(self, data): out["db"] = float(msg[7:10]) out["dt"] = float(msg[11:15]) out["freq"] = int(msg[16:20]) - wsjt_msg = msg[24:61].strip() + wsjt_msg = msg[24:60].strip() self.getLocator(wsjt_msg) out["msg"] = wsjt_msg From 83273636f62605db390e0bccd077494195030ab9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 7 Jul 2019 21:24:56 +0200 Subject: [PATCH 0213/2616] add a quick infowindow to show who's in a grid square --- htdocs/map.css | 10 ++++++++++ htdocs/map.js | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/htdocs/map.css b/htdocs/map.css index 4b5fb0040..55cb4aade 100644 --- a/htdocs/map.css +++ b/htdocs/map.css @@ -1,4 +1,14 @@ html, body { width: 100%; height: 100%; +} + +h3 { + margin: 10px 0; +} + +ul { + margin-block-start: 5px; + margin-block-end: 5px; + padding-inline-start: 25px; } \ No newline at end of file diff --git a/htdocs/map.js b/htdocs/map.js index 9ed13f2bf..875060f7d 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -73,6 +73,8 @@ rectangle = rectangles[update.callsign]; } else { rectangle = new google.maps.Rectangle(); + var center = new google.maps.LatLng({lat: lat + .5, lng: lon + 1}); + rectangle.addListener('click', buildRectangleClick(update.location.locator, center)); rectangles[update.callsign] = rectangle; } rectangle.setOptions($.extend({ @@ -88,6 +90,7 @@ } }, getRectangleOpacityOptions(update.lastseen) )); rectangle.lastseen = update.lastseen; + rectangle.locator = update.location.locator; break; } }); @@ -144,6 +147,28 @@ console.info("onerror"); }; + var infowindow; + + var buildRectangleClick = function(locator, pos) { + if (!infowindow) infowindow = new google.maps.InfoWindow(); + return function() { + var inLocator = $.map(rectangles, function(r, callsign) { + return {callsign: callsign, locator: r.locator} + }).filter(function(d) { + return d.locator == locator; + }); + infowindow.setContent( + '

    Locator: ' + locator + '

    ' + + '
    Active Callsigns:
    ' + + '
      ' + + inLocator.map(function(i){ return '
    • ' + i.callsign + '
    • ' }).join("") + + '
    ' + ); + infowindow.setPosition(pos); + infowindow.open(map); + }; + } + var getScale = function(lastseen) { var age = new Date().getTime() - lastseen; var scale = 1; From 94afa94428b608e7fe57ab89538a564fbb77c2a0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 7 Jul 2019 21:44:42 +0200 Subject: [PATCH 0214/2616] add a link to the map --- htdocs/openwebrx.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 7a1619c44..7e936a773 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1379,13 +1379,14 @@ function update_wsjt_panel(msg) { var $b = $('#openwebrx-panel-wsjt-message tbody'); var t = new Date(msg['timestamp']); var pad = function(i) { return ('' + i).padStart(2, "0"); } + var linkedmsg = msg['msg'].replace(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/, '$1
    $2'); $b.append($( '' + '' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '' + '' + msg['db'] + '' + '' + msg['dt'] + '' + '' + msg['freq'] + '' + - '' + msg['msg'] + '' + + '' + linkedmsg + '' + '' )); $b.scrollTop($b[0].scrollHeight); From 2201daaa20f53eb032c9a53cf8cd586a012f0c96 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 7 Jul 2019 22:36:34 +0200 Subject: [PATCH 0215/2616] click-through to selected locator on the map --- htdocs/map.js | 47 ++++++++++++++++++++++++++------------------- htdocs/openwebrx.js | 6 +++++- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/htdocs/map.js b/htdocs/map.js index 875060f7d..4a3309e18 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -15,6 +15,8 @@ var expectedCallsign; if (query.callsign) expectedCallsign = query.callsign; + var expectedLocator; + if (query.locator) expectedLocator = query.locator; var ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; if (!("WebSocket" in window)) return; @@ -68,13 +70,15 @@ var loc = update.location.locator; var lat = (loc.charCodeAt(1) - 65 - 9) * 10 + Number(loc[3]); var lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]) * 2; + var center = new google.maps.LatLng({lat: lat + .5, lng: lon + 1}); var rectangle; if (rectangles[update.callsign]) { rectangle = rectangles[update.callsign]; } else { rectangle = new google.maps.Rectangle(); - var center = new google.maps.LatLng({lat: lat + .5, lng: lon + 1}); - rectangle.addListener('click', buildRectangleClick(update.location.locator, center)); + rectangle.addListener('click', function(){ + showInfoWindow(update.location.locator, center); + }); rectangles[update.callsign] = rectangle; } rectangle.setOptions($.extend({ @@ -91,6 +95,12 @@ }, getRectangleOpacityOptions(update.lastseen) )); rectangle.lastseen = update.lastseen; rectangle.locator = update.location.locator; + + if (expectedLocator && expectedLocator == update.location.locator) { + map.panTo(center); + showInfoWindow(expectedLocator, center); + delete(expectedLocator); + } break; } }); @@ -148,25 +158,22 @@ }; var infowindow; - - var buildRectangleClick = function(locator, pos) { + var showInfoWindow = function(locator, pos) { if (!infowindow) infowindow = new google.maps.InfoWindow(); - return function() { - var inLocator = $.map(rectangles, function(r, callsign) { - return {callsign: callsign, locator: r.locator} - }).filter(function(d) { - return d.locator == locator; - }); - infowindow.setContent( - '

    Locator: ' + locator + '

    ' + - '
    Active Callsigns:
    ' + - '
      ' + - inLocator.map(function(i){ return '
    • ' + i.callsign + '
    • ' }).join("") + - '
    ' - ); - infowindow.setPosition(pos); - infowindow.open(map); - }; + var inLocator = $.map(rectangles, function(r, callsign) { + return {callsign: callsign, locator: r.locator} + }).filter(function(d) { + return d.locator == locator; + }); + infowindow.setContent( + '

    Locator: ' + locator + '

    ' + + '
    Active Callsigns:
    ' + + '
      ' + + inLocator.map(function(i){ return '
    • ' + i.callsign + '
    • ' }).join("") + + '
    ' + ); + infowindow.setPosition(pos); + infowindow.open(map); } var getScale = function(lastseen) { diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 7e936a773..fa36e68dd 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1379,7 +1379,11 @@ function update_wsjt_panel(msg) { var $b = $('#openwebrx-panel-wsjt-message tbody'); var t = new Date(msg['timestamp']); var pad = function(i) { return ('' + i).padStart(2, "0"); } - var linkedmsg = msg['msg'].replace(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/, '$1$2'); + var linkedmsg = msg['msg']; + var matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); + if (matches && matches[2] != 'RR73') { + linkedmsg = matches[1] + '' + matches[2] + ''; + } $b.append($( '' + '' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '' + From 561ff954362bdd9fa41ff5d342365fc96a2084ea Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 8 Jul 2019 20:16:29 +0200 Subject: [PATCH 0216/2616] make wsjt feature available (not used yet) --- owrx/feature.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/owrx/feature.py b/owrx/feature.py index d8bcdca0c..fcc13c401 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -22,7 +22,8 @@ class FeatureDetector(object): "hackrf": [ "hackrf_transfer" ], "airspy": [ "airspy_rx" ], "digital_voice_digiham": [ "digiham", "sox" ], - "digital_voice_dsd": [ "dsd", "sox", "digiham" ] + "digital_voice_dsd": [ "dsd", "sox", "digiham" ], + "wsjt-x": [ "wsjtx" ] } def feature_availability(self): @@ -193,3 +194,11 @@ def has_airspy_rx(self): In order to use an Airspy Receiver, you need to install the airspy_rx receiver software. """ return self.command_is_runnable("airspy_rx --help 2> /dev/null") + + def has_wsjtx(self): + """ + To decode FT8 and other digimodes, you need to install the WSJT-X software suite. Please check the + [WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions + on how to build from source. + """ + return self.command_is_runnable("jt9") From c7503f87d71d70505d4b4d58aa04eb526d0e51eb Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 8 Jul 2019 20:31:34 +0200 Subject: [PATCH 0217/2616] show ft8 panel only when ft8 is active --- htdocs/openwebrx.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index fa36e68dd..3e380af59 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -2384,7 +2384,9 @@ function openwebrx_init() } function digimodes_init() { - hide_digitalvoice_panels(); + $(".openwebrx-meta-panel").each(function(_, p){ + p.openwebrxHidden = true; + }); // initialze DMR timeslot muting $('.openwebrx-dmr-timeslot-panel').click(function(e) { @@ -2678,6 +2680,7 @@ function demodulator_digital_replace(subtype) break; } toggle_panel("openwebrx-panel-digimodes", true); + if (subtype == 'ft8') toggle_panel("openwebrx-panel-wsjt-message", true); } function secondary_demod_create_canvas() @@ -2732,6 +2735,7 @@ function secondary_demod_swap_canvases() function secondary_demod_init() { $("#openwebrx-panel-digimodes")[0].openwebrxHidden = true; + $("#openwebrx-panel-wsjt-message")[0].openwebrxHidden = true; secondary_demod_canvas_container = $("#openwebrx-digimode-canvas-container")[0]; $(secondary_demod_canvas_container) .mousemove(secondary_demod_canvas_container_mousemove) @@ -2797,6 +2801,7 @@ function secondary_demod_close_window() { secondary_demod_stop(); toggle_panel("openwebrx-panel-digimodes", false); + toggle_panel("openwebrx-panel-wsjt-message", false); } secondary_demod_fft_offset_db=30; //need to calculate that later From c6aa5c3a3c9f8d5cb7b0f67241a40cdf769ee895 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 8 Jul 2019 20:45:09 +0200 Subject: [PATCH 0218/2616] make the interface pretty --- htdocs/index.html | 6 +++--- htdocs/openwebrx.css | 13 +++++++++++++ htdocs/openwebrx.js | 9 +++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index 3e8ca708c..60f00cd40 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -173,9 +173,9 @@ - - - + + + diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index f9135aeb4..e85745088 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -1038,10 +1038,23 @@ img.openwebrx-mirror-img #openwebrx-panel-wsjt-message td { width: 50px; text-align: left; + padding: 1px 3px; } #openwebrx-panel-wsjt-message .message { width: 400px; } +#openwebrx-panel-wsjt-message .decimal { + text-align: right; + width: 35px; +} + +#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container { + display: none; +} +#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container { + height: 200px; + margin: -10px; +} \ No newline at end of file diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 3e380af59..1b4fe11ff 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1387,9 +1387,9 @@ function update_wsjt_panel(msg) { $b.append($( '' + '' + - '' + - '' + - '' + + '' + + '' + + '' + '' + '' )); @@ -2679,8 +2679,9 @@ function demodulator_digital_replace(subtype) demodulator_buttons_update(); break; } + $('#openwebrx-panel-digimodes').attr('data-mode', subtype); toggle_panel("openwebrx-panel-digimodes", true); - if (subtype == 'ft8') toggle_panel("openwebrx-panel-wsjt-message", true); + toggle_panel("openwebrx-panel-wsjt-message", subtype == 'ft8'); } function secondary_demod_create_canvas() From bab8ec1eaaef607693a6188fe625443ce11e0032 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 8 Jul 2019 20:47:50 +0200 Subject: [PATCH 0219/2616] even prettier --- htdocs/openwebrx.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index e85745088..a419b8d87 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -1057,4 +1057,8 @@ img.openwebrx-mirror-img #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container { height: 200px; margin: -10px; +} + +#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel { + display: none; } \ No newline at end of file From 58e819606a0cce739f8476f26d36829147e4ad30 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 8 Jul 2019 21:01:30 +0200 Subject: [PATCH 0220/2616] use moment.js to display a pretty time since last activity --- htdocs/features.html | 2 +- htdocs/index.html | 6 +++--- htdocs/{ => lib}/jquery-3.2.1.min.js | 0 htdocs/{ => lib}/jquery.nanoscroller.js | 0 htdocs/{ => lib}/nanoscroller.css | 0 htdocs/{ => lib}/nite-overlay.js | 0 htdocs/map.html | 3 ++- htdocs/map.js | 9 ++++++--- 8 files changed, 12 insertions(+), 8 deletions(-) rename htdocs/{ => lib}/jquery-3.2.1.min.js (100%) rename htdocs/{ => lib}/jquery.nanoscroller.js (100%) rename htdocs/{ => lib}/nanoscroller.css (100%) rename htdocs/{ => lib}/nite-overlay.js (100%) diff --git a/htdocs/features.html b/htdocs/features.html index 560256793..a4d227969 100644 --- a/htdocs/features.html +++ b/htdocs/features.html @@ -3,7 +3,7 @@ - +
    diff --git a/htdocs/index.html b/htdocs/index.html index 60f00cd40..e945f3f45 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -25,9 +25,9 @@ - - - + + + diff --git a/htdocs/jquery-3.2.1.min.js b/htdocs/lib/jquery-3.2.1.min.js similarity index 100% rename from htdocs/jquery-3.2.1.min.js rename to htdocs/lib/jquery-3.2.1.min.js diff --git a/htdocs/jquery.nanoscroller.js b/htdocs/lib/jquery.nanoscroller.js similarity index 100% rename from htdocs/jquery.nanoscroller.js rename to htdocs/lib/jquery.nanoscroller.js diff --git a/htdocs/nanoscroller.css b/htdocs/lib/nanoscroller.css similarity index 100% rename from htdocs/nanoscroller.css rename to htdocs/lib/nanoscroller.css diff --git a/htdocs/nite-overlay.js b/htdocs/lib/nite-overlay.js similarity index 100% rename from htdocs/nite-overlay.js rename to htdocs/lib/nite-overlay.js diff --git a/htdocs/map.html b/htdocs/map.html index ee8908cd8..056cc14b3 100644 --- a/htdocs/map.html +++ b/htdocs/map.html @@ -2,8 +2,9 @@ OpenWebRX Map - + + diff --git a/htdocs/map.js b/htdocs/map.js index 4a3309e18..8b268cd2c 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -129,7 +129,7 @@ zoom: 5 }); processUpdates(updateQueue); - $.getScript("/static/nite-overlay.js").done(function(){ + $.getScript("/static/lib/nite-overlay.js").done(function(){ nite.init(map); setInterval(function() { nite.refresh() }, 10000); // every 10s }); @@ -161,7 +161,7 @@ var showInfoWindow = function(locator, pos) { if (!infowindow) infowindow = new google.maps.InfoWindow(); var inLocator = $.map(rectangles, function(r, callsign) { - return {callsign: callsign, locator: r.locator} + return {callsign: callsign, locator: r.locator, lastseen: r.lastseen} }).filter(function(d) { return d.locator == locator; }); @@ -169,7 +169,10 @@ '

    Locator: ' + locator + '

    ' + '
    Active Callsigns:
    ' + '
      ' + - inLocator.map(function(i){ return '
    • ' + i.callsign + '
    • ' }).join("") + + inLocator.map(function(i){ + var timestring = moment(i.lastseen).fromNow(); + return '
    • ' + i.callsign + ' (' + timestring + ')
    • ' + }).join("") + '
    ' ); infowindow.setPosition(pos); From ad9855a79160ad2f1bc27c9551d6e7ca82aa1479 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 9 Jul 2019 17:28:41 +0200 Subject: [PATCH 0221/2616] pretty logo --- htdocs/openwebrx.css | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index a419b8d87..3c3ffcdc7 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -212,6 +212,7 @@ input[type=range]:focus::-ms-fill-upper width: 46px; height: 46px; padding: 4px; + border-radius: 8px; } #webrx-top-photo-clip From 438efa655fd49ef737ca0bf5de04ba27066367f4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 9 Jul 2019 17:32:49 +0200 Subject: [PATCH 0222/2616] fix javascript issues --- htdocs/map.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/htdocs/map.js b/htdocs/map.js index 8b268cd2c..56aa29bf4 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -6,8 +6,8 @@ var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){ var s = v.split('='); - r = {} - r[s[0]] = s.slice(1).join('=') + var r = {}; + r[s[0]] = s.slice(1).join('='); return r; }).reduce(function(a, b){ return a.assign(b); @@ -33,8 +33,8 @@ // reasonable default; will be overriden by server var retention_time = 2 * 60 * 60 * 1000; - strokeOpacity = 0.8; - fillOpacity = 0.35; + var strokeOpacity = 0.8; + var fillOpacity = 0.35; var processUpdates = function(updates) { if (!map) { @@ -45,7 +45,7 @@ switch (update.location.type) { case 'latlon': - var pos = new google.maps.LatLng(update.location.lat, update.location.lon) + var pos = new google.maps.LatLng(update.location.lat, update.location.lon); var marker; if (markers[update.callsign]) { marker = markers[update.callsign]; @@ -104,7 +104,7 @@ break; } }); - } + }; ws.onmessage = function(e){ if (typeof e.data != 'string') { @@ -116,7 +116,7 @@ return } try { - json = JSON.parse(e.data); + var json = JSON.parse(e.data); switch (json.type) { case "config": var config = json.value; @@ -135,10 +135,10 @@ }); }); retention_time = config.map_position_retention_time * 1000; - break + break; case "update": processUpdates(json.value); - break + break; } } catch (e) { // don't lose exception @@ -177,7 +177,7 @@ ); infowindow.setPosition(pos); infowindow.open(map); - } + }; var getScale = function(lastseen) { var age = new Date().getTime() - lastseen; @@ -186,7 +186,7 @@ scale = (retention_time - age) / (retention_time / 2); } return Math.max(0, Math.min(1, scale)); - } + }; var getRectangleOpacityOptions = function(lastseen) { var scale = getScale(lastseen); @@ -194,14 +194,14 @@ strokeOpacity: strokeOpacity * scale, fillOpacity: fillOpacity * scale }; - } + }; var getMarkerOpacityOptions = function(lastseen) { var scale = getScale(lastseen); return { opacity: scale }; - } + }; // fade out / remove positions after time setInterval(function(){ From 2536d9f74705702edd7d65577f62d88b0023a4b1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 9 Jul 2019 17:34:24 +0200 Subject: [PATCH 0223/2616] more javascript issues --- htdocs/features.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/features.js b/htdocs/features.js index e534bcbc1..6da77c804 100644 --- a/htdocs/features.js +++ b/htdocs/features.js @@ -1,9 +1,9 @@ $(function(){ var converter = new showdown.Converter(); $.ajax('/api/features').done(function(data){ - $table = $('table.features'); + var $table = $('table.features'); $.each(data, function(name, details) { - requirements = $.map(details.requirements, function(r, name){ + var requirements = $.map(details.requirements, function(r, name){ return '
    ' + '' + '' + From cb0b950d34c9505dc774db59dc25dd58874b5596 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 10 Jul 2019 22:09:31 +0200 Subject: [PATCH 0224/2616] protect the wave file switchover with a lock, since race conditions have occured --- owrx/wsjt.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index b05a7c17e..9fe3bbcd1 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -17,6 +17,7 @@ class Ft8Chopper(threading.Thread): def __init__(self, source): self.source = source (self.wavefilename, self.wavefile) = self.getWaveFile() + self.switchingLock = threading.Lock() self.scheduler = sched.scheduler(time.time, time.sleep) self.fileQueue = [] (self.outputReader, self.outputWriter) = Pipe() @@ -53,9 +54,11 @@ def _scheduleNextSwitch(self): self.scheduler.enterabs(self.getNextDecodingTime(), 1, self.switchFiles) def switchFiles(self): + self.switchingLock.acquire() file = self.wavefile filename = self.wavefilename (self.wavefilename, self.wavefile) = self.getWaveFile() + self.switchingLock.release() file.close() self.fileQueue.append(filename) @@ -90,7 +93,9 @@ def run(self) -> None: logger.warning("zero read on ft8 chopper") self.doRun = False else: + self.switchingLock.acquire() self.wavefile.writeframes(data) + self.switchingLock.release() self.decode() logger.debug("FT8 chopper shutting down") From 32c76beaa27ff42c7a47c0193fe8e301683c0d19 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 10 Jul 2019 22:18:16 +0200 Subject: [PATCH 0225/2616] improved fullscreen layout --- htdocs/map.css | 3 ++- htdocs/map.html | 1 + htdocs/map.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/htdocs/map.css b/htdocs/map.css index 55cb4aade..a982e1bcd 100644 --- a/htdocs/map.css +++ b/htdocs/map.css @@ -1,6 +1,7 @@ -html, body { +html, body, .openwebrx-map { width: 100%; height: 100%; + margin: 0; } h3 { diff --git a/htdocs/map.html b/htdocs/map.html index 056cc14b3..9e4b25b49 100644 --- a/htdocs/map.html +++ b/htdocs/map.html @@ -9,5 +9,6 @@ +
    diff --git a/htdocs/map.js b/htdocs/map.js index 56aa29bf4..6ca108c23 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -121,7 +121,7 @@ case "config": var config = json.value; $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ - map = new google.maps.Map($('body')[0], { + map = new google.maps.Map($('.openwebrx-map')[0], { center: { lat: config.receiver_gps[0], lng: config.receiver_gps[1] From 8a8768ed1d4da72447abcc3de934f3efb796b8e7 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 10 Jul 2019 22:31:06 +0200 Subject: [PATCH 0226/2616] fix ft8 audio sample rate issues with sox --- csdr.py | 13 ++++++++++--- owrx/feature.py | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/csdr.py b/csdr.py index c9d17c76d..9203d17d6 100755 --- a/csdr.py +++ b/csdr.py @@ -167,9 +167,15 @@ def chain(self,which): chain += last_decimation_block chain += [ "csdr agc_ff", - "csdr limit_ff", - "csdr convert_f_s16" + "csdr limit_ff" ] + # fixed sample rate necessary for the wsjt-x tools. fix with sox... + if self.get_secondary_demodulator() == "ft8" and self.get_audio_rate() != self.get_output_rate(): + chain += [ + "sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + ] + else: + chain += ["csdr convert_f_s16"] if self.audio_compression=="adpcm": chain += ["csdr encode_ima_adpcm_i16_u8"] @@ -472,7 +478,8 @@ def start(self): flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000), - unvoiced_quality = self.get_unvoiced_quality(), dmr_control_pipe = self.dmr_control_pipe) + unvoiced_quality = self.get_unvoiced_quality(), dmr_control_pipe = self.dmr_control_pipe, + audio_rate = self.get_audio_rate()) logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) my_env=os.environ.copy() diff --git a/owrx/feature.py b/owrx/feature.py index fcc13c401..9009e1355 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -23,7 +23,7 @@ class FeatureDetector(object): "airspy": [ "airspy_rx" ], "digital_voice_digiham": [ "digiham", "sox" ], "digital_voice_dsd": [ "dsd", "sox", "digiham" ], - "wsjt-x": [ "wsjtx" ] + "wsjt-x": [ "wsjtx", "sox" ] } def feature_availability(self): From 596c868b9da5477da79fadd8f97d4c36f602f7e0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 10 Jul 2019 22:56:32 +0200 Subject: [PATCH 0227/2616] improved map logo --- htdocs/gfx/openwebrx-panel-map.png | Bin 3218 -> 3027 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/htdocs/gfx/openwebrx-panel-map.png b/htdocs/gfx/openwebrx-panel-map.png index 36cb90e1fe39c49667abaf735aa749995cba5f11..81ec9e2f8aad25e5954a259c41686ae6b862b425 100644 GIT binary patch delta 3021 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 P00000NkvXXu0mjfU_zPl delta 3214 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 AbN~PV From d57f9de21efb2d3a3f8961a69759200d070eda7e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 10 Jul 2019 23:13:03 +0200 Subject: [PATCH 0228/2616] automatic map reconnection --- htdocs/map.js | 119 ++++++++++++++++++++++++++++---------------------- 1 file changed, 68 insertions(+), 51 deletions(-) diff --git a/htdocs/map.js b/htdocs/map.js index 6ca108c23..b104a0a8a 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -21,11 +21,6 @@ var ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; if (!("WebSocket" in window)) return; - var ws = new WebSocket(ws_url); - ws.onopen = function(){ - ws.send("SERVER DE CLIENT client=map.js type=map"); - }; - var map; var markers = {}; var rectangles = {}; @@ -106,57 +101,79 @@ }); }; - ws.onmessage = function(e){ - if (typeof e.data != 'string') { - console.error("unsupported binary data on websocket; ignoring"); - return - } - if (e.data.substr(0, 16) == "CLIENT DE SERVER") { - console.log("Server acknowledged WebSocket connection."); - return - } - try { - var json = JSON.parse(e.data); - switch (json.type) { - case "config": - var config = json.value; - $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ - map = new google.maps.Map($('.openwebrx-map')[0], { - center: { - lat: config.receiver_gps[0], - lng: config.receiver_gps[1] - }, - zoom: 5 - }); - processUpdates(updateQueue); - $.getScript("/static/lib/nite-overlay.js").done(function(){ - nite.init(map); - setInterval(function() { nite.refresh() }, 10000); // every 10s + var clearMap = function(){ + var reset = function(callsign, item) { item.setMap(); }; + $.each(markers, reset); + $.each(rectangles, reset); + markers = {}; + rectangles = {}; + }; + + var connect = function(){ + var ws = new WebSocket(ws_url); + ws.onopen = function(){ + ws.send("SERVER DE CLIENT client=map.js type=map"); + }; + + ws.onmessage = function(e){ + if (typeof e.data != 'string') { + console.error("unsupported binary data on websocket; ignoring"); + return + } + if (e.data.substr(0, 16) == "CLIENT DE SERVER") { + console.log("Server acknowledged WebSocket connection."); + return + } + try { + var json = JSON.parse(e.data); + switch (json.type) { + case "config": + var config = json.value; + if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ + map = new google.maps.Map($('.openwebrx-map')[0], { + center: { + lat: config.receiver_gps[0], + lng: config.receiver_gps[1] + }, + zoom: 5 + }); + processUpdates(updateQueue); + updateQueue = []; + $.getScript("/static/lib/nite-overlay.js").done(function(){ + nite.init(map); + setInterval(function() { nite.refresh() }, 10000); // every 10s + }); }); - }); - retention_time = config.map_position_retention_time * 1000; - break; - case "update": - processUpdates(json.value); - break; + retention_time = config.map_position_retention_time * 1000; + break; + case "update": + processUpdates(json.value); + break; + } + } catch (e) { + // don't lose exception + console.error(e); } - } catch (e) { - // don't lose exception - console.error(e); - } - }; - ws.onclose = function(){ - console.info("onclose"); - }; + }; + ws.onclose = function(){ + clearMap(); + setTimeout(connect, 5000); + }; - window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript - ws.onclose = function () {}; - ws.close(); - }; - ws.onerror = function(){ - console.info("onerror"); + window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript + ws.onclose = function () {}; + ws.close(); + }; + + /* + ws.onerror = function(){ + console.info("websocket error"); + }; + */ }; + connect(); + var infowindow; var showInfoWindow = function(locator, pos) { if (!infowindow) infowindow = new google.maps.InfoWindow(); From 2bf2fcd6850081b8635090dce6ff87890449632b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 11 Jul 2019 13:40:12 +0200 Subject: [PATCH 0229/2616] implement header on map page (not fully functional yet) --- htdocs/{ => css}/map.css | 8 ++ htdocs/css/openwebrx-globals.css | 8 ++ htdocs/css/openwebrx-header.css | 201 ++++++++++++++++++++++++++ htdocs/include/header.include.html | 30 ++++ htdocs/index.html | 31 +--- htdocs/map.html | 3 +- htdocs/openwebrx.css | 224 +---------------------------- owrx/controllers.py | 31 +++- 8 files changed, 279 insertions(+), 257 deletions(-) rename htdocs/{ => css}/map.css (53%) create mode 100644 htdocs/css/openwebrx-globals.css create mode 100644 htdocs/css/openwebrx-header.css create mode 100644 htdocs/include/header.include.html diff --git a/htdocs/map.css b/htdocs/css/map.css similarity index 53% rename from htdocs/map.css rename to htdocs/css/map.css index a982e1bcd..7595ab7b6 100644 --- a/htdocs/map.css +++ b/htdocs/css/map.css @@ -1,3 +1,11 @@ +@import url("openwebrx-header.css"); +@import url("openwebrx-globals.css"); + +/* expandable photo not implemented on map page */ +#webrx-top-photo-clip { + max-height: 67px; +} + html, body, .openwebrx-map { width: 100%; height: 100%; diff --git a/htdocs/css/openwebrx-globals.css b/htdocs/css/openwebrx-globals.css new file mode 100644 index 000000000..a01982d64 --- /dev/null +++ b/htdocs/css/openwebrx-globals.css @@ -0,0 +1,8 @@ +html, body +{ + margin: 0; + padding: 0; + height: 100%; + font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; + overflow: hidden; +} diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css new file mode 100644 index 000000000..9cfcc0c35 --- /dev/null +++ b/htdocs/css/openwebrx-header.css @@ -0,0 +1,201 @@ +#webrx-top-container +{ + position: relative; + z-index:1000; +} + +#webrx-top-photo +{ + width: 100%; + display: block; +} + +#webrx-top-photo-clip +{ + min-height: 67px; + max-height: 350px; + overflow: hidden; + position: relative; +} + +.webrx-top-bar-parts +{ + height:67px; +} + +#webrx-top-bar +{ + background: rgba(128, 128, 128, 0.15); + margin:0; + padding:0; + user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + overflow: hidden; + position: absolute; + left: 0; + top: 0; + right: 0; +} + +#webrx-top-logo +{ + padding: 12px; + float: left; +} + +#webrx-ha5kfu-top-logo +{ + float: right; + padding: 15px; +} + +#webrx-rx-avatar-background +{ + cursor:pointer; + background-image: url(../gfx/openwebrx-avatar-background.png); + background-origin: content-box; + background-repeat: no-repeat; + float: left; + width: 54px; + height: 54px; + padding: 7px; +} + +#webrx-rx-avatar +{ + cursor:pointer; + width: 46px; + height: 46px; + padding: 4px; + border-radius: 8px; +} + +#webrx-rx-texts { + float: left; + padding: 10px; +} + +#webrx-rx-texts div { + padding: 3px; +} + +#webrx-rx-title +{ + white-space:nowrap; + overflow: hidden; + cursor:pointer; + font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; + color: #909090; + font-size: 11pt; + font-weight: bold; +} + +#webrx-rx-desc +{ + white-space:nowrap; + overflow: hidden; + cursor:pointer; + font-size: 10pt; + color: #909090; +} + +#webrx-rx-desc a +{ + color: #909090; +} + +#openwebrx-rx-details-arrow +{ + cursor:pointer; + position: absolute; + left: 470px; + top: 51px; +} + +#openwebrx-rx-details-arrow a +{ + margin: 0; + padding: 0; +} + +#openwebrx-rx-details-arrow-down +{ + display:none; +} + +#openwebrx-main-buttons ul +{ + display: table; + margin:0; +} + + +#openwebrx-main-buttons ul li +{ + display: table-cell; + padding-left: 5px; + padding-right: 5px; + cursor:pointer; +} + +#openwebrx-main-buttons a { + color: inherit; + text-decoration: inherit; +} + +#openwebrx-main-buttons li:hover +{ + background-color: rgba(255, 255, 255, 0.3); +} + +#openwebrx-main-buttons li:active +{ + background-color: rgba(255, 255, 255, 0.55); +} + + +#openwebrx-main-buttons +{ + float: right; + margin:0; + color: white; + text-shadow: 0px 0px 4px #000000; + text-align: center; + font-size: 9pt; + font-weight: bold; +} + +#webrx-rx-photo-title +{ + position: absolute; + left: 15px; + top: 78px; + color: White; + font-size: 16pt; + text-shadow: 1px 1px 4px #444; + opacity: 1; +} + +#webrx-rx-photo-desc +{ + position: absolute; + left: 15px; + top: 109px; + color: White; + font-size: 10pt; + font-weight: bold; + text-shadow: 0px 0px 6px #444; + opacity: 1; + line-height: 1.5em; +} + +#webrx-rx-photo-desc a +{ + color: #5ca8ff; + text-shadow: none; +} + diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html new file mode 100644 index 000000000..b2f3bc9ff --- /dev/null +++ b/htdocs/include/header.include.html @@ -0,0 +1,30 @@ +
    +
    + +
    + + +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    +
      +

    • Status
    • +

    • Log
    • +

    • Receiver
    • +

    • Map
    • +
    +
    +
    +
    +
    +
    +
    diff --git a/htdocs/index.html b/htdocs/index.html index e945f3f45..c17d83b78 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -33,36 +33,7 @@
    -
    -
    - -
    - - -
    - -
    -
    -
    -
    -
    -
    - - -
    -
    -
      -

    • Status
    • -

    • Log
    • -

    • Receiver
    • -

    • Map
    • -
    -
    -
    -
    -
    -
    -
    + ${header}
    diff --git a/htdocs/map.html b/htdocs/map.html index 9e4b25b49..f09a44ad1 100644 --- a/htdocs/map.html +++ b/htdocs/map.html @@ -5,10 +5,11 @@ - + + ${header}
    diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 3c3ffcdc7..39a5b9537 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -18,15 +18,8 @@ along with this program. If not, see . */ - -html, body -{ - margin: 0; - padding: 0; - height: 100%; - font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; - overflow: hidden; -} +@import url("css/openwebrx-header.css"); +@import url("css/openwebrx-globals.css"); select { @@ -147,179 +140,12 @@ input[type=range]:focus::-ms-fill-upper background: #B6B6B6; } -#webrx-top-container -{ - position: relative; - z-index:1000; -} - -.webrx-top-bar-parts -{ - height:67px; -} - -#webrx-top-bar -{ - background: rgba(128, 128, 128, 0.15); - margin:0; - padding:0; - user-select: none; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - overflow: hidden; - position: absolute; - left: 0; - top: 0; - right: 0; -} - -#webrx-top-logo -{ - padding: 12px; - float: left; -} - -#webrx-ha5kfu-top-logo -{ - float: right; - padding: 15px; -} - -#webrx-top-photo -{ - width: 100%; - display: block; -} - -#webrx-rx-avatar-background -{ - cursor:pointer; - background-image: url(gfx/openwebrx-avatar-background.png); - background-origin: content-box; - background-repeat: no-repeat; - float: left; - width: 54px; - height: 54px; - padding: 7px; -} - -#webrx-rx-avatar -{ - cursor:pointer; - width: 46px; - height: 46px; - padding: 4px; - border-radius: 8px; -} - -#webrx-top-photo-clip -{ - min-height: 67px; - max-height: 350px; - overflow: hidden; - position: relative; -} - #webrx-page-container { min-height:100%; position:relative; } -#webrx-rx-photo-title -{ - position: absolute; - left: 15px; - top: 78px; - color: White; - font-size: 16pt; - text-shadow: 1px 1px 4px #444; - opacity: 1; -} - -#webrx-rx-photo-desc -{ - position: absolute; - left: 15px; - top: 109px; - color: White; - font-size: 10pt; - font-weight: bold; - text-shadow: 0px 0px 6px #444; - opacity: 1; - line-height: 1.5em; -} - -#webrx-rx-photo-desc a -{ - color: #5ca8ff; - text-shadow: none; -} - -#webrx-rx-texts { - float: left; - padding: 10px; -} - -#webrx-rx-texts div { - padding: 3px; -} - -#webrx-rx-title -{ - white-space:nowrap; - overflow: hidden; - cursor:pointer; - font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; - color: #909090; - font-size: 11pt; - font-weight: bold; -} - -#webrx-rx-desc -{ - white-space:nowrap; - overflow: hidden; - cursor:pointer; - font-size: 10pt; - color: #909090; -} - -#webrx-rx-desc a -{ - color: #909090; -} - -#openwebrx-rx-details-arrow -{ - cursor:pointer; - position: absolute; - left: 470px; - top: 51px; -} - -#openwebrx-rx-details-arrow a -{ - margin: 0; - padding: 0; -} - -#openwebrx-rx-details-arrow-down -{ - display:none; -} - -/*canvas#waterfall-canvas -{ - border-style: none; - border-width: 1px; - height: 150px; - width: 100%; -}*/ - #openwebrx-scale-container { height: 47px; @@ -638,52 +464,6 @@ img.openwebrx-mirror-img height: 20px; } -#openwebrx-main-buttons img -{ -} - -#openwebrx-main-buttons ul -{ - display: table; - margin:0; -} - - -#openwebrx-main-buttons ul li -{ - display: table-cell; - padding-left: 5px; - padding-right: 5px; - cursor:pointer; -} - -#openwebrx-main-buttons a { - color: inherit; - text-decoration: inherit; -} - -#openwebrx-main-buttons li:hover -{ - background-color: rgba(255, 255, 255, 0.3); -} - -#openwebrx-main-buttons li:active -{ - background-color: rgba(255, 255, 255, 0.55); -} - - -#openwebrx-main-buttons -{ - float: right; - margin:0; - color: white; - text-shadow: 0px 0px 4px #000000; - text-align: center; - font-size: 9pt; - font-weight: bold; -} - #openwebrx-panel-receiver { width:110px; diff --git a/owrx/controllers.py b/owrx/controllers.py index f979891d5..0732f2f2f 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -2,6 +2,7 @@ import mimetypes import json from datetime import datetime +from string import Template from owrx.websocket import WebSocketConnection from owrx.config import PropertyManager from owrx.source import ClientRegistry @@ -72,14 +73,36 @@ def handle_request(self): filename = self.request.matches.group(1) self.serve_file(filename) -class IndexController(AssetsController): +class TemplateController(Controller): + def render_template(self, file, **vars): + f = open('htdocs/' + file, 'r') + template = Template(f.read()) + f.close() + + return template.safe_substitute(**vars) + + def serve_template(self, file, **vars): + self.send_response(self.render_template(file, **vars), content_type = 'text/html') + + def default_variables(self): + return {} + + +class WebpageController(TemplateController): + def template_variables(self): + header = self.render_template('include/header.include.html') + return { "header": header } + + +class IndexController(WebpageController): def handle_request(self): - self.serve_file("index.html") + self.serve_template("index.html", **self.template_variables()) + -class MapController(AssetsController): +class MapController(WebpageController): def handle_request(self): #TODO check if we have a google maps api key first? - self.serve_file("map.html") + self.serve_template("map.html", **self.template_variables()) class FeatureController(AssetsController): def handle_request(self): From 649450a24c1d97bbdfad87fae708429861a81e86 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 11 Jul 2019 13:44:04 +0200 Subject: [PATCH 0230/2616] move css --- htdocs/{ => css}/openwebrx.css | 24 ++++++++++++------------ htdocs/index.html | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) rename htdocs/{ => css}/openwebrx.css (95%) diff --git a/htdocs/openwebrx.css b/htdocs/css/openwebrx.css similarity index 95% rename from htdocs/openwebrx.css rename to htdocs/css/openwebrx.css index 39a5b9537..13bb3fd71 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -18,8 +18,8 @@ along with this program. If not, see . */ -@import url("css/openwebrx-header.css"); -@import url("css/openwebrx-globals.css"); +@import url("openwebrx-header.css"); +@import url("openwebrx-globals.css"); select { @@ -149,7 +149,7 @@ input[type=range]:focus::-ms-fill-upper #openwebrx-scale-container { height: 47px; - background-image: url("gfx/openwebrx-scale-background.png"); + background-image: url("../gfx/openwebrx-scale-background.png"); background-repeat: repeat-x; overflow: hidden; z-index:1000; @@ -158,14 +158,14 @@ input[type=range]:focus::-ms-fill-upper #webrx-canvas-container { - /*background-image:url('gfx/openwebrx-blank-background-1.jpg');*/ + /*background-image:url('../gfx/openwebrx-blank-background-1.jpg');*/ position: relative; height: 2000px; overflow-y: scroll; overflow-x: hidden; /*background-color: #646464;*/ /*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/ - background-image: url('gfx/openwebrx-background-cool-blue.png'); + background-image: url('../gfx/openwebrx-background-cool-blue.png'); background-repeat: no-repeat; background-color: #1e5f7f; cursor: crosshair; @@ -255,15 +255,15 @@ input[type=range]:focus::-ms-fill-upper /* removed non-free fonts like that: */ /*@font-face { font-family: 'unibody_8_pro_regregular'; - src: url('gfx/unibody8pro-regular-webfont.eot'); - src: url('gfx/unibody8pro-regular-webfont.ttf'); + src: url('../gfx/unibody8pro-regular-webfont.eot'); + src: url('../gfx/unibody8pro-regular-webfont.ttf'); font-weight: normal; font-style: normal; }*/ @font-face { font-family: 'expletus-sans-medium'; - src: url('gfx/font-expletus-sans/ExpletusSans-Medium.ttf'); + src: url('../gfx/font-expletus-sans/ExpletusSans-Medium.ttf'); font-weight: normal; font-style: normal; } @@ -737,7 +737,7 @@ img.openwebrx-mirror-img .openwebrx-meta-slot.muted:before { display: block; content: ""; - background-image: url("gfx/openwebrx-mute.png"); + background-image: url("../gfx/openwebrx-mute.png"); width:100%; height:133px; background-position: center; @@ -779,11 +779,11 @@ img.openwebrx-mirror-img } .openwebrx-meta-slot.active .openwebrx-meta-user-image { - background-image: url("gfx/openwebrx-directcall.png"); + background-image: url("../gfx/openwebrx-directcall.png"); } .openwebrx-meta-slot.active .openwebrx-meta-user-image.group { - background-image: url("gfx/openwebrx-groupcall.png"); + background-image: url("../gfx/openwebrx-groupcall.png"); } .openwebrx-dmr-timeslot-panel * { @@ -791,7 +791,7 @@ img.openwebrx-mirror-img } .openwebrx-maps-pin { - background-image: url("gfx/google_maps_pin.svg"); + background-image: url("../gfx/google_maps_pin.svg"); background-position: center; background-repeat: no-repeat; width: 15px; diff --git a/htdocs/index.html b/htdocs/index.html index c17d83b78..ee688c51e 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -28,7 +28,7 @@ - + From 688bd769dd90eaab0918815968c7e2cd4e9df47a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 11 Jul 2019 13:44:41 +0200 Subject: [PATCH 0231/2616] move css --- htdocs/{ => css}/features.css | 0 htdocs/features.html | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename htdocs/{ => css}/features.css (100%) diff --git a/htdocs/features.css b/htdocs/css/features.css similarity index 100% rename from htdocs/features.css rename to htdocs/css/features.css diff --git a/htdocs/features.html b/htdocs/features.html index a4d227969..cfcfe669f 100644 --- a/htdocs/features.html +++ b/htdocs/features.html @@ -1,7 +1,7 @@ OpenWebRX Feature report - + From 5887522dce12ed90f6cfc2a104f74f1781f34913 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 11 Jul 2019 16:44:33 +0200 Subject: [PATCH 0232/2616] header for feature report --- htdocs/css/features.css | 8 ++++++++ htdocs/css/openwebrx-header.css | 2 ++ htdocs/features.html | 1 + owrx/controllers.py | 4 ++-- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/htdocs/css/features.css b/htdocs/css/features.css index cc821b122..7b0b008da 100644 --- a/htdocs/css/features.css +++ b/htdocs/css/features.css @@ -1,3 +1,11 @@ +@import url("openwebrx-header.css"); +@import url("openwebrx-globals.css"); + +/* expandable photo not implemented on features page */ +#webrx-top-photo-clip { + max-height: 67px; +} + h1 { text-align: center; margin: 50px 0; diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css index 9cfcc0c35..ef0a129bb 100644 --- a/htdocs/css/openwebrx-header.css +++ b/htdocs/css/openwebrx-header.css @@ -63,6 +63,7 @@ width: 54px; height: 54px; padding: 7px; + box-sizing: content-box; } #webrx-rx-avatar @@ -72,6 +73,7 @@ height: 46px; padding: 4px; border-radius: 8px; + box-sizing: content-box; } #webrx-rx-texts { diff --git a/htdocs/features.html b/htdocs/features.html index cfcfe669f..6e1eb5538 100644 --- a/htdocs/features.html +++ b/htdocs/features.html @@ -6,6 +6,7 @@ + ${header}

    OpenWebRX Feature Report

    UTCdBDTFreqdBDTFreq Message
    ' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '' + msg['db'] + '' + msg['dt'] + '' + msg['freq'] + '' + msg['db'] + '' + msg['dt'] + '' + msg['freq'] + '' + linkedmsg + '
    ' + name + '
    diff --git a/owrx/controllers.py b/owrx/controllers.py index 0732f2f2f..c011677d1 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -104,9 +104,9 @@ def handle_request(self): #TODO check if we have a google maps api key first? self.serve_template("map.html", **self.template_variables()) -class FeatureController(AssetsController): +class FeatureController(WebpageController): def handle_request(self): - self.serve_file("features.html") + self.serve_template("features.html", **self.template_variables()) class ApiController(Controller): def handle_request(self): From d2f524bf90e188bda1386ab4b8a6368b33794459 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 11 Jul 2019 16:49:06 +0200 Subject: [PATCH 0233/2616] fix scrolling on feature report --- htdocs/css/openwebrx-globals.css | 2 +- htdocs/css/openwebrx.css | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/htdocs/css/openwebrx-globals.css b/htdocs/css/openwebrx-globals.css index a01982d64..41ef284a0 100644 --- a/htdocs/css/openwebrx-globals.css +++ b/htdocs/css/openwebrx-globals.css @@ -4,5 +4,5 @@ html, body padding: 0; height: 100%; font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; - overflow: hidden; } + diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 13bb3fd71..b23c50b02 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -21,6 +21,10 @@ @import url("openwebrx-header.css"); @import url("openwebrx-globals.css"); +html, body { + overflow: hidden; +} + select { font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; From fdd2dd1b40ddad469da633f40fc52de5d9e6eca6 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 11 Jul 2019 17:38:53 +0200 Subject: [PATCH 0234/2616] use flexbox since the header breaks the map height --- htdocs/css/map.css | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/htdocs/css/map.css b/htdocs/css/map.css index 7595ab7b6..57a783695 100644 --- a/htdocs/css/map.css +++ b/htdocs/css/map.css @@ -6,10 +6,17 @@ max-height: 67px; } -html, body, .openwebrx-map { - width: 100%; - height: 100%; - margin: 0; +body { + display: flex; + flex-direction: column; +} + +#webrx-top-container { + flex: none; +} + +.openwebrx-map { + flex: 1 1 auto; } h3 { From 5ada234f64196cd3b6751291067836d701adb133 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 11 Jul 2019 19:37:00 +0200 Subject: [PATCH 0235/2616] remove javascript from the header --- htdocs/include/header.include.html | 16 ++++++++-------- htdocs/openwebrx.js | 9 +++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html index b2f3bc9ff..c7efe130e 100644 --- a/htdocs/include/header.include.html +++ b/htdocs/include/header.include.html @@ -5,21 +5,21 @@
    - +
    -
    -
    +
    +
    - - + +
      -

    • Status
    • -

    • Log
    • -

    • Receiver
    • +

    • Status
    • +

    • Log
    • +

    • Receiver

    • Map
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 1b4fe11ff..e004ae492 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -86,6 +86,7 @@ function init_rx_photo() window.setTimeout(function() { animate(e("webrx-rx-photo-title"),"opacity","",1,0,1,500,30); },1000); window.setTimeout(function() { animate(e("webrx-rx-photo-desc"),"opacity","",1,0,1,500,30); },1500); window.setTimeout(function() { close_rx_photo() },2500); + $('#webrx-top-container .openwebrx-photo-trigger').click(toggle_rx_photo); } dont_toggle_rx_photo_flag=0; @@ -2365,6 +2366,13 @@ function openwebrx_resize() check_top_bar_congestion(); } +function init_header() +{ + $('#openwebrx-main-buttons li[data-toggle-panel]').click(function() { + toggle_panel($(this).data('toggle-panel')); + }); +} + function openwebrx_init() { if(ios||is_chrome) e("openwebrx-big-grey").style.display="table-cell"; @@ -2377,6 +2385,7 @@ function openwebrx_init() window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); window.addEventListener("resize",openwebrx_resize); check_top_bar_congestion(); + init_header(); //Synchronise volume with slider updateVolume(); From d606c854436d9b2a71e53ddf101c94ee88a5d8e7 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 11 Jul 2019 20:48:02 +0200 Subject: [PATCH 0236/2616] separate decoder files --- owrx/wsjt.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 9fe3bbcd1..1e8c27128 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -25,7 +25,10 @@ def __init__(self, source): super().__init__() def getWaveFile(self): - filename = "/tmp/openwebrx-ft8chopper-{0}.wav".format(datetime.now().strftime("%Y%m%d-%H%M%S")) + filename = "/tmp/openwebrx-ft8chopper-{id}-{timestamp}.wav".format( + id = id(self), + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + ) wavefile = wave.open(filename, "wb") wavefile.setnchannels(1) wavefile.setsampwidth(2) From 8edc7c1374ec6a4b8e97525308fafc07cd9b0001 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 11 Jul 2019 20:53:59 +0200 Subject: [PATCH 0237/2616] sort by lastseen --- htdocs/map.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/htdocs/map.js b/htdocs/map.js index b104a0a8a..407931219 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -181,6 +181,8 @@ return {callsign: callsign, locator: r.locator, lastseen: r.lastseen} }).filter(function(d) { return d.locator == locator; + }).sort(function(a, b){ + return b.lastseen - a.lastseen; }); infowindow.setContent( '

    Locator: ' + locator + '

    ' + From acbf2939c97671db042806b8e546ca5aa7ef2691 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 11 Jul 2019 21:21:01 +0200 Subject: [PATCH 0238/2616] infowindow for ysf markers --- htdocs/css/map.css | 1 + htdocs/map.js | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/htdocs/css/map.css b/htdocs/css/map.css index 57a783695..4b27afe13 100644 --- a/htdocs/css/map.css +++ b/htdocs/css/map.css @@ -21,6 +21,7 @@ body { h3 { margin: 10px 0; + text-align: center; } ul { diff --git a/htdocs/map.js b/htdocs/map.js index 407931219..6354604ee 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -46,6 +46,9 @@ marker = markers[update.callsign]; } else { marker = new google.maps.Marker(); + marker.addListener('click', function(){ + showMarkerInfoWindow(update.callsign, pos); + }); markers[update.callsign] = marker; } marker.setOptions($.extend({ @@ -58,6 +61,7 @@ // TODO the trim should happen on the server side if (expectedCallsign && expectedCallsign == update.callsign.trim()) { map.panTo(pos); + showMarkerInfoWindow(update.callsign, pos); delete(expectedCallsign); } break; @@ -72,7 +76,7 @@ } else { rectangle = new google.maps.Rectangle(); rectangle.addListener('click', function(){ - showInfoWindow(update.location.locator, center); + showLocatorInfoWindow(update.location.locator, center); }); rectangles[update.callsign] = rectangle; } @@ -93,7 +97,7 @@ if (expectedLocator && expectedLocator == update.location.locator) { map.panTo(center); - showInfoWindow(expectedLocator, center); + showLocatorInfoWindow(expectedLocator, center); delete(expectedLocator); } break; @@ -175,7 +179,7 @@ connect(); var infowindow; - var showInfoWindow = function(locator, pos) { + var showLocatorInfoWindow = function(locator, pos) { if (!infowindow) infowindow = new google.maps.InfoWindow(); var inLocator = $.map(rectangles, function(r, callsign) { return {callsign: callsign, locator: r.locator, lastseen: r.lastseen} @@ -198,6 +202,17 @@ infowindow.open(map); }; + var showMarkerInfoWindow = function(callsign, pos) { + if (!infowindow) infowindow = new google.maps.InfoWindow(); + var marker = markers[callsign]; + var timestring = moment(marker.lastseen).fromNow(); + infowindow.setContent( + '

    ' + callsign + '

    ' + + '
    ' + timestring + '
    ' + ); + infowindow.open(map, marker); + } + var getScale = function(lastseen) { var age = new Date().getTime() - lastseen; var scale = 1; From 2470c2bfa6ba63ed02dee3d5071c5a5d078f6dab Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 11 Jul 2019 23:40:09 +0200 Subject: [PATCH 0239/2616] pass through the mode on the map --- htdocs/map.js | 8 +++++--- owrx/map.py | 10 ++++++---- owrx/meta.py | 3 ++- owrx/wsjt.py | 14 +++++++++++--- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/htdocs/map.js b/htdocs/map.js index 6354604ee..d2253ad81 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -57,6 +57,7 @@ title: update.callsign }, getMarkerOpacityOptions(update.lastseen) )); marker.lastseen = update.lastseen; + marker.mode = update.mode; // TODO the trim should happen on the server side if (expectedCallsign && expectedCallsign == update.callsign.trim()) { @@ -94,6 +95,7 @@ }, getRectangleOpacityOptions(update.lastseen) )); rectangle.lastseen = update.lastseen; rectangle.locator = update.location.locator; + rectangle.mode = update.mode; if (expectedLocator && expectedLocator == update.location.locator) { map.panTo(center); @@ -182,7 +184,7 @@ var showLocatorInfoWindow = function(locator, pos) { if (!infowindow) infowindow = new google.maps.InfoWindow(); var inLocator = $.map(rectangles, function(r, callsign) { - return {callsign: callsign, locator: r.locator, lastseen: r.lastseen} + return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode} }).filter(function(d) { return d.locator == locator; }).sort(function(a, b){ @@ -194,7 +196,7 @@ '
      ' + inLocator.map(function(i){ var timestring = moment(i.lastseen).fromNow(); - return '
    • ' + i.callsign + ' (' + timestring + ')
    • ' + return '
    • ' + i.callsign + ' (' + timestring + ' via ' + i.mode + ')
    • ' }).join("") + '
    ' ); @@ -208,7 +210,7 @@ var timestring = moment(marker.lastseen).fromNow(); infowindow.setContent( '

    ' + callsign + '

    ' + - '
    ' + timestring + '
    ' + '
    ' + timestring + ' via ' + marker.mode + '
    ' ); infowindow.open(map, marker); } diff --git a/owrx/map.py b/owrx/map.py index 2819a3ee3..4d65a4927 100644 --- a/owrx/map.py +++ b/owrx/map.py @@ -44,7 +44,8 @@ def addClient(self, client): { "callsign": callsign, "location": record["location"].__dict__(), - "lastseen": record["updated"].timestamp() * 1000 + "lastseen": record["updated"].timestamp() * 1000, + "mode" : record["mode"] } for (callsign, record) in self.positions.items() ]) @@ -55,14 +56,15 @@ def removeClient(self, client): except ValueError: pass - def updateLocation(self, callsign, loc: Location): + def updateLocation(self, callsign, loc: Location, mode: str): ts = datetime.now() - self.positions[callsign] = {"location": loc, "updated": ts} + self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode} self.broadcast([ { "callsign": callsign, "location": loc.__dict__(), - "lastseen": ts.timestamp() * 1000 + "lastseen": ts.timestamp() * 1000, + "mode" : mode } ]) diff --git a/owrx/meta.py b/owrx/meta.py index ad3f0d35d..8a85bad01 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -71,7 +71,8 @@ class YsfMetaEnricher(object): def enrich(self, meta): if "source" in meta and "lat" in meta and "lon" in meta: # TODO parsing the float values should probably happen earlier - Map.getSharedInstance().updateLocation(meta["source"], LatLngLocation(float(meta["lat"]), float(meta["lon"]))) + loc = LatLngLocation(float(meta["lat"]), float(meta["lon"])) + Map.getSharedInstance().updateLocation(meta["source"], loc, "YSF") return None diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 1e8c27128..6bb6dfe85 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -118,9 +118,15 @@ def __init__(self, handler): self.handler = handler self.locator_pattern = re.compile(".*\s([A-Z0-9]+)\s([A-R]{2}[0-9]{2})$") + modes = { + "~": "FT8" + } + def parse(self, data): try: msg = data.decode().rstrip() + # sample + # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' # known debug messages we know to skip if msg.startswith(""): return @@ -133,15 +139,17 @@ def parse(self, data): out["db"] = float(msg[7:10]) out["dt"] = float(msg[11:15]) out["freq"] = int(msg[16:20]) + modeChar = msg[21:22] + out["mode"] = mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" wsjt_msg = msg[24:60].strip() - self.getLocator(wsjt_msg) + self.parseLocator(wsjt_msg, mode) out["msg"] = wsjt_msg self.handler.write_wsjt_message(out) except ValueError: logger.exception("error while parsing wsjt message") - def getLocator(self, msg): + def parseLocator(self, msg, mode): m = self.locator_pattern.match(msg) if m is None: return @@ -149,4 +157,4 @@ def getLocator(self, msg): # likely this just means roger roger goodbye. if m.group(2) == "RR73": return - Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2))) + Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2)), mode) From c19337d65cead6b2712b6f8e688bf1e3473ef9c9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 12 Jul 2019 19:28:40 +0200 Subject: [PATCH 0240/2616] fix ft8/usb switchover --- csdr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/csdr.py b/csdr.py index 9203d17d6..4f72c98e0 100755 --- a/csdr.py +++ b/csdr.py @@ -203,6 +203,7 @@ def set_secondary_demodulator(self, what): if self.get_secondary_demodulator() == what: return self.secondary_demodulator = what + self.calculate_decimation() self.restart() def secondary_fft_block_size(self): From efc5b936f83c34583071e576fa099a59d07803ac Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 12 Jul 2019 19:34:04 +0200 Subject: [PATCH 0241/2616] clean up after use --- owrx/wsjt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 6bb6dfe85..7bb10cc0b 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -105,6 +105,10 @@ def run(self) -> None: self.outputReader.close() self.outputWriter.close() self.emptyScheduler() + try: + os.unlink(self.wavefilename) + except Exception: + logger.exception("error removing undecoded file") def read(self): try: From 935e79c9c2f1be4ec291ed549baafbdba8cad3c9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 13 Jul 2019 17:16:38 +0200 Subject: [PATCH 0242/2616] use a temporary directory to avoid permission problems --- config_webrx.py | 4 +++- csdr.py | 6 +++++- owrx/source.py | 8 +++++--- owrx/wsjt.py | 7 +++++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index 12f9c1479..eaba80313 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -236,4 +236,6 @@ # how long should positions be visible on the map? # they will start fading out after half of that # in seconds; default: 2 hours -map_position_retention_time = 2 * 60 * 60 \ No newline at end of file +map_position_retention_time = 2 * 60 * 60 + +temporary_directory = "/tmp" diff --git a/csdr.py b/csdr.py index 4f72c98e0..43bd0ac77 100755 --- a/csdr.py +++ b/csdr.py @@ -74,6 +74,10 @@ def __init__(self, output): self.unvoiced_quality = 1 self.modification_lock = threading.Lock() self.output = output + self.temporary_directory = "/tmp" + + def set_temporary_directory(self, what): + self.temporary_directory = what def chain(self,which): chain = ["nc -v 127.0.0.1 {nc_port}"] @@ -468,7 +472,7 @@ def start(self): logger.debug(command_base) #create control pipes for csdr - self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self)) + self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self)) self.try_create_pipes(self.pipe_names, command_base) diff --git a/owrx/source.py b/owrx/source.py index 3f1b2accc..7a37ef645 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -275,7 +275,7 @@ def __init__(self, sdrSource): self.props = props = self.sdrSource.props.collect( "samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression", - "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through" + "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through", "temporary_directory" ).defaults(PropertyManager.getSharedInstance()) self.dsp = dsp = csdr.dsp(self) @@ -295,6 +295,7 @@ def set_fft_averages(key, value): props.getProperty("fft_size").wire(dsp.set_fft_size), props.getProperty("fft_fps").wire(dsp.set_fft_fps), props.getProperty("fft_compression").wire(dsp.set_fft_compression), + props.getProperty("temporary_directory").wire(dsp.set_temporary_directory), props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages) ] @@ -352,7 +353,7 @@ def __init__(self, handler, sdrSource): self.localProps = self.sdrSource.getProps().collect( "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality", - "dmr_filter" + "dmr_filter", "temporary_directory" ).defaults(PropertyManager.getSharedInstance()) self.dsp = csdr.dsp(self) @@ -380,7 +381,8 @@ def set_high_cut(cut): self.localProps.getProperty("high_cut").wire(set_high_cut), self.localProps.getProperty("mod").wire(self.dsp.set_demodulator), self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality), - self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter) + self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter), + self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory) ] self.dsp.set_offset_freq(0) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 7bb10cc0b..fc07b1014 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -8,6 +8,7 @@ from multiprocessing.connection import Pipe from owrx.map import Map, LocatorLocation import re +from owrx.config import PropertyManager import logging logger = logging.getLogger(__name__) @@ -16,6 +17,7 @@ class Ft8Chopper(threading.Thread): def __init__(self, source): self.source = source + self.tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"] (self.wavefilename, self.wavefile) = self.getWaveFile() self.switchingLock = threading.Lock() self.scheduler = sched.scheduler(time.time, time.sleep) @@ -25,7 +27,8 @@ def __init__(self, source): super().__init__() def getWaveFile(self): - filename = "/tmp/openwebrx-ft8chopper-{id}-{timestamp}.wav".format( + filename = "{tmp_dir}/openwebrx-ft8chopper-{id}-{timestamp}.wav".format( + tmp_dir = self.tmp_dir, id = id(self), timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") ) @@ -70,7 +73,7 @@ def switchFiles(self): def decode(self): def decode_and_unlink(file): #TODO expose decoding quality parameters through config - decoder = subprocess.Popen(["jt9", "--ft8", "-d", "3", file], stdout=subprocess.PIPE) + decoder = subprocess.Popen(["jt9", "--ft8", "-d", "3", file], stdout=subprocess.PIPE, cwd=self.tmp_dir) while True: line = decoder.stdout.readline() if line is None or (isinstance(line, bytes) and len(line) == 0): From 9a25c68d9a4b0c0a03baf181cb3c34d89c59c949 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 13 Jul 2019 17:20:03 +0200 Subject: [PATCH 0243/2616] wording change --- htdocs/map.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/map.js b/htdocs/map.js index d2253ad81..82f4a5999 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -196,7 +196,7 @@ '
      ' + inLocator.map(function(i){ var timestring = moment(i.lastseen).fromNow(); - return '
    • ' + i.callsign + ' (' + timestring + ' via ' + i.mode + ')
    • ' + return '
    • ' + i.callsign + ' (' + timestring + ' using ' + i.mode + ')
    • ' }).join("") + '
    ' ); @@ -210,7 +210,7 @@ var timestring = moment(marker.lastseen).fromNow(); infowindow.setContent( '

    ' + callsign + '

    ' + - '
    ' + timestring + ' via ' + marker.mode + '
    ' + '
    ' + timestring + ' using ' + marker.mode + '
    ' ); infowindow.open(map, marker); } From 95c117973f58c9d25ed202c3998bad7d010c5bd7 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 13 Jul 2019 18:59:06 +0200 Subject: [PATCH 0244/2616] update readme with new image --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d505a7965..493f71c73 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,13 @@ It has the following features: - [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF) - [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN) +**News (2019-07-13 by DD5JFK)** +- Latest Features: + - FT8 Integration (using wsjt-x demodulators) + - New Map Feature that shows both decoded grid squares from FT8 and Locations decoded from YSF digital voice + - New Feature report that will show what functionality is available +- There's a new Raspbian SD Card image available (see below) + **News (2019-06-30 by DD5JFK)** - I have done some major rework on the openwebrx core, and I am planning to continue adding more features in the near future. Please check this place for updates. - My work has not been accepted into the upstream repository, so you will need to chose between my fork and the official version. @@ -42,7 +49,7 @@ It has the following features: ### Raspberry Pi SD Card Images -Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-06-21-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+. +Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-07-13-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+. This is based off the Raspbian Lite distribution, so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply. From f490fbc2c9c19df4282c215a280982aa802f6c0d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 13 Jul 2019 21:35:57 +0200 Subject: [PATCH 0245/2616] update dependencies add wsjt-x to build for ft8 capabilities --- docker/scripts/install-dependencies-hackrf.sh | 4 ++-- docker/scripts/install-dependencies-sdrplay.sh | 4 ++-- docker/scripts/install-dependencies-soapysdr.sh | 4 +++- docker/scripts/install-dependencies.sh | 14 +++++++++++--- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/docker/scripts/install-dependencies-hackrf.sh b/docker/scripts/install-dependencies-hackrf.sh index 1a460cc35..478664422 100755 --- a/docker/scripts/install-dependencies-hackrf.sh +++ b/docker/scripts/install-dependencies-hackrf.sh @@ -14,8 +14,8 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="libusb fftw" -BUILD_PACKAGES="git cmake make patch wget sudo udev gcc g++ libusb-dev fftw-dev" +STATIC_PACKAGES="libusb fftw udev" +BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev fftw-dev" apk add --no-cache $STATIC_PACKAGES apk add --no-cache --virtual .build-deps $BUILD_PACKAGES diff --git a/docker/scripts/install-dependencies-sdrplay.sh b/docker/scripts/install-dependencies-sdrplay.sh index 3ac29cc3f..fba859881 100755 --- a/docker/scripts/install-dependencies-sdrplay.sh +++ b/docker/scripts/install-dependencies-sdrplay.sh @@ -14,8 +14,8 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="libusb" -BUILD_PACKAGES="git cmake make patch wget sudo udev gcc g++ libusb-dev" +STATIC_PACKAGES="libusb udev" +BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev" apk add --no-cache $STATIC_PACKAGES apk add --no-cache --virtual .build-deps $BUILD_PACKAGES diff --git a/docker/scripts/install-dependencies-soapysdr.sh b/docker/scripts/install-dependencies-soapysdr.sh index 9e598c779..1731ed8f0 100755 --- a/docker/scripts/install-dependencies-soapysdr.sh +++ b/docker/scripts/install-dependencies-soapysdr.sh @@ -14,8 +14,10 @@ function cmakebuild() { cd /tmp -BUILD_PACKAGES="git cmake make patch wget sudo udev gcc g++" +STATIC_PACKAGES="udev" +BUILD_PACKAGES="git cmake make patch wget sudo gcc g++" +apk add --no-cache $STATIC_PACKAGES apk add --no-cache --virtual .build-deps $BUILD_PACKAGES git clone https://github.com/pothosware/SoapySDR diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 48136b281..46d698c1c 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -14,8 +14,8 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack" -BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers" +STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport" +BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev asciidoctor asciidoc" apk add --no-cache $STATIC_PACKAGES apk add --no-cache --virtual .build-deps $BUILD_PACKAGES @@ -23,7 +23,7 @@ apk add --no-cache --virtual .build-deps $BUILD_PACKAGES git clone https://git.code.sf.net/p/itpp/git itpp cmakebuild itpp -git clone https://github.com/simonyiszk/csdr.git +git clone https://github.com/jketterl/csdr.git -b 48khz_filter cd csdr patch -Np1 <<'EOF' --- a/csdr.c @@ -68,6 +68,8 @@ rm -rf csdr git clone https://github.com/szechyjs/mbelib.git cmakebuild mbelib +# no idea why it's put into there now. alpine does not handle it correctly, so move it. +mv /usr/local/lib64/libmbe* /usr/local/lib git clone https://github.com/jketterl/digiham.git cmakebuild digiham @@ -75,4 +77,10 @@ cmakebuild digiham git clone https://github.com/f4exb/dsd.git cmakebuild dsd +WSJT_DIR=wsjtx-2.0.1 +WSJT_TGZ=${WSJT_DIR}.tgz +wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ +tar xvfz $WSJT_TGZ +cmakebuild $WSJT_DIR + apk del .build-deps From 9f2b715d9ffbecbfeec1799f7137e2af6c34f013 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 13 Jul 2019 21:40:48 +0200 Subject: [PATCH 0246/2616] exponential backoff --- htdocs/openwebrx.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index e004ae492..69175416f 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1472,6 +1472,7 @@ function on_ws_opened() { ws.send("SERVER DE CLIENT client=openwebrx.js type=receiver"); divlog("WebSocket opened to "+ws_url); + reconnect_timeout = false; } var was_error=0; @@ -1852,6 +1853,8 @@ function audio_init() } +var reconnect_timeout = false; + function on_ws_closed() { try @@ -1860,9 +1863,16 @@ function on_ws_closed() } catch (dont_care) {} audio_initialized = 0; - divlog("WebSocket has closed unexpectedly. Attempting to reconnect in 5 seconds...", 1); + if (reconnect_timeout) { + // max value: roundabout 8 and a half minutes + reconnect_timeout = Math.min(reconnect_timeout * 2, 512000); + } else { + // initial value: 1s + reconnect_timeout = 1000; + } + divlog("WebSocket has closed unexpectedly. Attempting to reconnect in " + reconnect_timeout / 1000 + " seconds...", 1); - setTimeout(open_websocket, 5000); + setTimeout(open_websocket, reconnect_timeout); } function on_ws_error(event) From 420b0c60d75241f92f56dc8d8311a49c86bb4092 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 13 Jul 2019 21:44:48 +0200 Subject: [PATCH 0247/2616] exponential backoff, part 2 --- htdocs/map.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/htdocs/map.js b/htdocs/map.js index 82f4a5999..b5f905aa9 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -115,10 +115,13 @@ rectangles = {}; }; + var reconnect_timeout = false; + var connect = function(){ var ws = new WebSocket(ws_url); ws.onopen = function(){ ws.send("SERVER DE CLIENT client=map.js type=map"); + reconnect_timeout = false }; ws.onmessage = function(e){ @@ -163,7 +166,14 @@ }; ws.onclose = function(){ clearMap(); - setTimeout(connect, 5000); + if (reconnect_timeout) { + // max value: roundabout 8 and a half minutes + reconnect_timeout = Math.min(reconnect_timeout * 2, 512000); + } else { + // initial value: 1s + reconnect_timeout = 1000; + } + setTimeout(connect, reconnect_timeout); }; window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript From 6d5c8491e4de7b3e5990cc9c1ede4c2665dc382e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 13 Jul 2019 23:16:25 +0200 Subject: [PATCH 0248/2616] implement wspr --- csdr.py | 23 +++++--- htdocs/css/openwebrx.css | 12 +++-- htdocs/index.html | 1 + htdocs/openwebrx.js | 16 +++--- owrx/feature.py | 25 ++++++--- owrx/wsjt.py | 111 ++++++++++++++++++++++++++++++--------- 6 files changed, 135 insertions(+), 53 deletions(-) diff --git a/csdr.py b/csdr.py index 43bd0ac77..d9452c0a2 100755 --- a/csdr.py +++ b/csdr.py @@ -25,7 +25,7 @@ import signal import threading from functools import partial -from owrx.wsjt import Ft8Chopper +from owrx.wsjt import Ft8Chopper, WsprChopper import logging logger = logging.getLogger(__name__) @@ -174,7 +174,7 @@ def chain(self,which): "csdr limit_ff" ] # fixed sample rate necessary for the wsjt-x tools. fix with sox... - if self.get_secondary_demodulator() == "ft8" and self.get_audio_rate() != self.get_output_rate(): + if self.isWsjtMode() and self.get_audio_rate() != self.get_output_rate(): chain += [ "sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " ] @@ -196,8 +196,8 @@ def secondary_chain(self, which): "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" - elif which == "ft8": - chain = secondary_chain_base + "csdr realpart_cf | " + elif self.isWsjtMode(which): + chain = secondary_chain_base + "csdr realpart_cf | " if self.last_decimation != 1.0 : chain += "csdr fractional_decimator_ff {last_decimation} | " chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" @@ -271,8 +271,12 @@ def start_secondary_demodulator(self): self.secondary_processes_running = True self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read()))) - if self.get_secondary_demodulator() == "ft8": - chopper = Ft8Chopper(self.secondary_process_demod.stdout) + if self.isWsjtMode(): + smd = self.get_secondary_demodulator() + if smd == "ft8": + chopper = Ft8Chopper(self.secondary_process_demod.stdout) + elif smd == "wspr": + chopper = WsprChopper(self.secondary_process_demod.stdout) chopper.start() self.output.add_output("wsjt_demod", chopper.read) else: @@ -355,7 +359,7 @@ def get_output_rate(self): def get_audio_rate(self): if self.isDigitalVoice(): return 48000 - elif self.secondary_demodulator == "ft8": + elif self.isWsjtMode(): return 12000 return self.get_output_rate() @@ -364,6 +368,11 @@ def isDigitalVoice(self, demodulator = None): demodulator = self.get_demodulator() return demodulator in ["dmr", "dstar", "nxdn", "ysf"] + def isWsjtMode(self, demodulator = None): + if demodulator is None: + demodulator = self.get_secondary_demodulator() + return demodulator in ["ft8", "wspr"] + def set_output_rate(self,output_rate): self.output_rate=output_rate self.calculate_decimation() diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index b23c50b02..87b060895 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -835,15 +835,21 @@ img.openwebrx-mirror-img width: 35px; } -#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container { +#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container +{ display: none; } -#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container { +#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container, +#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container +{ height: 200px; margin: -10px; } -#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel { +#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel +{ display: none; } \ No newline at end of file diff --git a/htdocs/index.html b/htdocs/index.html index ee688c51e..45e94e42a 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -81,6 +81,7 @@ +
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 69175416f..c47786e98 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -2693,6 +2693,7 @@ function demodulator_digital_replace(subtype) case "bpsk31": case "rtty": case "ft8": + case "wspr": secondary_demod_start(subtype); demodulator_analog_replace('usb', true); demodulator_buttons_update(); @@ -2700,7 +2701,7 @@ function demodulator_digital_replace(subtype) } $('#openwebrx-panel-digimodes').attr('data-mode', subtype); toggle_panel("openwebrx-panel-digimodes", true); - toggle_panel("openwebrx-panel-wsjt-message", subtype == 'ft8'); + toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr'].indexOf(subtype) >= 0); } function secondary_demod_create_canvas() @@ -2862,20 +2863,17 @@ function secondary_demod_waterfall_dequeue() secondary_demod_listbox_updating = false; function secondary_demod_listbox_changed() { - if(secondary_demod_listbox_updating) return; - switch ($("#openwebrx-secondary-demod-listbox")[0].value) - { + if (secondary_demod_listbox_updating) return; + var sdm = $("#openwebrx-secondary-demod-listbox")[0].value; + switch (sdm) { case "none": demodulator_analog_replace_last(); break; case "bpsk31": - demodulator_digital_replace('bpsk31'); - break; case "rtty": - demodulator_digital_replace('rtty'); - break; case "ft8": - demodulator_digital_replace('ft8'); + case "wspr": + demodulator_digital_replace(sdm); break; } } diff --git a/owrx/feature.py b/owrx/feature.py index 9009e1355..cd67f628c 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -165,13 +165,15 @@ def check_digiham_version(command): return version >= required_version except FileNotFoundError: return False - return reduce(and_, - map( - check_digiham_version, - ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator", - "digitalvoice_filter"] - ), - True) + return reduce( + and_, + map( + check_digiham_version, + ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator", + "digitalvoice_filter"] + ), + True + ) def has_dsd(self): """ @@ -201,4 +203,11 @@ def has_wsjtx(self): [WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions on how to build from source. """ - return self.command_is_runnable("jt9") + return reduce( + and_, + map( + self.command_is_runnable, + ["jt9", "wsprd"] + ), + True + ) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index fc07b1014..5d0c447cd 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) -class Ft8Chopper(threading.Thread): +class WsjtChopper(threading.Thread): def __init__(self, source): self.source = source self.tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"] @@ -27,7 +27,7 @@ def __init__(self, source): super().__init__() def getWaveFile(self): - filename = "{tmp_dir}/openwebrx-ft8chopper-{id}-{timestamp}.wav".format( + filename = "{tmp_dir}/openwebrx-wsjtchopper-{id}-{timestamp}.wav".format( tmp_dir = self.tmp_dir, id = id(self), timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") @@ -40,11 +40,10 @@ def getWaveFile(self): def getNextDecodingTime(self): t = datetime.now() - seconds = (int(t.second / 15) + 1) * 15 - if seconds >= 60: - t = t + timedelta(minutes = 1) - seconds = 0 - t = t.replace(second = seconds, microsecond = 0) + zeroed = t.replace(minute=0, second=0, microsecond=0) + delta = t - zeroed + seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval + t = zeroed + timedelta(seconds = seconds) logger.debug("scheduling: {0}".format(t)) return t.timestamp() @@ -70,10 +69,15 @@ def switchFiles(self): self.fileQueue.append(filename) self._scheduleNextSwitch() + def decoder_commandline(self, file): + ''' + must be overridden in child classes + ''' + return [] + def decode(self): def decode_and_unlink(file): - #TODO expose decoding quality parameters through config - decoder = subprocess.Popen(["jt9", "--ft8", "-d", "3", file], stdout=subprocess.PIPE, cwd=self.tmp_dir) + decoder = subprocess.Popen(self.decoder_commandline(file), stdout=subprocess.PIPE, cwd=self.tmp_dir) while True: line = decoder.stdout.readline() if line is None or (isinstance(line, bytes) and len(line) == 0): @@ -91,12 +95,12 @@ def decode_and_unlink(file): threading.Thread(target=decode_and_unlink, args=[file]).start() def run(self) -> None: - logger.debug("FT8 chopper starting up") + logger.debug("WSJT chopper starting up") self.startScheduler() while self.doRun: data = self.source.read(256) if data is None or (isinstance(data, bytes) and len(data) == 0): - logger.warning("zero read on ft8 chopper") + logger.warning("zero read on WSJT chopper") self.doRun = False else: self.switchingLock.acquire() @@ -104,7 +108,7 @@ def run(self) -> None: self.switchingLock.release() self.decode() - logger.debug("FT8 chopper shutting down") + logger.debug("WSJT chopper shutting down") self.outputReader.close() self.outputWriter.close() self.emptyScheduler() @@ -120,10 +124,34 @@ def read(self): return None +class Ft8Chopper(WsjtChopper): + def __init__(self, source): + self.interval = 15 + super().__init__(source) + + def decoder_commandline(self, file): + #TODO expose decoding quality parameters through config + return ["jt9", "--ft8", "-d", "3", file] + + +class WsprChopper(WsjtChopper): + def __init__(self, source): + self.interval = 120 + super().__init__(source) + + def decoder_commandline(self, file): + #TODO expose decoding quality parameters through config + return ["wsprd", "-d", file] + + class WsjtParser(object): + locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") + jt9_pattern = re.compile("^[0-9]{6} .*") + wspr_pattern = re.compile("^[0-9]{4} .*") + wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-90]+)") + def __init__(self, handler): self.handler = handler - self.locator_pattern = re.compile(".*\s([A-Z0-9]+)\s([A-R]{2}[0-9]{2})$") modes = { "~": "FT8" @@ -132,8 +160,6 @@ def __init__(self, handler): def parse(self, data): try: msg = data.decode().rstrip() - # sample - # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' # known debug messages we know to skip if msg.startswith(""): return @@ -141,23 +167,33 @@ def parse(self, data): return out = {} - ts = datetime.strptime(msg[0:6], "%H%M%S") - out["timestamp"] = int(datetime.combine(date.today(), ts.time(), datetime.now().tzinfo).timestamp() * 1000) - out["db"] = float(msg[7:10]) - out["dt"] = float(msg[11:15]) - out["freq"] = int(msg[16:20]) - modeChar = msg[21:22] - out["mode"] = mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" - wsjt_msg = msg[24:60].strip() - self.parseLocator(wsjt_msg, mode) - out["msg"] = wsjt_msg + if WsjtParser.jt9_pattern.match(msg): + out = self.parse_from_jt9(msg) + elif WsjtParser.wspr_pattern.match(msg): + out = self.parse_from_wsprd(msg) self.handler.write_wsjt_message(out) except ValueError: logger.exception("error while parsing wsjt message") + def parse_from_jt9(self, msg): + # ft8 sample + # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' + out = {} + ts = datetime.strptime(msg[0:6], "%H%M%S") + out["timestamp"] = int(datetime.combine(date.today(), ts.time(), datetime.now().tzinfo).timestamp() * 1000) + out["db"] = float(msg[7:10]) + out["dt"] = float(msg[11:15]) + out["freq"] = int(msg[16:20]) + modeChar = msg[21:22] + out["mode"] = mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" + wsjt_msg = msg[24:60].strip() + self.parseLocator(wsjt_msg, mode) + out["msg"] = wsjt_msg + return out + def parseLocator(self, msg, mode): - m = self.locator_pattern.match(msg) + m = WsjtParser.locator_pattern.match(msg) if m is None: return # this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very @@ -165,3 +201,26 @@ def parseLocator(self, msg, mode): if m.group(2) == "RR73": return Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2)), mode) + + def parse_from_wsprd(self, msg): + # wspr sample + # '2600 -24 0.4 0.001492 -1 G8AXA JO01 33' + out = {} + now = datetime.now() + ts = datetime.strptime(msg[0:4], "%M%S").replace(hour=now.hour) + out["timestamp"] = int(datetime.combine(date.today(), ts.time(), now.tzinfo).timestamp() * 1000) + out["db"] = float(msg[5:8]) + out["dt"] = float(msg[9:13]) + out["freq"] = float(msg[14:24]) + out["drift"] = int(msg[25:28]) + out["mode"] = "wspr" + wsjt_msg = msg[29:60].strip() + out["msg"] = wsjt_msg + self.parseWsprMessage(wsjt_msg) + return out + + def parseWsprMessage(self, msg): + m = WsjtParser.wspr_splitter_pattern.match(msg) + if m is None: + return + Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2)), "WSPR") From a6f294f36187058c1993d9e8226a05603d88858e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 13 Jul 2019 21:51:30 +0000 Subject: [PATCH 0249/2616] lib64 hack only if lib64 exists --- docker/scripts/install-dependencies.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 46d698c1c..2c7883e4e 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -68,8 +68,10 @@ rm -rf csdr git clone https://github.com/szechyjs/mbelib.git cmakebuild mbelib -# no idea why it's put into there now. alpine does not handle it correctly, so move it. -mv /usr/local/lib64/libmbe* /usr/local/lib +if [ -d "/usr/local/lib64" ]; then + # no idea why it's put into there now. alpine does not handle it correctly, so move it. + mv /usr/local/lib64/libmbe* /usr/local/lib +fi git clone https://github.com/jketterl/digiham.git cmakebuild digiham From 69c3a6379438b30894ba2170c75a852352f1157c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 14 Jul 2019 14:33:30 +0200 Subject: [PATCH 0250/2616] link the map in wpsr messages, too --- htdocs/openwebrx.js | 21 ++++++++++++++++++--- owrx/wsjt.py | 6 +++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index c47786e98..29520576d 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1376,14 +1376,29 @@ function update_metadata(meta) { } +function html_escape(input) { + return $('
    ').text(input).html() +} + function update_wsjt_panel(msg) { var $b = $('#openwebrx-panel-wsjt-message tbody'); var t = new Date(msg['timestamp']); var pad = function(i) { return ('' + i).padStart(2, "0"); } var linkedmsg = msg['msg']; - var matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); - if (matches && matches[2] != 'RR73') { - linkedmsg = matches[1] + '' + matches[2] + ''; + if (msg['mode'] == 'FT8') { + var matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); + if (matches && matches[2] != 'RR73') { + linkedmsg = html_escape(matches[1]) + '' + matches[2] + ''; + } else { + linkedmsg = html_escape(linkedmsg); + } + } else if (msg['mode'] == 'WSPR') { + var matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/); + if (matches) { + linkedmsg = html_escape(matches[1]) + '' + matches[2] + '' + html_escape(matches[3]); + } else { + linkedmsg = html_escape(linkedmsg); + } } $b.append($( '
    ' + diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 5d0c447cd..925b3b0bb 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -148,7 +148,7 @@ class WsjtParser(object): locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") jt9_pattern = re.compile("^[0-9]{6} .*") wspr_pattern = re.compile("^[0-9]{4} .*") - wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-90]+)") + wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") def __init__(self, handler): self.handler = handler @@ -213,8 +213,8 @@ def parse_from_wsprd(self, msg): out["dt"] = float(msg[9:13]) out["freq"] = float(msg[14:24]) out["drift"] = int(msg[25:28]) - out["mode"] = "wspr" - wsjt_msg = msg[29:60].strip() + out["mode"] = "WSPR" + wsjt_msg = msg[29:].strip() out["msg"] = wsjt_msg self.parseWsprMessage(wsjt_msg) return out From 30b46c4cdd939586d0f9341158a813d8cd2f7880 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 14 Jul 2019 14:43:44 +0200 Subject: [PATCH 0251/2616] allocate more space to the freq column --- htdocs/css/openwebrx.css | 6 +++++- htdocs/index.html | 2 +- htdocs/openwebrx.js | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 87b060895..a251a3ab1 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -827,7 +827,7 @@ img.openwebrx-mirror-img } #openwebrx-panel-wsjt-message .message { - width: 400px; + width: 380px; } #openwebrx-panel-wsjt-message .decimal { @@ -835,6 +835,10 @@ img.openwebrx-mirror-img width: 35px; } +#openwebrx-panel-wsjt-message .decimal.freq { + width: 70px; +} + #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container { diff --git a/htdocs/index.html b/htdocs/index.html index 45e94e42a..8c9aa50d6 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -147,7 +147,7 @@ - + diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 29520576d..92905cf35 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1405,7 +1405,7 @@ function update_wsjt_panel(msg) { '' + '' + '' + - '' + + '' + '' + '' )); From 7dcfead84317e396f50cd456c3cdc7c410752722 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 14 Jul 2019 17:09:34 +0200 Subject: [PATCH 0252/2616] let's try to implement jt65 and jt9 as well --- csdr.py | 8 +++++-- htdocs/css/openwebrx.css | 18 ++++++++------- htdocs/index.html | 2 ++ htdocs/openwebrx.js | 8 +++++-- owrx/wsjt.py | 47 ++++++++++++++++++++++++++++++++-------- 5 files changed, 62 insertions(+), 21 deletions(-) diff --git a/csdr.py b/csdr.py index d9452c0a2..05b8973fc 100755 --- a/csdr.py +++ b/csdr.py @@ -25,7 +25,7 @@ import signal import threading from functools import partial -from owrx.wsjt import Ft8Chopper, WsprChopper +from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper import logging logger = logging.getLogger(__name__) @@ -277,6 +277,10 @@ def start_secondary_demodulator(self): chopper = Ft8Chopper(self.secondary_process_demod.stdout) elif smd == "wspr": chopper = WsprChopper(self.secondary_process_demod.stdout) + elif smd == "jt65": + chopper = Jt65Chopper(self.secondary_process_demod.stdout) + elif smd == "jt9": + chopper = Jt9Chopper(self.secondary_process_demod.stdout) chopper.start() self.output.add_output("wsjt_demod", chopper.read) else: @@ -371,7 +375,7 @@ def isDigitalVoice(self, demodulator = None): def isWsjtMode(self, demodulator = None): if demodulator is None: demodulator = self.get_secondary_demodulator() - return demodulator in ["ft8", "wspr"] + return demodulator in ["ft8", "wspr", "jt65", "jt9"] def set_output_rate(self,output_rate): self.output_rate=output_rate diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index a251a3ab1..47cf9cfc2 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -840,20 +840,22 @@ img.openwebrx-mirror-img } #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container, -#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container +#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel { display: none; } #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container, -#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container +#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container, +#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-canvas-container, +#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container { height: 200px; margin: -10px; } - -#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, -#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel -{ - display: none; -} \ No newline at end of file diff --git a/htdocs/index.html b/htdocs/index.html index 8c9aa50d6..854236f93 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -82,6 +82,8 @@ + +
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 92905cf35..4d184bf70 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1385,7 +1385,7 @@ function update_wsjt_panel(msg) { var t = new Date(msg['timestamp']); var pad = function(i) { return ('' + i).padStart(2, "0"); } var linkedmsg = msg['msg']; - if (msg['mode'] == 'FT8') { + if (['FT8', 'JT65', 'JT9'].indexOf(msg['mode']) >= 0) { var matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); if (matches && matches[2] != 'RR73') { linkedmsg = html_escape(matches[1]) + '' + matches[2] + ''; @@ -2709,6 +2709,8 @@ function demodulator_digital_replace(subtype) case "rtty": case "ft8": case "wspr": + case "jt65": + case "jt9": secondary_demod_start(subtype); demodulator_analog_replace('usb', true); demodulator_buttons_update(); @@ -2716,7 +2718,7 @@ function demodulator_digital_replace(subtype) } $('#openwebrx-panel-digimodes').attr('data-mode', subtype); toggle_panel("openwebrx-panel-digimodes", true); - toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr'].indexOf(subtype) >= 0); + toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9'].indexOf(subtype) >= 0); } function secondary_demod_create_canvas() @@ -2888,6 +2890,8 @@ function secondary_demod_listbox_changed() case "rtty": case "ft8": case "wspr": + case "jt65": + case "jt9": demodulator_digital_replace(sdm); break; } diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 925b3b0bb..75188ac36 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -144,9 +144,29 @@ def decoder_commandline(self, file): return ["wsprd", "-d", file] +class Jt65Chopper(WsjtChopper): + def __init__(self, source): + self.interval = 60 + super().__init__(source) + + def decoder_commandline(self, file): + #TODO expose decoding quality parameters through config + return ["jt9", "--jt65", "-d", "3", file] + + +class Jt9Chopper(WsjtChopper): + def __init__(self, source): + self.interval = 60 + super().__init__(source) + + def decoder_commandline(self, file): + #TODO expose decoding quality parameters through config + return ["jt9", "--jt9", "-d", "3", file] + + class WsjtParser(object): locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") - jt9_pattern = re.compile("^[0-9]{6} .*") + jt9_pattern = re.compile("^([0-9]{6}|\\*{4}) .*") wspr_pattern = re.compile("^[0-9]{4} .*") wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") @@ -154,7 +174,9 @@ def __init__(self, handler): self.handler = handler modes = { - "~": "FT8" + "~": "FT8", + "#": "JT65", + "@": "JT9" } def parse(self, data): @@ -179,15 +201,22 @@ def parse(self, data): def parse_from_jt9(self, msg): # ft8 sample # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' + # jt65 sample + # '**** -10 0.4 1556 # CQ RN6AM KN95' out = {} - ts = datetime.strptime(msg[0:6], "%H%M%S") - out["timestamp"] = int(datetime.combine(date.today(), ts.time(), datetime.now().tzinfo).timestamp() * 1000) - out["db"] = float(msg[7:10]) - out["dt"] = float(msg[11:15]) - out["freq"] = int(msg[16:20]) - modeChar = msg[21:22] + if msg.startswith("****"): + out["timestamp"] = int(datetime.now().timestamp() * 1000) + msg = msg[5:] + else: + ts = datetime.strptime(msg[0:6], "%H%M%S") + out["timestamp"] = int(datetime.combine(date.today(), ts.time(), datetime.now().tzinfo).timestamp() * 1000) + msg = msg[7:] + out["db"] = float(msg[0:3]) + out["dt"] = float(msg[4:8]) + out["freq"] = int(msg[9:13]) + modeChar = msg[14:15] out["mode"] = mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" - wsjt_msg = msg[24:60].strip() + wsjt_msg = msg[17:53].strip() self.parseLocator(wsjt_msg, mode) out["msg"] = wsjt_msg return out From c94331bf24ac3ce50ae20f7ffeaab327d937c55e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 14 Jul 2019 18:22:02 +0200 Subject: [PATCH 0253/2616] hide modes if not available --- htdocs/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index 854236f93..7e9f38061 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -80,10 +80,10 @@
    From a15341fdcf59af199241c1be36d991b05c00c235 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 14 Jul 2019 19:32:48 +0200 Subject: [PATCH 0254/2616] detect and pass band information to the map --- bands.json | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++ htdocs/map.js | 9 +++-- owrx/bands.py | 32 +++++++++++++++++ owrx/map.py | 11 +++--- owrx/source.py | 8 +++-- owrx/wsjt.py | 11 ++++-- 6 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 bands.json create mode 100644 owrx/bands.py diff --git a/bands.json b/bands.json new file mode 100644 index 000000000..3a6939a61 --- /dev/null +++ b/bands.json @@ -0,0 +1,97 @@ +[ + { + "name": "160m", + "lower_bound": 1810000, + "upper_bound": 2000000 + }, + { + "name": "80m", + "lower_bound": 3500000, + "upper_bound": 3800000 + }, + { + "name": "60m", + "lower_bound": 5351500, + "upper_bound": 3566500 + }, + { + "name": "40m", + "lower_bound": 7000000, + "upper_bound": 7200000 + }, + { + "name": "30m", + "lower_bound": 10100000, + "upper_bound": 10150000 + }, + { + "name": "20m", + "lower_bound": 14000000, + "upper_bound": 14350000 + }, + { + "name": "17m", + "lower_bound": 18068000, + "upper_bound": 18168000 + }, + { + "name": "15m", + "lower_bound": 21000000, + "upper_bound": 21450000 + }, + { + "name": "12m", + "lower_bound": 24890000, + "upper_bound": 24990000 + }, + { + "name": "10m", + "lower_bound": 28000000, + "upper_bound": 29700000 + }, + { + "name": "6m", + "lower_bound": 50030000, + "upper_bound": 51000000 + }, + { + "name": "4m", + "lower_bound": 70150000, + "upper_bound": 70200000 + }, + { + "name": "2m", + "lower_bound": 144000000, + "upper_bound": 146000000 + }, + { + "name": "70cm", + "lower_bound": 430000000, + "upper_bound": 440000000 + }, + { + "name": "23cm", + "lower_bound": 1240000000, + "upper_bound": 1300000000 + }, + { + "name": "13cm", + "lower_bound": 2320000000, + "upper_bound": 2450000000 + }, + { + "name": "9cm", + "lower_bound": 3400000000, + "upper_bound": 3475000000 + }, + { + "name": "6cm", + "lower_bound": 5650000000, + "upper_bound": 5850000000 + }, + { + "name": "3cm", + "lower_bound": 10000000000, + "upper_bound": 10500000000 + } +] \ No newline at end of file diff --git a/htdocs/map.js b/htdocs/map.js index b5f905aa9..6af68ca3e 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -58,6 +58,7 @@ }, getMarkerOpacityOptions(update.lastseen) )); marker.lastseen = update.lastseen; marker.mode = update.mode; + marker.band = update.band; // TODO the trim should happen on the server side if (expectedCallsign && expectedCallsign == update.callsign.trim()) { @@ -96,6 +97,7 @@ rectangle.lastseen = update.lastseen; rectangle.locator = update.location.locator; rectangle.mode = update.mode; + rectangle.band = update.band; if (expectedLocator && expectedLocator == update.location.locator) { map.panTo(center); @@ -194,7 +196,7 @@ var showLocatorInfoWindow = function(locator, pos) { if (!infowindow) infowindow = new google.maps.InfoWindow(); var inLocator = $.map(rectangles, function(r, callsign) { - return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode} + return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band} }).filter(function(d) { return d.locator == locator; }).sort(function(a, b){ @@ -206,7 +208,10 @@ '
      ' + inLocator.map(function(i){ var timestring = moment(i.lastseen).fromNow(); - return '
    • ' + i.callsign + ' (' + timestring + ' using ' + i.mode + ')
    • ' + var message = i.callsign + ' (' + timestring + ' using ' + i.mode; + if (i.band) message += ' on ' + i.band; + message += ')'; + return '
    • ' + message + '
    • ' }).join("") + '
    ' ); diff --git a/owrx/bands.py b/owrx/bands.py new file mode 100644 index 000000000..c2ed79eee --- /dev/null +++ b/owrx/bands.py @@ -0,0 +1,32 @@ +import json + + +class Band(object): + def __init__(self, dict): + self.name = dict["name"] + self.lower_bound = dict["lower_bound"] + self.upper_bound = dict["upper_bound"] + + def inBand(self, freq): + return self.lower_bound <= freq <= self.upper_bound + + def getName(self): + return self.name + + +class Bandplan(object): + sharedInstance = None + @staticmethod + def getSharedInstance(): + if Bandplan.sharedInstance is None: + Bandplan.sharedInstance = Bandplan() + return Bandplan.sharedInstance + + def __init__(self): + f = open("bands.json", "r") + bands_json = json.load(f) + f.close() + self.bands = [Band(d) for d in bands_json] + + def findBand(self, freq): + return next(band for band in self.bands if band.inBand(freq)) diff --git a/owrx/map.py b/owrx/map.py index 4d65a4927..a6adbb67c 100644 --- a/owrx/map.py +++ b/owrx/map.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta import threading, time from owrx.config import PropertyManager +from owrx.bands import Band import logging logger = logging.getLogger(__name__) @@ -45,7 +46,8 @@ def addClient(self, client): "callsign": callsign, "location": record["location"].__dict__(), "lastseen": record["updated"].timestamp() * 1000, - "mode" : record["mode"] + "mode" : record["mode"], + "band" : record["band"].getName() if record["band"] is not None else None } for (callsign, record) in self.positions.items() ]) @@ -56,15 +58,16 @@ def removeClient(self, client): except ValueError: pass - def updateLocation(self, callsign, loc: Location, mode: str): + def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None): ts = datetime.now() - self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode} + self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band} self.broadcast([ { "callsign": callsign, "location": loc.__dict__(), "lastseen": ts.timestamp() * 1000, - "mode" : mode + "mode" : mode, + "band" : band.getName() if band is not None else None } ]) diff --git a/owrx/source.py b/owrx/source.py index 7a37ef645..2e2bca796 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -353,7 +353,7 @@ def __init__(self, handler, sdrSource): self.localProps = self.sdrSource.getProps().collect( "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality", - "dmr_filter", "temporary_directory" + "dmr_filter", "temporary_directory", "center_freq" ).defaults(PropertyManager.getSharedInstance()) self.dsp = csdr.dsp(self) @@ -369,6 +369,9 @@ def set_high_cut(cut): bpf[1] = cut self.dsp.set_bpf(*bpf) + def set_dial_freq(key, value): + self.wsjtParser.setDialFrequency(self.localProps["center_freq"] + self.localProps["offset_freq"]) + self.subscriptions = [ self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression), self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression), @@ -382,7 +385,8 @@ def set_high_cut(cut): self.localProps.getProperty("mod").wire(self.dsp.set_demodulator), self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality), self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter), - self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory) + self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory), + self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq) ] self.dsp.set_offset_freq(0) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 75188ac36..df3eda97d 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -9,6 +9,7 @@ from owrx.map import Map, LocatorLocation import re from owrx.config import PropertyManager +from owrx.bands import Bandplan import logging logger = logging.getLogger(__name__) @@ -172,6 +173,8 @@ class WsjtParser(object): def __init__(self, handler): self.handler = handler + self.dial_freq = None + self.band = None modes = { "~": "FT8", @@ -229,7 +232,7 @@ def parseLocator(self, msg, mode): # likely this just means roger roger goodbye. if m.group(2) == "RR73": return - Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2)), mode) + Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2)), mode, self.band) def parse_from_wsprd(self, msg): # wspr sample @@ -252,4 +255,8 @@ def parseWsprMessage(self, msg): m = WsjtParser.wspr_splitter_pattern.match(msg) if m is None: return - Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2)), "WSPR") + Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2)), "WSPR", self.band) + + def setDialFrequency(self, freq): + self.dial_freq = freq + self.band = Bandplan.getSharedInstance().findBand(freq) From f1098801e24feb4ad16ed0d22877c27a51c32c6c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 15 Jul 2019 21:35:39 +0200 Subject: [PATCH 0255/2616] let's try to avoid browser problems --- htdocs/openwebrx.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 4d184bf70..b199eec7b 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1418,11 +1418,11 @@ var wsjt_removal_interval; function init_wsjt_removal_timer() { if (wsjt_removal_interval) clearInterval(wsjt_removal_interval); setInterval(function(){ - // let's keep 2 hours that should be plenty for most users - var cutoff = new Date().getTime()- 2 * 60 * 60 * 1000; - $('#openwebrx-panel-wsjt-message tbody tr').filter(function(_, e){ - return $(e).data('timestamp') < cutoff; - }).remove(); + var $elements = $('#openwebrx-panel-wsjt-message tbody tr'); + // limit to 1000 entries in the list since browsers get laggy at some point + var toRemove = $elements.length - 1000; + if (toRemove <= 0) return; + $elements.slice(0, toRemove).remove(); }, 15000); } From 4493f369ddb54c1f284a47483bf162d65115ad7a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 19 Jul 2019 17:01:50 +0200 Subject: [PATCH 0256/2616] enable 64-bit frames for large amounts of data --- owrx/websocket.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/owrx/websocket.py b/owrx/websocket.py index a247b2a1f..360f28b5f 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -23,10 +23,31 @@ def __init__(self, handler, messageHandler): def get_header(self, size, opcode): ws_first_byte = 0b10000000 | (opcode & 0x0F) - if (size > 125): - return bytes([ws_first_byte, 126, (size>>8) & 0xff, size & 0xff]) + if (size > 2**16 - 1): + # frame size can be increased up to 2^64 by setting the size to 127 + # anything beyond that would need to be segmented into frames. i don't really think we'll need more. + return bytes([ + ws_first_byte, + 127, + (size >> 56) & 0xff, + (size >> 48) & 0xff, + (size >> 40) & 0xff, + (size >> 32) & 0xff, + (size >> 24) & 0xff, + (size >> 16) & 0xff, + (size >> 8) & 0xff, + size & 0xff + ]) + elif (size > 125): + # up to 2^16 can be sent using the extended payload size field by putting the size to 126 + return bytes([ + ws_first_byte, + 126, + (size >> 8) & 0xff, + size & 0xff + ]) else: - # 256 bytes binary message in a single unmasked frame + # 125 bytes binary message in a single unmasked frame return bytes([ws_first_byte, size]) def send(self, data): From a7a032dc8ff4e61d0eba9bd98d842bebcd2087e4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 19 Jul 2019 21:16:16 +0200 Subject: [PATCH 0257/2616] this goes in there --- htdocs/openwebrx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index b199eec7b..eee6175c8 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1417,7 +1417,7 @@ var wsjt_removal_interval; // remove old wsjt messages in fixed intervals function init_wsjt_removal_timer() { if (wsjt_removal_interval) clearInterval(wsjt_removal_interval); - setInterval(function(){ + wsjt_removal_interval = setInterval(function(){ var $elements = $('#openwebrx-panel-wsjt-message tbody tr'); // limit to 1000 entries in the list since browsers get laggy at some point var toRemove = $elements.length - 1000; From a1856482ff579c00f626e072644eae162685b726 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 19 Jul 2019 22:41:51 +0200 Subject: [PATCH 0258/2616] add dial frequencies --- bands.json | 106 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 93 insertions(+), 13 deletions(-) diff --git a/bands.json b/bands.json index 3a6939a61..971ab90e1 100644 --- a/bands.json +++ b/bands.json @@ -2,67 +2,147 @@ { "name": "160m", "lower_bound": 1810000, - "upper_bound": 2000000 + "upper_bound": 2000000, + "frequencies": { + "psk31": 1838000, + "ft8": 1840000, + "wspr": 1836600, + "jt65": 1838000, + "jt9": 1839000 + } }, { "name": "80m", "lower_bound": 3500000, - "upper_bound": 3800000 + "upper_bound": 3800000, + "frequencies": { + "psk31": 3580000, + "ft8": 3573000, + "wspr": 3592600, + "jt65": 3570000, + "jt9": 3572000 + } }, { "name": "60m", "lower_bound": 5351500, - "upper_bound": 3566500 + "upper_bound": 3566500, + "frequencies": { + "ft8": 5357000, + "wspr": 5287200 + } }, { "name": "40m", "lower_bound": 7000000, - "upper_bound": 7200000 + "upper_bound": 7200000, + "frequencies": { + "psk31": 7040000, + "ft8": 7074000, + "wspr": 7038600, + "jt65": 7076000, + "jt9": 7078000 + } }, { "name": "30m", "lower_bound": 10100000, - "upper_bound": 10150000 + "upper_bound": 10150000, + "frequencies": { + "psk31": 10141000, + "ft8": 10136000, + "wspr": 10138700, + "jt65": 10138000, + "jt9": 10140000 + } }, { "name": "20m", "lower_bound": 14000000, - "upper_bound": 14350000 + "upper_bound": 14350000, + "frequencies": { + "psk31": 14070000, + "ft8": 14074000, + "wspr": 14095600, + "jt65": 14076000, + "jt9": 14078000 + } }, { "name": "17m", "lower_bound": 18068000, - "upper_bound": 18168000 + "upper_bound": 18168000, + "frequencies": { + "psk31": 18098000, + "ft8": 18100000, + "wspr": 18104600, + "jt65": 18102000, + "jt9": 18104000 + } }, { "name": "15m", "lower_bound": 21000000, - "upper_bound": 21450000 + "upper_bound": 21450000, + "frequencies": { + "psk31": 21070000, + "ft8": 21074000, + "wspr": 21094600, + "jt65": 21076000, + "jt9": 21078000 + } }, { "name": "12m", "lower_bound": 24890000, - "upper_bound": 24990000 + "upper_bound": 24990000, + "frequencies": { + "psk31": 24920000, + "ft8": 24915000, + "wspr": 24924600, + "jt65": 24917000, + "jt9": 24919000 + } }, { "name": "10m", "lower_bound": 28000000, - "upper_bound": 29700000 + "upper_bound": 29700000, + "frequencies": { + "psk31": [28070000, 28120000], + "ft8": 28074000, + "wspr": 28124600, + "jt65": 28076000, + "jt9": 28078000 + } }, { "name": "6m", "lower_bound": 50030000, - "upper_bound": 51000000 + "upper_bound": 51000000, + "frequencies": { + "psk31": 50305000, + "ft8": 50313000, + "wspr": 50293000, + "jt65": 50310000, + "jt9": 50312000 + } }, { "name": "4m", "lower_bound": 70150000, - "upper_bound": 70200000 + "upper_bound": 70200000, + "frequencies": { + "wspr": 70091000 + } }, { "name": "2m", "lower_bound": 144000000, - "upper_bound": 146000000 + "upper_bound": 146000000, + "frequencies": { + "wspr": 144489000 + } }, { "name": "70cm", From 6e08a428d643342794f8ebfb1e71f6e443287738 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 19 Jul 2019 23:15:10 +0200 Subject: [PATCH 0259/2616] import frequencies; fix band errors --- bands.json | 2 +- owrx/bands.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/bands.json b/bands.json index 971ab90e1..f8b6bb67d 100644 --- a/bands.json +++ b/bands.json @@ -26,7 +26,7 @@ { "name": "60m", "lower_bound": 5351500, - "upper_bound": 3566500, + "upper_bound": 5366500, "frequencies": { "ft8": 5357000, "wspr": 5287200 diff --git a/owrx/bands.py b/owrx/bands.py index c2ed79eee..47bd8577f 100644 --- a/owrx/bands.py +++ b/owrx/bands.py @@ -1,11 +1,24 @@ import json +import logging +logger = logging.getLogger(__name__) + class Band(object): def __init__(self, dict): self.name = dict["name"] self.lower_bound = dict["lower_bound"] self.upper_bound = dict["upper_bound"] + self.frequencies = [] + if "frequencies" in dict: + for (mode, freqs) in dict["frequencies"].items(): + if not isinstance(freqs, list): + freqs = [freqs] + for f in freqs: + if not self.inBand(f): + logger.warning("Frequency for {mode} on {band} is not within band limits: {frequency}".format(mode = mode, frequency = f, band = self.name)) + else: + self.frequencies.append([{"mode": mode, "frequency": f}]) def inBand(self, freq): return self.lower_bound <= freq <= self.upper_bound From abd5cf07954d4e1b7e88a2bb44cc9b56d3c072bc Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 19 Jul 2019 23:55:52 +0200 Subject: [PATCH 0260/2616] collect dial frequencies and send to client --- owrx/bands.py | 9 ++++++++- owrx/connection.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/owrx/bands.py b/owrx/bands.py index 47bd8577f..5971bf85c 100644 --- a/owrx/bands.py +++ b/owrx/bands.py @@ -18,7 +18,7 @@ def __init__(self, dict): if not self.inBand(f): logger.warning("Frequency for {mode} on {band} is not within band limits: {frequency}".format(mode = mode, frequency = f, band = self.name)) else: - self.frequencies.append([{"mode": mode, "frequency": f}]) + self.frequencies.append({"mode": mode, "frequency": f}) def inBand(self, freq): return self.lower_bound <= freq <= self.upper_bound @@ -26,6 +26,10 @@ def inBand(self, freq): def getName(self): return self.name + def getDialFrequencies(self, range): + (low, hi) = range + return [e for e in self.frequencies if low <= e["frequency"] <= hi] + class Bandplan(object): sharedInstance = None @@ -43,3 +47,6 @@ def __init__(self): def findBand(self, freq): return next(band for band in self.bands if band.inBand(freq)) + + def collectDialFrequencis(self, range): + return [e for b in self.bands for e in b.getDialFrequencies(range)] diff --git a/owrx/connection.py b/owrx/connection.py index d83dacd1b..17551ff7f 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -2,6 +2,7 @@ from owrx.source import DspManager, CpuUsageThread, SdrService, ClientRegistry from owrx.feature import FeatureDetector from owrx.version import openwebrx_version +from owrx.bands import Bandplan import json from owrx.map import Map @@ -83,6 +84,12 @@ def sendConfig(key, value): config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"] self.write_config(config) + cf = configProps["center_freq"] + srh = configProps["samp_rate"] / 2 + frequencyRange = (cf - srh, cf + srh) + self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencis(frequencyRange)) + + self.configSub = configProps.wire(sendConfig) sendConfig(None, None) @@ -162,6 +169,9 @@ def write_metadata(self, metadata): def write_wsjt_message(self, message): self.protected_send({"type": "wsjt_message", "value": message}) + def write_dial_frequendies(self, frequencies): + self.protected_send({"type": "dial_frequencies", "value": frequencies}) + class MapConnection(Client): def __init__(self, conn): From 18b65f769fc5f041e29ce41fa0deae9947d1e935 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 20 Jul 2019 12:47:10 +0200 Subject: [PATCH 0261/2616] better timestamping and overhaul --- owrx/wsjt.py | 76 +++++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index df3eda97d..b0c8c4ba5 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -1,6 +1,6 @@ import threading import wave -from datetime import datetime, timedelta, date +from datetime import datetime, timedelta, date, timezone import time import sched import subprocess @@ -31,7 +31,7 @@ def getWaveFile(self): filename = "{tmp_dir}/openwebrx-wsjtchopper-{id}-{timestamp}.wav".format( tmp_dir = self.tmp_dir, id = id(self), - timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + timestamp = datetime.utcnow().strftime(self.fileTimestampFormat) ) wavefile = wave.open(filename, "wb") wavefile.setnchannels(1) @@ -71,9 +71,9 @@ def switchFiles(self): self._scheduleNextSwitch() def decoder_commandline(self, file): - ''' + """ must be overridden in child classes - ''' + """ return [] def decode(self): @@ -128,6 +128,7 @@ def read(self): class Ft8Chopper(WsjtChopper): def __init__(self, source): self.interval = 15 + self.fileTimestampFormat = "%y%m%d_%H%M%S" super().__init__(source) def decoder_commandline(self, file): @@ -138,6 +139,7 @@ def decoder_commandline(self, file): class WsprChopper(WsjtChopper): def __init__(self, source): self.interval = 120 + self.fileTimestampFormat = "%y%m%d_%H%M" super().__init__(source) def decoder_commandline(self, file): @@ -148,6 +150,7 @@ def decoder_commandline(self, file): class Jt65Chopper(WsjtChopper): def __init__(self, source): self.interval = 60 + self.fileTimestampFormat = "%y%m%d_%H%M" super().__init__(source) def decoder_commandline(self, file): @@ -158,6 +161,7 @@ def decoder_commandline(self, file): class Jt9Chopper(WsjtChopper): def __init__(self, source): self.interval = 60 + self.fileTimestampFormat = "%y%m%d_%H%M" super().__init__(source) def decoder_commandline(self, file): @@ -167,8 +171,6 @@ def decoder_commandline(self, file): class WsjtParser(object): locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") - jt9_pattern = re.compile("^([0-9]{6}|\\*{4}) .*") - wspr_pattern = re.compile("^[0-9]{4} .*") wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") def __init__(self, handler): @@ -191,38 +193,45 @@ def parse(self, data): if msg.startswith(" EOF on input file"): return - out = {} - if WsjtParser.jt9_pattern.match(msg): + modes = list(WsjtParser.modes.keys()) + if msg[21] in modes or msg[19] in modes: out = self.parse_from_jt9(msg) - elif WsjtParser.wspr_pattern.match(msg): + else: out = self.parse_from_wsprd(msg) self.handler.write_wsjt_message(out) except ValueError: logger.exception("error while parsing wsjt message") + def parse_timestamp(self, instring, dateformat): + ts = datetime.strptime(instring, dateformat).replace(tzinfo=timezone.utc) + return int(datetime.combine(date.today(), ts.time(), timezone.utc).timestamp() * 1000) + def parse_from_jt9(self, msg): # ft8 sample # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' # jt65 sample - # '**** -10 0.4 1556 # CQ RN6AM KN95' - out = {} - if msg.startswith("****"): - out["timestamp"] = int(datetime.now().timestamp() * 1000) - msg = msg[5:] + # '2352 -7 0.4 1801 # R0WAS R2ABM KO85' + # '0003 -4 0.4 1762 # CQ R2ABM KO85' + modes = list(WsjtParser.modes.keys()) + if msg[19] in modes: + dateformat = "%H%M" else: - ts = datetime.strptime(msg[0:6], "%H%M%S") - out["timestamp"] = int(datetime.combine(date.today(), ts.time(), datetime.now().tzinfo).timestamp() * 1000) - msg = msg[7:] - out["db"] = float(msg[0:3]) - out["dt"] = float(msg[4:8]) - out["freq"] = int(msg[9:13]) + dateformat = "%H%M%S" + timestamp = self.parse_timestamp(msg[0:len(dateformat)], dateformat) + msg = msg[len(dateformat) + 1:] modeChar = msg[14:15] - out["mode"] = mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" + mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" wsjt_msg = msg[17:53].strip() self.parseLocator(wsjt_msg, mode) - out["msg"] = wsjt_msg - return out + return { + "timestamp": timestamp, + "db": float(msg[0:3]), + "dt": float(msg[4:8]), + "freq": int(msg[9:13]), + "mode": mode, + "msg": wsjt_msg + } def parseLocator(self, msg, mode): m = WsjtParser.locator_pattern.match(msg) @@ -237,19 +246,18 @@ def parseLocator(self, msg, mode): def parse_from_wsprd(self, msg): # wspr sample # '2600 -24 0.4 0.001492 -1 G8AXA JO01 33' - out = {} - now = datetime.now() - ts = datetime.strptime(msg[0:4], "%M%S").replace(hour=now.hour) - out["timestamp"] = int(datetime.combine(date.today(), ts.time(), now.tzinfo).timestamp() * 1000) - out["db"] = float(msg[5:8]) - out["dt"] = float(msg[9:13]) - out["freq"] = float(msg[14:24]) - out["drift"] = int(msg[25:28]) - out["mode"] = "WSPR" + # '0052 -29 2.6 0.001486 0 G02CWT IO92 23' wsjt_msg = msg[29:].strip() - out["msg"] = wsjt_msg self.parseWsprMessage(wsjt_msg) - return out + return { + "timestamp": self.parse_timestamp(msg[0:4], "%H%M"), + "db": float(msg[5:8]), + "dt": float(msg[9:13]), + "freq": float(msg[14:24]), + "drift": int(msg[25:28]), + "mode": "WSPR", + "msg": wsjt_msg + } def parseWsprMessage(self, msg): m = WsjtParser.wspr_splitter_pattern.match(msg) From 25b0e86f09114db8e02a1db4e22c2bbd412a443b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 20 Jul 2019 13:38:25 +0200 Subject: [PATCH 0262/2616] add FT4 because why not --- csdr.py | 6 ++++-- htdocs/css/openwebrx.css | 7 +++++-- htdocs/index.html | 1 + htdocs/openwebrx.js | 6 ++++-- owrx/wsjt.py | 14 +++++++++++++- 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/csdr.py b/csdr.py index 05b8973fc..8b8edef45 100755 --- a/csdr.py +++ b/csdr.py @@ -25,7 +25,7 @@ import signal import threading from functools import partial -from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper +from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper import logging logger = logging.getLogger(__name__) @@ -281,6 +281,8 @@ def start_secondary_demodulator(self): chopper = Jt65Chopper(self.secondary_process_demod.stdout) elif smd == "jt9": chopper = Jt9Chopper(self.secondary_process_demod.stdout) + elif smd == "ft4": + chopper = Ft4Chopper(self.secondary_process_demod.stdout) chopper.start() self.output.add_output("wsjt_demod", chopper.read) else: @@ -375,7 +377,7 @@ def isDigitalVoice(self, demodulator = None): def isWsjtMode(self, demodulator = None): if demodulator is None: demodulator = self.get_secondary_demodulator() - return demodulator in ["ft8", "wspr", "jt65", "jt9"] + return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"] def set_output_rate(self,output_rate): self.output_rate=output_rate diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 47cf9cfc2..91e03e8a9 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -843,10 +843,12 @@ img.openwebrx-mirror-img #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel, -#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel +#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel { display: none; } @@ -854,7 +856,8 @@ img.openwebrx-mirror-img #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-canvas-container, -#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container +#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container, +#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container { height: 200px; margin: -10px; diff --git a/htdocs/index.html b/htdocs/index.html index 7e9f38061..e2b801078 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -84,6 +84,7 @@ +
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index eee6175c8..be79ee5e0 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1385,7 +1385,7 @@ function update_wsjt_panel(msg) { var t = new Date(msg['timestamp']); var pad = function(i) { return ('' + i).padStart(2, "0"); } var linkedmsg = msg['msg']; - if (['FT8', 'JT65', 'JT9'].indexOf(msg['mode']) >= 0) { + if (['FT8', 'JT65', 'JT9', 'FT4'].indexOf(msg['mode']) >= 0) { var matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); if (matches && matches[2] != 'RR73') { linkedmsg = html_escape(matches[1]) + '' + matches[2] + ''; @@ -2711,6 +2711,7 @@ function demodulator_digital_replace(subtype) case "wspr": case "jt65": case "jt9": + case "ft4": secondary_demod_start(subtype); demodulator_analog_replace('usb', true); demodulator_buttons_update(); @@ -2718,7 +2719,7 @@ function demodulator_digital_replace(subtype) } $('#openwebrx-panel-digimodes').attr('data-mode', subtype); toggle_panel("openwebrx-panel-digimodes", true); - toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9'].indexOf(subtype) >= 0); + toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(subtype) >= 0); } function secondary_demod_create_canvas() @@ -2892,6 +2893,7 @@ function secondary_demod_listbox_changed() case "wspr": case "jt65": case "jt9": + case "ft4": demodulator_digital_replace(sdm); break; } diff --git a/owrx/wsjt.py b/owrx/wsjt.py index b0c8c4ba5..0e89e1128 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -169,6 +169,17 @@ def decoder_commandline(self, file): return ["jt9", "--jt9", "-d", "3", file] +class Ft4Chopper(WsjtChopper): + def __init__(self, source): + self.interval = 7.5 + self.fileTimestampFormat = "%y%m%d_%H%M%S" + super().__init__(source) + + def decoder_commandline(self, file): + #TODO expose decoding quality parameters through config + return ["jt9", "--ft4", "-d", "3", file] + + class WsjtParser(object): locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") @@ -181,7 +192,8 @@ def __init__(self, handler): modes = { "~": "FT8", "#": "JT65", - "@": "JT9" + "@": "JT9", + "+": "FT4" } def parse(self, data): From f09f730bff88f3a0be0cc7999bb0660bf097190c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 20 Jul 2019 19:52:46 +0200 Subject: [PATCH 0263/2616] ft4 frequency for 20m (at least to my knowledge) --- bands.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bands.json b/bands.json index f8b6bb67d..487ef29a2 100644 --- a/bands.json +++ b/bands.json @@ -65,7 +65,8 @@ "ft8": 14074000, "wspr": 14095600, "jt65": 14076000, - "jt9": 14078000 + "jt9": 14078000, + "ft4": 14080000 } }, { From ea9feeefd2239c14ee15f1b3afa44af03ce62af0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 20 Jul 2019 19:53:42 +0200 Subject: [PATCH 0264/2616] complete dial frequency feature frontend --- htdocs/css/openwebrx.css | 16 +++++++++++++++- htdocs/index.html | 10 ++++++++++ htdocs/openwebrx.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 91e03e8a9..cd1498ba0 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -364,6 +364,20 @@ input[type=range]:focus::-ms-fill-upper text-align: center; } +.openwebrx-dial-button svg { + width: 19px; + height: 19px; + vertical-align: bottom; +} + +.openwebrx-dial-button #ph_dial { + fill: #888; +} + +.openwebrx-dial-button.available #ph_dial { + fill: #FFF; +} + .openwebrx-square-button img { height: 27px; @@ -602,7 +616,7 @@ img.openwebrx-mirror-img #openwebrx-secondary-demod-listbox { - width: 201px; + width: 174px; height: 27px; padding-left:3px; } diff --git a/htdocs/index.html b/htdocs/index.html index e2b801078..bcd410e75 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -86,6 +86,16 @@ +
    + + + + + + + + +
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index be79ee5e0..631b322ed 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1249,6 +1249,10 @@ function on_ws_recv(evt) case "wsjt_message": update_wsjt_panel(json.value); break; + case "dial_frequencies": + dial_frequencies = json.value; + update_dial_button(); + break; default: console.warn('received message of unknown type: ' + json.type); } @@ -1314,6 +1318,29 @@ function on_ws_recv(evt) } } +var dial_frequencies = []; + +function find_dial_frequencies() { + var sdm = $("#openwebrx-secondary-demod-listbox")[0].value; + return dial_frequencies.filter(function(d){ + return d.mode == sdm; + }); +} + +function update_dial_button() { + var available = find_dial_frequencies(); + $("#openwebrx-secondary-demod-dial-button")[available.length ? "addClass" : "removeClass"]("available"); +} + +function dial_button_click() { + var available = find_dial_frequencies(); + if (!available.length) return; + var frequency = available[0].frequency; + console.info(frequency); + demodulator_set_offset_frequency(0, frequency - center_freq); + $("#webrx-actual-freq").html(format_frequency("{x} MHz", frequency, 1e6, 4)); +} + function update_metadata(meta) { if (meta.protocol) switch (meta.protocol) { case 'DMR': @@ -2897,6 +2924,7 @@ function secondary_demod_listbox_changed() demodulator_digital_replace(sdm); break; } + update_dial_button(); } function secondary_demod_listbox_update() From 2fae8ffa705837a02cd563dbf4c8165722ca9e99 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 20 Jul 2019 20:45:13 +0200 Subject: [PATCH 0265/2616] remove some pointless stuff --- htdocs/index.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index bcd410e75..2db20eada 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -87,10 +87,7 @@
    - - - - + From 6900810f5d29e92919ce31a73ac1864417b7e1a9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 21 Jul 2019 13:07:38 +0100 Subject: [PATCH 0266/2616] modify so that it runs with python 3.5, too --- owrx/wsjt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 0e89e1128..fb97ac8c9 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -216,8 +216,8 @@ def parse(self, data): logger.exception("error while parsing wsjt message") def parse_timestamp(self, instring, dateformat): - ts = datetime.strptime(instring, dateformat).replace(tzinfo=timezone.utc) - return int(datetime.combine(date.today(), ts.time(), timezone.utc).timestamp() * 1000) + ts = datetime.strptime(instring, dateformat) + return int(datetime.combine(date.today(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000) def parse_from_jt9(self, msg): # ft8 sample From fc5abd38cc9d37e3214caa4b4ed44ad39e3415bb Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 21 Jul 2019 18:38:54 +0200 Subject: [PATCH 0267/2616] add information about wsjt-x --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 493f71c73..1b4833c42 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,13 @@ It has the following features: - Multiple SDR devices can be used simultaneously - [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF) - [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN) +- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9) + +**News (2019-07-21 by DD5JFK)** +- Latest Features: + - More WSJT-X have been added, including the new FT4 mode + - I started adding a bandplan feature, the first thing visible is the "dial" indicator that brings you right to the dial frequency for digital modes + - fixed some bugs in the websocket communication which broke the map **News (2019-07-13 by DD5JFK)** - Latest Features: @@ -77,9 +84,17 @@ Optional Dependencies if you want to be able to listen do digital voice: - [digiham](https://github.com/jketterl/digiham) - [dsd](https://github.com/f4exb/dsdcc) +Optional Dependency if you want to decode WSJT-X modes: + +- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) + After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server: python openwebrx.py + +You may need to specify the Python version explicitly if your distribution still defaults to Python 2: + + python3 openwebrx.py You can now open the GUI at http://localhost:8073. From 79062ff3d663c9953a4d0929995158e90cc70c5d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 21 Jul 2019 18:40:00 +0200 Subject: [PATCH 0268/2616] fix wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b4833c42..1df5b19b3 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ It has the following features: **News (2019-07-21 by DD5JFK)** - Latest Features: - - More WSJT-X have been added, including the new FT4 mode + - More WSJT-X modes have been added, including the new FT4 mode - I started adding a bandplan feature, the first thing visible is the "dial" indicator that brings you right to the dial frequency for digital modes - fixed some bugs in the websocket communication which broke the map From e15dc1ce11c92aa45592e17096566867f33bc982 Mon Sep 17 00:00:00 2001 From: D0han Date: Sun, 21 Jul 2019 19:40:28 +0200 Subject: [PATCH 0269/2616] Reformatted with black -l 120 -t py35 . --- config_webrx.py | 124 ++++++++------- csdr.py | 366 ++++++++++++++++++++++++-------------------- openwebrx.py | 19 ++- owrx/bands.py | 8 +- owrx/config.py | 20 ++- owrx/connection.py | 86 +++++++---- owrx/controllers.py | 45 ++++-- owrx/feature.py | 50 +++--- owrx/http.py | 25 ++- owrx/map.py | 56 +++---- owrx/meta.py | 30 ++-- owrx/sdrhu.py | 15 +- owrx/source.py | 175 +++++++++++++++------ owrx/version.py | 2 +- owrx/websocket.py | 85 +++++----- owrx/wsjt.py | 34 ++-- sdrhu.py | 3 +- 17 files changed, 681 insertions(+), 462 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index eaba80313..15569de5e 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -35,21 +35,21 @@ # https://github.com/simonyiszk/openwebrx/wiki # ==== Server settings ==== -web_port=8073 -max_clients=20 +web_port = 8073 +max_clients = 20 # ==== Web GUI configuration ==== -receiver_name="[Callsign]" -receiver_location="Budapest, Hungary" -receiver_qra="JN97ML" -receiver_asl=200 -receiver_ant="Longwire" -receiver_device="RTL-SDR" -receiver_admin="example@example.com" -receiver_gps=(47.000000,19.000000) -photo_height=350 -photo_title="Panorama of Budapest from Schönherz Zoltán Dormitory" -photo_desc=""" +receiver_name = "[Callsign]" +receiver_location = "Budapest, Hungary" +receiver_qra = "JN97ML" +receiver_asl = 200 +receiver_ant = "Longwire" +receiver_device = "RTL-SDR" +receiver_admin = "example@example.com" +receiver_gps = (47.000000, 19.000000) +photo_height = 350 +photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory" +photo_desc = """ You can add your own background photo and receiver information.
    Receiver is operated by: %[RX_ADMIN]
    Device: %[RX_DEVICE]
    @@ -64,18 +64,20 @@ sdrhu_key = "" # 3. Set this setting to True to enable listing: sdrhu_public_listing = False -server_hostname="localhost" +server_hostname = "localhost" # ==== DSP/RX settings ==== -fft_fps=9 -fft_size=4096 #Should be power of 2 -fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. +fft_fps = 9 +fft_size = 4096 # Should be power of 2 +fft_voverlap_factor = ( + 0.3 +) # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. -audio_compression="adpcm" #valid values: "adpcm", "none" -fft_compression="adpcm" #valid values: "adpcm", "none" +audio_compression = "adpcm" # valid values: "adpcm", "none" +fft_compression = "adpcm" # valid values: "adpcm", "none" -digimodes_enable=True #Decoding digimodes come with higher CPU usage. -digimodes_fft_size=1024 +digimodes_enable = True # Decoding digimodes come with higher CPU usage. +digimodes_fft_size = 1024 # determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes # if you're running on a Raspi (up to 3B+) you'll want to leave this on 1 @@ -116,7 +118,7 @@ "rf_gain": 30, "samp_rate": 2400000, "start_freq": 439275000, - "start_mod": "nfm" + "start_mod": "nfm", }, "2m": { "name": "2m komplett", @@ -124,9 +126,9 @@ "rf_gain": 30, "samp_rate": 2400000, "start_freq": 145725000, - "start_mod": "nfm" - } - } + "start_mod": "nfm", + }, + }, }, "sdrplay": { "name": "SDRPlay RSP2", @@ -134,39 +136,39 @@ "ppm": 0, "profiles": { "20m": { - "name":"20m", + "name": "20m", "center_freq": 14150000, "rf_gain": 4, "samp_rate": 500000, "start_freq": 14070000, "start_mod": "usb", - "antenna": "Antenna A" + "antenna": "Antenna A", }, "30m": { - "name":"30m", + "name": "30m", "center_freq": 10125000, "rf_gain": 4, "samp_rate": 250000, "start_freq": 10142000, - "start_mod": "usb" + "start_mod": "usb", }, "40m": { - "name":"40m", + "name": "40m", "center_freq": 7100000, "rf_gain": 4, "samp_rate": 500000, "start_freq": 7070000, "start_mod": "usb", - "antenna": "Antenna A" + "antenna": "Antenna A", }, "80m": { - "name":"80m", + "name": "80m", "center_freq": 3650000, "rf_gain": 4, "samp_rate": 500000, "start_freq": 3570000, "start_mod": "usb", - "antenna": "Antenna A" + "antenna": "Antenna A", }, "49m": { "name": "49m Broadcast", @@ -175,42 +177,43 @@ "samp_rate": 500000, "start_freq": 6070000, "start_mod": "am", - "antenna": "Antenna A" - } - } + "antenna": "Antenna A", + }, + }, }, # this one is just here to test feature detection - "test": { - "type": "test" - } + "test": {"type": "test"}, } # ==== Misc settings ==== client_audio_buffer_size = 5 -#increasing client_audio_buffer_size will: +# increasing client_audio_buffer_size will: # - also increase the latency # - decrease the chance of audio underruns -iq_port_range = [4950, 4960] #TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default. +iq_port_range = [ + 4950, + 4960, +] # TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default. # ==== Color themes ==== -#A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels +# A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels ### default theme by teejez: -waterfall_colors = [0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff] -waterfall_min_level = -88 #in dB +waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF] +waterfall_min_level = -88 # in dB waterfall_max_level = -20 waterfall_auto_level_margin = (5, 40) ### old theme by HA7ILM: -#waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" -#waterfall_min_level = -115 #in dB -#waterfall_max_level = 0 -#waterfall_auto_level_margin = (20, 30) +# waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" +# waterfall_min_level = -115 #in dB +# waterfall_max_level = 0 +# waterfall_auto_level_margin = (20, 30) ##For the old colors, you might also want to set [fft_voverlap_factor] to 0. -#Note: When the auto waterfall level button is clicked, the following happens: +# Note: When the auto waterfall level button is clicked, the following happens: # [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]] # [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]] # @@ -219,17 +222,26 @@ # current_max_power_level __| # 3D view settings -mathbox_waterfall_frequency_resolution = 128 #bins -mathbox_waterfall_history_length = 10 #seconds -mathbox_waterfall_colors = [0x000000ff,0x2e6893ff,0x69a5d0ff,0x214b69ff,0x9dc4e0ff,0xfff775ff,0xff8a8aff,0xb20000ff] +mathbox_waterfall_frequency_resolution = 128 # bins +mathbox_waterfall_history_length = 10 # seconds +mathbox_waterfall_colors = [ + 0x000000FF, + 0x2E6893FF, + 0x69A5D0FF, + 0x214B69FF, + 0x9DC4E0FF, + 0xFFF775FF, + 0xFF8A8AFF, + 0xB20000FF, +] # === Experimental settings === -#Warning! The settings below are very experimental. -csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr. +# Warning! The settings below are very experimental. +csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr. csdr_print_bufsizes = False # This prints the buffer sizes used for csdr processes. -csdr_through = False # Setting this True will print out how much data is going into the DSP chains. +csdr_through = False # Setting this True will print out how much data is going into the DSP chains. -nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux. +nmux_memory = 50 # in megabytes. This sets the approximate size of the circular buffer used by nmux. google_maps_api_key = "" diff --git a/csdr.py b/csdr.py index 8b8edef45..54fd13c09 100755 --- a/csdr.py +++ b/csdr.py @@ -28,26 +28,29 @@ from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper import logging + logger = logging.getLogger(__name__) + class output(object): def add_output(self, type, read_fn): pass + def reset(self): pass -class dsp(object): +class dsp(object): def __init__(self, output): self.samp_rate = 250000 - self.output_rate = 11025 #this is default, and cannot be set at the moment + self.output_rate = 11025 # this is default, and cannot be set at the moment self.fft_size = 1024 self.fft_fps = 5 self.offset_freq = 0 self.low_cut = -4000 self.high_cut = 4000 - self.bpf_transition_bw = 320 #Hz, and this is a constant - self.ddc_transition_bw_rate = 0.15 # of the IF sample rate + self.bpf_transition_bw = 320 # Hz, and this is a constant + self.ddc_transition_bw_rate = 0.15 # of the IF sample rate self.running = False self.secondary_processes_running = False self.audio_compression = "none" @@ -67,9 +70,17 @@ def __init__(self, output): self.secondary_fft_size = 1024 self.secondary_process_fft = None self.secondary_process_demod = None - self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", - "iqtee2_pipe", "dmr_control_pipe"] - self.secondary_pipe_names=["secondary_shift_pipe"] + self.pipe_names = [ + "bpf_pipe", + "shift_pipe", + "squelch_pipe", + "smeter_pipe", + "meta_pipe", + "iqtee_pipe", + "iqtee2_pipe", + "dmr_control_pipe", + ] + self.secondary_pipe_names = ["secondary_shift_pipe"] self.secondary_offset_freq = 1000 self.unvoiced_quality = 1 self.modification_lock = threading.Lock() @@ -79,15 +90,19 @@ def __init__(self, output): def set_temporary_directory(self, what): self.temporary_directory = what - def chain(self,which): + def chain(self, which): chain = ["nc -v 127.0.0.1 {nc_port}"] - if self.csdr_dynamic_bufsize: chain += ["csdr setbuf {start_bufsize}"] - if self.csdr_through: chain += ["csdr through"] + if self.csdr_dynamic_bufsize: + chain += ["csdr setbuf {start_bufsize}"] + if self.csdr_through: + chain += ["csdr through"] if which == "fft": chain += [ "csdr fft_cc {fft_size} {fft_block_size}", - "csdr logpower_cf -70" if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages}", - "csdr fft_exchange_sides_ff {fft_size}" + "csdr logpower_cf -70" + if self.fft_averages == 0 + else "csdr logaveragepower_cf -70 {fft_size} {fft_averages}", + "csdr fft_exchange_sides_ff {fft_size}", ] if self.fft_compression == "adpcm": chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"] @@ -96,37 +111,24 @@ def chain(self,which): "csdr shift_addition_cc --fifo {shift_pipe}", "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING", "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING", - "csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}" + "csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}", ] if self.secondary_demodulator: - chain += [ - "csdr tee {iqtee_pipe}", - "csdr tee {iqtee2_pipe}" - ] + chain += ["csdr tee {iqtee_pipe}", "csdr tee {iqtee2_pipe}"] # safe some cpu cycles... no need to decimate if decimation factor is 1 - last_decimation_block = ["csdr fractional_decimator_ff {last_decimation}"] if self.last_decimation != 1.0 else [] + last_decimation_block = ( + ["csdr fractional_decimator_ff {last_decimation}"] if self.last_decimation != 1.0 else [] + ) if which == "nfm": - chain += [ - "csdr fmdemod_quadri_cf", - "csdr limit_ff" - ] + chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"] chain += last_decimation_block - chain += [ - "csdr deemphasis_nfm_ff {output_rate}", - "csdr convert_f_s16" - ] + chain += ["csdr deemphasis_nfm_ff {output_rate}", "csdr convert_f_s16"] elif self.isDigitalVoice(which): - chain += [ - "csdr fmdemod_quadri_cf", - "dc_block " - ] + chain += ["csdr fmdemod_quadri_cf", "dc_block "] chain += last_decimation_block # dsd modes - if which in [ "dstar", "nxdn" ]: - chain += [ - "csdr limit_ff", - "csdr convert_f_s16" - ] + if which in ["dstar", "nxdn"]: + chain += ["csdr limit_ff", "csdr convert_f_s16"] if which == "dstar": chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "] elif which == "nxdn": @@ -135,44 +137,28 @@ def chain(self,which): max_gain = 5 # digiham modes else: - chain += [ - "rrc_filter", - "gfsk_demodulator" - ] + chain += ["rrc_filter", "gfsk_demodulator"] if which == "dmr": chain += [ "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe}", - "mbe_synthesizer -f -u {unvoiced_quality}" + "mbe_synthesizer -f -u {unvoiced_quality}", ] elif which == "ysf": - chain += [ - "ysf_decoder --fifo {meta_pipe}", - "mbe_synthesizer -y -f -u {unvoiced_quality}" - ] + chain += ["ysf_decoder --fifo {meta_pipe}", "mbe_synthesizer -y -f -u {unvoiced_quality}"] max_gain = 0.0005 chain += [ "digitalvoice_filter -f", "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain}".format(max_gain=max_gain), - "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ", ] elif which == "am": - chain += [ - "csdr amdemod_cf", - "csdr fastdcblock_ff" - ] + chain += ["csdr amdemod_cf", "csdr fastdcblock_ff"] chain += last_decimation_block - chain += [ - "csdr agc_ff", - "csdr limit_ff", - "csdr convert_f_s16" - ] + chain += ["csdr agc_ff", "csdr limit_ff", "csdr convert_f_s16"] elif which == "ssb": chain += ["csdr realpart_cf"] chain += last_decimation_block - chain += [ - "csdr agc_ff", - "csdr limit_ff" - ] + chain += ["csdr agc_ff", "csdr limit_ff"] # fixed sample rate necessary for the wsjt-x tools. fix with sox... if self.isWsjtMode() and self.get_audio_rate() != self.get_output_rate(): chain += [ @@ -181,24 +167,31 @@ def chain(self,which): else: chain += ["csdr convert_f_s16"] - if self.audio_compression=="adpcm": + if self.audio_compression == "adpcm": chain += ["csdr encode_ima_adpcm_i16_u8"] return chain def secondary_chain(self, which): - secondary_chain_base="cat {input_pipe} | " + secondary_chain_base = "cat {input_pipe} | " if which == "fft": - return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "") + return ( + secondary_chain_base + + "csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression == "adpcm" else "") + ) elif which == "bpsk31": - return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \ - "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " + \ - "csdr simple_agc_cc 0.001 0.5 | " + \ - "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ - "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ - "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" + return ( + secondary_chain_base + + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + + "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " + + "csdr simple_agc_cc 0.001 0.5 | " + + "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + + "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + + "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" + ) elif self.isWsjtMode(which): chain = secondary_chain_base + "csdr realpart_cf | " - if self.last_decimation != 1.0 : + if self.last_decimation != 1.0: chain += "csdr fractional_decimator_ff {last_decimation} | " chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" return chain @@ -211,14 +204,16 @@ def set_secondary_demodulator(self, what): self.restart() def secondary_fft_block_size(self): - return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here + return (self.samp_rate / self.decimation) / ( + self.fft_fps * 2 + ) # *2 is there because we do FFT on real signal here def secondary_decimation(self): - return 1 #currently unused + return 1 # currently unused def secondary_bpf_cutoff(self): if self.secondary_demodulator == "bpsk31": - return 31.25 / self.if_samp_rate() + return 31.25 / self.if_samp_rate() return 0 def secondary_bpf_transition_bw(self): @@ -228,7 +223,7 @@ def secondary_bpf_transition_bw(self): def secondary_samples_per_bits(self): if self.secondary_demodulator == "bpsk31": - return int(round(self.if_samp_rate()/31.25))&~3 + return int(round(self.if_samp_rate() / 31.25)) & ~3 return 0 def secondary_bw(self): @@ -236,19 +231,20 @@ def secondary_bw(self): return 31.25 def start_secondary_demodulator(self): - if not self.secondary_demodulator: return - logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate()) - secondary_command_fft=self.secondary_chain("fft") - secondary_command_demod=self.secondary_chain(self.secondary_demodulator) + if not self.secondary_demodulator: + return + logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate()) + secondary_command_fft = self.secondary_chain("fft") + secondary_command_demod = self.secondary_chain(self.secondary_demodulator) self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft) - secondary_command_fft=secondary_command_fft.format( + secondary_command_fft = secondary_command_fft.format( input_pipe=self.iqtee_pipe, secondary_fft_input_size=self.secondary_fft_size, secondary_fft_size=self.secondary_fft_size, secondary_fft_block_size=self.secondary_fft_block_size(), - ) - secondary_command_demod=secondary_command_demod.format( + ) + secondary_command_demod = secondary_command_demod.format( input_pipe=self.iqtee2_pipe, secondary_shift_pipe=self.secondary_shift_pipe, secondary_decimation=self.secondary_decimation(), @@ -256,21 +252,29 @@ def start_secondary_demodulator(self): secondary_bpf_cutoff=self.secondary_bpf_cutoff(), secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), if_samp_rate=self.if_samp_rate(), - last_decimation=self.last_decimation - ) + last_decimation=self.last_decimation, + ) logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft) logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (demod) = %s", secondary_command_demod) - my_env=os.environ.copy() - #if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; - if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; - self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) + my_env = os.environ.copy() + # if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; + if self.csdr_print_bufsizes: + my_env["CSDR_PRINT_BUFSIZES"] = "1" + self.secondary_process_fft = subprocess.Popen( + secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env + ) logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)") - self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes - logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") #TODO digimodes + self.secondary_process_demod = subprocess.Popen( + secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env + ) # TODO digimodes + logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") # TODO digimodes self.secondary_processes_running = True - self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read()))) + self.output.add_output( + "secondary_fft", + partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())), + ) if self.isWsjtMode(): smd = self.get_secondary_demodulator() if smd == "ft8": @@ -288,19 +292,20 @@ def start_secondary_demodulator(self): else: self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) - #open control pipes for csdr and send initialization data - if self.secondary_shift_pipe != None: #TODO digimodes - self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes - self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes + # open control pipes for csdr and send initialization data + if self.secondary_shift_pipe != None: # TODO digimodes + self.secondary_shift_pipe_file = open(self.secondary_shift_pipe, "w") # TODO digimodes + self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes def set_secondary_offset_freq(self, value): - self.secondary_offset_freq=value + self.secondary_offset_freq = value if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"): - self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate())) + self.secondary_shift_pipe_file.write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())) self.secondary_shift_pipe_file.flush() def stop_secondary_demodulator(self): - if self.secondary_processes_running == False: return + if self.secondary_processes_running == False: + return self.try_delete_pipes(self.secondary_pipe_names) if self.secondary_process_fft: try: @@ -319,42 +324,47 @@ def stop_secondary_demodulator(self): def get_secondary_demodulator(self): return self.secondary_demodulator - def set_secondary_fft_size(self,secondary_fft_size): - #to change this, restart is required - self.secondary_fft_size=secondary_fft_size + def set_secondary_fft_size(self, secondary_fft_size): + # to change this, restart is required + self.secondary_fft_size = secondary_fft_size - def set_audio_compression(self,what): + def set_audio_compression(self, what): self.audio_compression = what - def set_fft_compression(self,what): + def set_fft_compression(self, what): self.fft_compression = what def get_fft_bytes_to_read(self): - if self.fft_compression=="none": return self.fft_size*4 - if self.fft_compression=="adpcm": return (self.fft_size/2)+(10/2) + if self.fft_compression == "none": + return self.fft_size * 4 + if self.fft_compression == "adpcm": + return (self.fft_size / 2) + (10 / 2) def get_secondary_fft_bytes_to_read(self): - if self.fft_compression=="none": return self.secondary_fft_size*4 - if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2) + if self.fft_compression == "none": + return self.secondary_fft_size * 4 + if self.fft_compression == "adpcm": + return (self.secondary_fft_size / 2) + (10 / 2) - def set_samp_rate(self,samp_rate): - self.samp_rate=samp_rate + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate self.calculate_decimation() - if self.running: self.restart() + if self.running: + self.restart() def calculate_decimation(self): (self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate()) def get_decimation(self, input_rate, output_rate): - decimation=1 - while input_rate / (decimation+1) >= output_rate: + decimation = 1 + while input_rate / (decimation + 1) >= output_rate: decimation += 1 fraction = float(input_rate / decimation) / output_rate intermediate_rate = input_rate / decimation return (decimation, fraction, intermediate_rate) def if_samp_rate(self): - return self.samp_rate/self.decimation + return self.samp_rate / self.decimation def get_name(self): return self.name @@ -369,59 +379,64 @@ def get_audio_rate(self): return 12000 return self.get_output_rate() - def isDigitalVoice(self, demodulator = None): + def isDigitalVoice(self, demodulator=None): if demodulator is None: demodulator = self.get_demodulator() return demodulator in ["dmr", "dstar", "nxdn", "ysf"] - def isWsjtMode(self, demodulator = None): + def isWsjtMode(self, demodulator=None): if demodulator is None: demodulator = self.get_secondary_demodulator() return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"] - def set_output_rate(self,output_rate): - self.output_rate=output_rate + def set_output_rate(self, output_rate): + self.output_rate = output_rate self.calculate_decimation() - def set_demodulator(self,demodulator): - if (self.demodulator == demodulator): return - self.demodulator=demodulator + def set_demodulator(self, demodulator): + if self.demodulator == demodulator: + return + self.demodulator = demodulator self.calculate_decimation() self.restart() def get_demodulator(self): return self.demodulator - def set_fft_size(self,fft_size): - self.fft_size=fft_size + def set_fft_size(self, fft_size): + self.fft_size = fft_size self.restart() - def set_fft_fps(self,fft_fps): - self.fft_fps=fft_fps + def set_fft_fps(self, fft_fps): + self.fft_fps = fft_fps self.restart() - def set_fft_averages(self,fft_averages): - self.fft_averages=fft_averages + def set_fft_averages(self, fft_averages): + self.fft_averages = fft_averages self.restart() def fft_block_size(self): - if self.fft_averages == 0: return self.samp_rate/self.fft_fps - else: return self.samp_rate/self.fft_fps/self.fft_averages + if self.fft_averages == 0: + return self.samp_rate / self.fft_fps + else: + return self.samp_rate / self.fft_fps / self.fft_averages - def set_offset_freq(self,offset_freq): - self.offset_freq=offset_freq + def set_offset_freq(self, offset_freq): + self.offset_freq = offset_freq if self.running: self.modification_lock.acquire() - self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate)) + self.shift_pipe_file.write("%g\n" % (-float(self.offset_freq) / self.samp_rate)) self.shift_pipe_file.flush() self.modification_lock.release() - def set_bpf(self,low_cut,high_cut): - self.low_cut=low_cut - self.high_cut=high_cut + def set_bpf(self, low_cut, high_cut): + self.low_cut = low_cut + self.high_cut = high_cut if self.running: self.modification_lock.acquire() - self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) ) + self.bpf_pipe_file.write( + "%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate()) + ) self.bpf_pipe_file.flush() self.modification_lock.release() @@ -429,12 +444,12 @@ def get_bpf(self): return [self.low_cut, self.high_cut] def set_squelch_level(self, squelch_level): - self.squelch_level=squelch_level - #no squelch required on digital voice modes + self.squelch_level = squelch_level + # no squelch required on digital voice modes actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level if self.running: self.modification_lock.acquire() - self.squelch_pipe_file.write("%g\n"%(float(actual_squelch))) + self.squelch_pipe_file.write("%g\n" % (float(actual_squelch))) self.squelch_pipe_file.flush() self.modification_lock.release() @@ -450,7 +465,7 @@ def set_dmr_filter(self, filter): self.dmr_control_pipe_file.write("{0}\n".format(filter)) self.dmr_control_pipe_file.flush() - def mkfifo(self,path): + def mkfifo(self, path): try: os.unlink(path) except: @@ -458,27 +473,28 @@ def mkfifo(self,path): os.mkfifo(path) def ddc_transition_bw(self): - return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate)) + return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate)) def try_create_pipes(self, pipe_names, command_base): for pipe_name in pipe_names: - if "{"+pipe_name+"}" in command_base: - setattr(self, pipe_name, self.pipe_base_path+pipe_name) + if "{" + pipe_name + "}" in command_base: + setattr(self, pipe_name, self.pipe_base_path + pipe_name) self.mkfifo(getattr(self, pipe_name)) else: setattr(self, pipe_name, None) def try_delete_pipes(self, pipe_names): for pipe_name in pipe_names: - pipe_path = getattr(self,pipe_name,None) + pipe_path = getattr(self, pipe_name, None) if pipe_path: - try: os.unlink(pipe_path) + try: + os.unlink(pipe_path) except Exception: logger.exception("try_delete_pipes()") def start(self): self.modification_lock.acquire() - if (self.running): + if self.running: self.modification_lock.release() return self.running = True @@ -486,37 +502,58 @@ def start(self): command_base = " | ".join(self.chain(self.demodulator)) logger.debug(command_base) - #create control pipes for csdr + # create control pipes for csdr self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self)) self.try_create_pipes(self.pipe_names, command_base) - #run the command - command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, - last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, - bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), - flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, - squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, - output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000), - unvoiced_quality = self.get_unvoiced_quality(), dmr_control_pipe = self.dmr_control_pipe, - audio_rate = self.get_audio_rate()) + # run the command + command = command_base.format( + bpf_pipe=self.bpf_pipe, + shift_pipe=self.shift_pipe, + decimation=self.decimation, + last_decimation=self.last_decimation, + fft_size=self.fft_size, + fft_block_size=self.fft_block_size(), + fft_averages=self.fft_averages, + bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(), + ddc_transition_bw=self.ddc_transition_bw(), + flowcontrol=int(self.samp_rate * 2), + start_bufsize=self.base_bufsize * self.decimation, + nc_port=self.nc_port, + squelch_pipe=self.squelch_pipe, + smeter_pipe=self.smeter_pipe, + meta_pipe=self.meta_pipe, + iqtee_pipe=self.iqtee_pipe, + iqtee2_pipe=self.iqtee2_pipe, + output_rate=self.get_output_rate(), + smeter_report_every=int(self.if_samp_rate() / 6000), + unvoiced_quality=self.get_unvoiced_quality(), + dmr_control_pipe=self.dmr_control_pipe, + audio_rate=self.get_audio_rate(), + ) logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) - my_env=os.environ.copy() - if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; - if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; + my_env = os.environ.copy() + if self.csdr_dynamic_bufsize: + my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1" + if self.csdr_print_bufsizes: + my_env["CSDR_PRINT_BUFSIZES"] = "1" self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) def watch_thread(): rc = self.process.wait() logger.debug("dsp thread ended with rc=%d", rc) - if (rc == 0 and self.running and not self.modification_lock.locked()): + if rc == 0 and self.running and not self.modification_lock.locked(): logger.debug("restarting since rc = 0, self.running = true, and no modification") self.restart() - threading.Thread(target = watch_thread).start() + threading.Thread(target=watch_thread).start() - self.output.add_output("audio", partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256)) + self.output.add_output( + "audio", + partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256), + ) # open control pipes for csdr if self.bpf_pipe: @@ -537,23 +574,27 @@ def watch_thread(): if self.bpf_pipe: self.set_bpf(self.low_cut, self.high_cut) if self.smeter_pipe: - self.smeter_pipe_file=open(self.smeter_pipe,"r") + self.smeter_pipe_file = open(self.smeter_pipe, "r") + def read_smeter(): raw = self.smeter_pipe_file.readline() if len(raw) == 0: return None else: return float(raw.rstrip("\n")) + self.output.add_output("smeter", read_smeter) if self.meta_pipe != None: # TODO make digiham output unicode and then change this here - self.meta_pipe_file=open(self.meta_pipe, "r", encoding="cp437") + self.meta_pipe_file = open(self.meta_pipe, "r", encoding="cp437") + def read_meta(): raw = self.meta_pipe_file.readline() if len(raw) == 0: return None else: return raw.rstrip("\n") + self.output.add_output("meta", read_meta) if self.dmr_control_pipe: @@ -575,10 +616,11 @@ def stop(self): self.modification_lock.release() def restart(self): - if not self.running: return + if not self.running: + return self.stop() self.start() def __del__(self): self.stop() - del(self.process) + del self.process diff --git a/openwebrx.py b/openwebrx.py index 99b1419d8..07b48dee1 100644 --- a/openwebrx.py +++ b/openwebrx.py @@ -1,13 +1,14 @@ from http.server import HTTPServer from owrx.http import RequestHandler from owrx.config import PropertyManager -from owrx.feature import FeatureDetector +from owrx.feature import FeatureDetector from owrx.source import SdrService, ClientRegistry from socketserver import ThreadingMixIn from owrx.sdrhu import SdrHuUpdater import logging -logging.basicConfig(level = logging.DEBUG, format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s") + +logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") class ThreadedHttpServer(ThreadingMixIn, HTTPServer): @@ -15,21 +16,25 @@ class ThreadedHttpServer(ThreadingMixIn, HTTPServer): def main(): - print(""" + print( + """ OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package _________________________________________________________________________________________________ Author contact info: Andras Retzler, HA7ILM - """) + """ + ) pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") featureDetector = FeatureDetector() if not featureDetector.is_available("core"): - print("you are missing required dependencies to run openwebrx. " - "please check that the following core requirements are installed:") + print( + "you are missing required dependencies to run openwebrx. " + "please check that the following core requirements are installed:" + ) print(", ".join(featureDetector.get_requirements("core"))) return @@ -40,7 +45,7 @@ def main(): updater = SdrHuUpdater() updater.start() - server = ThreadedHttpServer(('0.0.0.0', pm.getPropertyValue("web_port")), RequestHandler) + server = ThreadedHttpServer(("0.0.0.0", pm.getPropertyValue("web_port")), RequestHandler) server.serve_forever() diff --git a/owrx/bands.py b/owrx/bands.py index 5971bf85c..bc76b2abe 100644 --- a/owrx/bands.py +++ b/owrx/bands.py @@ -1,6 +1,7 @@ import json import logging + logger = logging.getLogger(__name__) @@ -16,7 +17,11 @@ def __init__(self, dict): freqs = [freqs] for f in freqs: if not self.inBand(f): - logger.warning("Frequency for {mode} on {band} is not within band limits: {frequency}".format(mode = mode, frequency = f, band = self.name)) + logger.warning( + "Frequency for {mode} on {band} is not within band limits: {frequency}".format( + mode=mode, frequency=f, band=self.name + ) + ) else: self.frequencies.append({"mode": mode, "frequency": f}) @@ -33,6 +38,7 @@ def getDialFrequencies(self, range): class Bandplan(object): sharedInstance = None + @staticmethod def getSharedInstance(): if Bandplan.sharedInstance is None: diff --git a/owrx/config.py b/owrx/config.py index 6db151e1e..d8b6ad2b1 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -1,4 +1,5 @@ import logging + logger = logging.getLogger(__name__) @@ -15,7 +16,7 @@ def cancel(self): class Property(object): - def __init__(self, value = None): + def __init__(self, value=None): self.value = value self.subscribers = [] @@ -23,7 +24,7 @@ def getValue(self): return self.value def setValue(self, value): - if (self.value == value): + if self.value == value: return self self.value = value for c in self.subscribers: @@ -36,7 +37,8 @@ def setValue(self, value): def wire(self, callback): sub = Subscription(self, callback) self.subscribers.append(sub) - if not self.value is None: sub.call(self.value) + if not self.value is None: + sub.call(self.value) return sub def unwire(self, sub): @@ -47,8 +49,10 @@ def unwire(self, sub): pass return self + class PropertyManager(object): sharedInstance = None + @staticmethod def getSharedInstance(): if PropertyManager.sharedInstance is None: @@ -56,9 +60,11 @@ def getSharedInstance(): return PropertyManager.sharedInstance def collect(self, *props): - return PropertyManager({name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props}) + return PropertyManager( + {name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props} + ) - def __init__(self, properties = None): + def __init__(self, properties=None): self.properties = {} self.subscribers = [] if properties is not None: @@ -67,12 +73,14 @@ def __init__(self, properties = None): def add(self, name, prop): self.properties[name] = prop + def fireCallbacks(value): for c in self.subscribers: try: c.call(name, value) except Exception as e: logger.exception(e) + prop.wire(fireCallbacks) return self @@ -88,7 +96,7 @@ def __setitem__(self, name, value): self.getProperty(name).setValue(value) def __dict__(self): - return {k:v.getValue() for k, v in self.properties.items()} + return {k: v.getValue() for k, v in self.properties.items()} def hasProperty(self, name): return name in self.properties diff --git a/owrx/connection.py b/owrx/connection.py index 17551ff7f..b68cbaf37 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -7,6 +7,7 @@ from owrx.map import Map import logging + logger = logging.getLogger(__name__) @@ -29,11 +30,26 @@ def close(self): class OpenWebRxReceiverClient(Client): - config_keys = ["waterfall_colors", "waterfall_min_level", "waterfall_max_level", - "waterfall_auto_level_margin", "lfo_offset", "samp_rate", "fft_size", "fft_fps", - "audio_compression", "fft_compression", "max_clients", "start_mod", - "client_audio_buffer_size", "start_freq", "center_freq", "mathbox_waterfall_colors", - "mathbox_waterfall_history_length", "mathbox_waterfall_frequency_resolution"] + config_keys = [ + "waterfall_colors", + "waterfall_min_level", + "waterfall_max_level", + "waterfall_auto_level_margin", + "lfo_offset", + "samp_rate", + "fft_size", + "fft_fps", + "audio_compression", + "fft_compression", + "max_clients", + "start_mod", + "client_audio_buffer_size", + "start_freq", + "center_freq", + "mathbox_waterfall_colors", + "mathbox_waterfall_history_length", + "mathbox_waterfall_frequency_resolution", + ] def __init__(self, conn): super().__init__(conn) @@ -49,12 +65,23 @@ def __init__(self, conn): self.setSdr() # send receiver info - receiver_keys = ["receiver_name", "receiver_location", "receiver_qra", "receiver_asl", "receiver_gps", - "photo_title", "photo_desc"] + receiver_keys = [ + "receiver_name", + "receiver_location", + "receiver_qra", + "receiver_asl", + "receiver_gps", + "photo_title", + "photo_desc", + ] receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys) self.write_receiver_details(receiver_details) - profiles = [{"name": s.getName() + " " + p["name"], "id":sid + "|" + pid} for (sid, s) in SdrService.getSources().items() for (pid, p) in s.getProfiles().items()] + profiles = [ + {"name": s.getName() + " " + p["name"], "id": sid + "|" + pid} + for (sid, s) in SdrService.getSources().items() + for (pid, p) in s.getProfiles().items() + ] self.write_profiles(profiles) features = FeatureDetector().feature_availability() @@ -62,9 +89,9 @@ def __init__(self, conn): CpuUsageThread.getSharedInstance().add_client(self) - def setSdr(self, id = None): + def setSdr(self, id=None): next = SdrService.getSource(id) - if (next == self.sdr): + if next == self.sdr: return self.stopDsp() @@ -76,7 +103,11 @@ def setSdr(self, id = None): self.sdr = next # send initial config - configProps = self.sdr.getProps().collect(*OpenWebRxReceiverClient.config_keys).defaults(PropertyManager.getSharedInstance()) + configProps = ( + self.sdr.getProps() + .collect(*OpenWebRxReceiverClient.config_keys) + .defaults(PropertyManager.getSharedInstance()) + ) def sendConfig(key, value): config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys) @@ -89,7 +120,6 @@ def sendConfig(key, value): frequencyRange = (cf - srh, cf + srh) self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencis(frequencyRange)) - self.configSub = configProps.wire(sendConfig) sendConfig(None, None) @@ -118,8 +148,11 @@ def stopDsp(self): def setParams(self, params): # only the keys in the protected property manager can be overridden from the web - protected = self.sdr.getProps().collect("samp_rate", "center_freq", "rf_gain", "type", "if_gain") \ + protected = ( + self.sdr.getProps() + .collect("samp_rate", "center_freq", "rf_gain", "type", "if_gain") .defaults(PropertyManager.getSharedInstance()) + ) for key, value in params.items(): protected[key] = value @@ -134,13 +167,13 @@ def write_dsp_data(self, data): self.protected_send(bytes([0x02]) + data) def write_s_meter_level(self, level): - self.protected_send({"type":"smeter","value":level}) + self.protected_send({"type": "smeter", "value": level}) def write_cpu_usage(self, usage): - self.protected_send({"type":"cpuusage","value":usage}) + self.protected_send({"type": "cpuusage", "value": usage}) def write_clients(self, clients): - self.protected_send({"type":"clients","value":clients}) + self.protected_send({"type": "clients", "value": clients}) def write_secondary_fft(self, data): self.protected_send(bytes([0x03]) + data) @@ -149,22 +182,22 @@ def write_secondary_demod(self, data): self.protected_send(bytes([0x04]) + data) def write_secondary_dsp_config(self, cfg): - self.protected_send({"type":"secondary_config", "value":cfg}) + self.protected_send({"type": "secondary_config", "value": cfg}) def write_config(self, cfg): - self.protected_send({"type":"config","value":cfg}) + self.protected_send({"type": "config", "value": cfg}) def write_receiver_details(self, details): - self.protected_send({"type":"receiver_details","value":details}) + self.protected_send({"type": "receiver_details", "value": details}) def write_profiles(self, profiles): - self.protected_send({"type":"profiles","value":profiles}) + self.protected_send({"type": "profiles", "value": profiles}) def write_features(self, features): - self.protected_send({"type":"features","value":features}) + self.protected_send({"type": "features", "value": features}) def write_metadata(self, metadata): - self.protected_send({"type":"metadata","value":metadata}) + self.protected_send({"type": "metadata", "value": metadata}) def write_wsjt_message(self, message): self.protected_send({"type": "wsjt_message", "value": message}) @@ -187,10 +220,11 @@ def close(self): super().close() def write_config(self, cfg): - self.protected_send({"type":"config","value":cfg}) + self.protected_send({"type": "config", "value": cfg}) def write_update(self, update): - self.protected_send({"type":"update","value":update}) + self.protected_send({"type": "update", "value": update}) + class WebSocketMessageHandler(object): def __init__(self): @@ -199,11 +233,11 @@ def __init__(self): self.dsp = None def handleTextMessage(self, conn, message): - if (message[:16] == "SERVER DE CLIENT"): + if message[:16] == "SERVER DE CLIENT": meta = message[17:].split(" ") self.handshake = {v[0]: "=".join(v[1:]) for v in map(lambda x: x.split("="), meta)} - conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version = openwebrx_version)) + conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version=openwebrx_version)) logger.debug("client connection intitialized") if "type" in self.handshake: diff --git a/owrx/controllers.py b/owrx/controllers.py index c011677d1..c6c0da55d 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -11,13 +11,16 @@ from owrx.feature import FeatureDetector import logging + logger = logging.getLogger(__name__) + class Controller(object): def __init__(self, handler, request): self.handler = handler self.request = request - def send_response(self, content, code = 200, content_type = "text/html", last_modified: datetime = None, max_age = None): + + def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None): self.handler.send_response(code) if content_type is not None: self.handler.send_header("Content-Type", content_type) @@ -26,7 +29,7 @@ def send_response(self, content, code = 200, content_type = "text/html", last_mo if max_age is not None: self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age)) self.handler.end_headers() - if (type(content) == str): + if type(content) == str: content = content.encode() self.handler.wfile.write(content) @@ -45,44 +48,49 @@ def handle_request(self): "asl": pm["receiver_asl"], "loc": pm["receiver_location"], "sw_version": openwebrx_version, - "avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png") + "avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png"), } - self.send_response("\n".join(["{key}={value}".format(key = key, value = value) for key, value in vars.items()])) + self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()])) + class AssetsController(Controller): - def serve_file(self, file, content_type = None): + def serve_file(self, file, content_type=None): try: - modified = datetime.fromtimestamp(os.path.getmtime('htdocs/' + file)) + modified = datetime.fromtimestamp(os.path.getmtime("htdocs/" + file)) if "If-Modified-Since" in self.handler.headers: - client_modified = datetime.strptime(self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z") + client_modified = datetime.strptime( + self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z" + ) if modified <= client_modified: - self.send_response("", code = 304) + self.send_response("", code=304) return - f = open('htdocs/' + file, 'rb') + f = open("htdocs/" + file, "rb") data = f.read() f.close() if content_type is None: (content_type, encoding) = mimetypes.MimeTypes().guess_type(file) - self.send_response(data, content_type = content_type, last_modified = modified, max_age = 3600) + self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600) except FileNotFoundError: - self.send_response("file not found", code = 404) + self.send_response("file not found", code=404) + def handle_request(self): filename = self.request.matches.group(1) self.serve_file(filename) + class TemplateController(Controller): def render_template(self, file, **vars): - f = open('htdocs/' + file, 'r') + f = open("htdocs/" + file, "r") template = Template(f.read()) f.close() return template.safe_substitute(**vars) def serve_template(self, file, **vars): - self.send_response(self.render_template(file, **vars), content_type = 'text/html') + self.send_response(self.render_template(file, **vars), content_type="text/html") def default_variables(self): return {} @@ -90,8 +98,8 @@ def default_variables(self): class WebpageController(TemplateController): def template_variables(self): - header = self.render_template('include/header.include.html') - return { "header": header } + header = self.render_template("include/header.include.html") + return {"header": header} class IndexController(WebpageController): @@ -101,17 +109,20 @@ def handle_request(self): class MapController(WebpageController): def handle_request(self): - #TODO check if we have a google maps api key first? + # TODO check if we have a google maps api key first? self.serve_template("map.html", **self.template_variables()) + class FeatureController(WebpageController): def handle_request(self): self.serve_template("features.html", **self.template_variables()) + class ApiController(Controller): def handle_request(self): data = json.dumps(FeatureDetector().feature_report()) - self.send_response(data, content_type = "application/json") + self.send_response(data, content_type="application/json") + class WebSocketController(Controller): def handle_request(self): diff --git a/owrx/feature.py b/owrx/feature.py index cd67f628c..66fe1f9b8 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -7,6 +7,7 @@ import inspect import logging + logger = logging.getLogger(__name__) @@ -16,14 +17,14 @@ class UnknownFeatureException(Exception): class FeatureDetector(object): features = { - "core": [ "csdr", "nmux", "nc" ], - "rtl_sdr": [ "rtl_sdr" ], - "sdrplay": [ "rx_tools" ], - "hackrf": [ "hackrf_transfer" ], - "airspy": [ "airspy_rx" ], - "digital_voice_digiham": [ "digiham", "sox" ], - "digital_voice_dsd": [ "dsd", "sox", "digiham" ], - "wsjt-x": [ "wsjtx", "sox" ] + "core": ["csdr", "nmux", "nc"], + "rtl_sdr": ["rtl_sdr"], + "sdrplay": ["rx_tools"], + "hackrf": ["hackrf_transfer"], + "airspy": ["airspy_rx"], + "digital_voice_digiham": ["digiham", "sox"], + "digital_voice_dsd": ["dsd", "sox", "digiham"], + "wsjt-x": ["wsjtx", "sox"], } def feature_availability(self): @@ -36,14 +37,14 @@ def requirement_details(name): "available": available, # as of now, features are always enabled as soon as they are available. this may change in the future. "enabled": available, - "description": self.get_requirement_description(name) + "description": self.get_requirement_description(name), } def feature_details(name): return { "description": "", "available": self.is_available(name), - "requirements": {name: requirement_details(name) for name in self.get_requirements(name)} + "requirements": {name: requirement_details(name) for name in self.get_requirements(name)}, } return {name: feature_details(name) for name in FeatureDetector.features} @@ -55,7 +56,7 @@ def get_requirements(self, feature): try: return FeatureDetector.features[feature] except KeyError: - raise UnknownFeatureException("Feature \"{0}\" is not known.".format(feature)) + raise UnknownFeatureException('Feature "{0}" is not known.'.format(feature)) def has_requirements(self, requirements): passed = True @@ -102,7 +103,7 @@ def has_nc(self): Nc is the client used to connect to the nmux multiplexer. It is provided by either the BSD netcat (recommended for better performance) or GNU netcat packages. Please check your distribution package manager for options. """ - return self.command_is_runnable('nc --help') + return self.command_is_runnable("nc --help") def has_rtl_sdr(self): """ @@ -156,7 +157,8 @@ def has_digiham(self): """ required_version = LooseVersion("0.2") - digiham_version_regex = re.compile('^digiham version (.*)$') + digiham_version_regex = re.compile("^digiham version (.*)$") + def check_digiham_version(command): try: process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE) @@ -165,14 +167,21 @@ def check_digiham_version(command): return version >= required_version except FileNotFoundError: return False + return reduce( and_, map( check_digiham_version, - ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator", - "digitalvoice_filter"] + [ + "rrc_filter", + "ysf_decoder", + "dmr_decoder", + "mbe_synthesizer", + "gfsk_demodulator", + "digitalvoice_filter", + ], ), - True + True, ) def has_dsd(self): @@ -203,11 +212,4 @@ def has_wsjtx(self): [WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions on how to build from source. """ - return reduce( - and_, - map( - self.command_is_runnable, - ["jt9", "wsprd"] - ), - True - ) + return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True) diff --git a/owrx/http.py b/owrx/http.py index ce821b9b5..99c100344 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -1,23 +1,36 @@ -from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController, MapController, FeatureController, ApiController +from owrx.controllers import ( + StatusController, + IndexController, + AssetsController, + WebSocketController, + MapController, + FeatureController, + ApiController, +) from http.server import BaseHTTPRequestHandler import re from urllib.parse import urlparse, parse_qs import logging + logger = logging.getLogger(__name__) + class RequestHandler(BaseHTTPRequestHandler): def __init__(self, request, client_address, server): self.router = Router() super().__init__(request, client_address, server) + def do_GET(self): self.router.route(self) + class Request(object): - def __init__(self, query = None, matches = None): + def __init__(self, query=None, matches=None): self.query = query self.matches = matches + class Router(object): mappings = [ {"route": "/", "controller": IndexController}, @@ -29,8 +42,9 @@ class Router(object): {"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController}, {"route": "/map", "controller": MapController}, {"route": "/features", "controller": FeatureController}, - {"route": "/api/features", "controller": ApiController} + {"route": "/api/features", "controller": ApiController}, ] + def find_controller(self, path): for m in Router.mappings: if "route" in m: @@ -41,13 +55,16 @@ def find_controller(self, path): matches = regex.match(path) if matches: return (m["controller"], matches) + def route(self, handler): url = urlparse(handler.path) res = self.find_controller(url.path) if res is not None: (controller, matches) = res query = parse_qs(url.query) - logger.debug("path: {0}, controller: {1}, query: {2}, matches: {3}".format(handler.path, controller, query, matches)) + logger.debug( + "path: {0}, controller: {1}, query: {2}, matches: {3}".format(handler.path, controller, query, matches) + ) request = Request(query, matches) controller(handler, request).handle_request() else: diff --git a/owrx/map.py b/owrx/map.py index a6adbb67c..9908d7afd 100644 --- a/owrx/map.py +++ b/owrx/map.py @@ -4,6 +4,7 @@ from owrx.bands import Band import logging + logger = logging.getLogger(__name__) @@ -14,6 +15,7 @@ def __dict__(self): class Map(object): sharedInstance = None + @staticmethod def getSharedInstance(): if Map.sharedInstance is None: @@ -41,16 +43,18 @@ def broadcast(self, update): def addClient(self, client): self.clients.append(client) - client.write_update([ - { - "callsign": callsign, - "location": record["location"].__dict__(), - "lastseen": record["updated"].timestamp() * 1000, - "mode" : record["mode"], - "band" : record["band"].getName() if record["band"] is not None else None - } - for (callsign, record) in self.positions.items() - ]) + client.write_update( + [ + { + "callsign": callsign, + "location": record["location"].__dict__(), + "lastseen": record["updated"].timestamp() * 1000, + "mode": record["mode"], + "band": record["band"].getName() if record["band"] is not None else None, + } + for (callsign, record) in self.positions.items() + ] + ) def removeClient(self, client): try: @@ -61,15 +65,17 @@ def removeClient(self, client): def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None): ts = datetime.now() self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band} - self.broadcast([ - { - "callsign": callsign, - "location": loc.__dict__(), - "lastseen": ts.timestamp() * 1000, - "mode" : mode, - "band" : band.getName() if band is not None else None - } - ]) + self.broadcast( + [ + { + "callsign": callsign, + "location": loc.__dict__(), + "lastseen": ts.timestamp() * 1000, + "mode": mode, + "band": band.getName() if band is not None else None, + } + ] + ) def removeLocation(self, callsign): self.positions.pop(callsign, None) @@ -84,17 +90,14 @@ def removeOldPositions(self): for callsign in to_be_removed: self.removeLocation(callsign) + class LatLngLocation(Location): def __init__(self, lat: float, lon: float): self.lat = lat self.lon = lon def __dict__(self): - return { - "type":"latlon", - "lat":self.lat, - "lon":self.lon - } + return {"type": "latlon", "lat": self.lat, "lon": self.lon} class LocatorLocation(Location): @@ -102,7 +105,4 @@ def __init__(self, locator: str): self.locator = locator def __dict__(self): - return { - "type":"locator", - "locator":self.locator - } + return {"type": "locator", "locator": self.locator} diff --git a/owrx/meta.py b/owrx/meta.py index 8a85bad01..1d7a63f1d 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -8,8 +8,10 @@ logger = logging.getLogger(__name__) + class DmrCache(object): sharedInstance = None + @staticmethod def getSharedInstance(): if DmrCache.sharedInstance is None: @@ -18,21 +20,20 @@ def getSharedInstance(): def __init__(self): self.cache = {} - self.cacheTimeout = timedelta(seconds = 86400) + self.cacheTimeout = timedelta(seconds=86400) def isValid(self, key): - if not key in self.cache: return False + if not key in self.cache: + return False entry = self.cache[key] return entry["timestamp"] + self.cacheTimeout > datetime.now() def put(self, key, value): - self.cache[key] = { - "timestamp": datetime.now(), - "data": value - } + self.cache[key] = {"timestamp": datetime.now(), "data": value} def get(self, key): - if not self.isValid(key): return None + if not self.isValid(key): + return None return self.cache[key]["data"] @@ -52,8 +53,10 @@ def downloadRadioIdData(self, id): del self.threads[id] def enrich(self, meta): - if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: return None - if not "source" in meta: return None + if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: + return None + if not "source" in meta: + return None id = meta["source"] cache = DmrCache.getSharedInstance() if not cache.isValid(id): @@ -77,10 +80,7 @@ def enrich(self, meta): class MetaParser(object): - enrichers = { - "DMR": DmrMetaEnricher(), - "YSF": YsfMetaEnricher() - } + enrichers = {"DMR": DmrMetaEnricher(), "YSF": YsfMetaEnricher()} def __init__(self, handler): self.handler = handler @@ -93,6 +93,6 @@ def parse(self, meta): protocol = meta["protocol"] if protocol in MetaParser.enrichers: additional_data = MetaParser.enrichers[protocol].enrich(meta) - if additional_data is not None: meta["additional"] = additional_data + if additional_data is not None: + meta["additional"] = additional_data self.handler.write_metadata(meta) - diff --git a/owrx/sdrhu.py b/owrx/sdrhu.py index 5f0d7fb2f..c84a2f50c 100644 --- a/owrx/sdrhu.py +++ b/owrx/sdrhu.py @@ -4,23 +4,26 @@ from owrx.config import PropertyManager import logging + logger = logging.getLogger(__name__) class SdrHuUpdater(threading.Thread): def __init__(self): self.doRun = True - super().__init__(daemon = True) + super().__init__(daemon=True) def update(self): pm = PropertyManager.getSharedInstance() - cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}\" 2>&1".format(**pm.__dict__()) + cmd = 'wget --timeout=15 -4qO- https://sdr.hu/update --post-data "url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}" 2>&1'.format( + **pm.__dict__() + ) logger.debug(cmd) - returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate() - returned=returned[0].decode('utf-8') + returned = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate() + returned = returned[0].decode("utf-8") if "UPDATE:" in returned: retrytime_mins = 20 - value=returned.split("UPDATE:")[1].split("\n",1)[0] + value = returned.split("UPDATE:")[1].split("\n", 1)[0] if value.startswith("SUCCESS"): logger.info("Update succeeded!") else: @@ -33,4 +36,4 @@ def update(self): def run(self): while self.doRun: retrytime_mins = self.update() - time.sleep(60*retrytime_mins) + time.sleep(60 * retrytime_mins) diff --git a/owrx/source.py b/owrx/source.py index 2e2bca796..da2e928a8 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -14,10 +14,12 @@ logger = logging.getLogger(__name__) + class SdrService(object): sdrProps = None sources = {} lastPort = None + @staticmethod def getNextPort(): pm = PropertyManager.getSharedInstance() @@ -29,45 +31,61 @@ def getNextPort(): if SdrService.lastPort > end: raise IndexError("no more available ports to start more sdrs") return SdrService.lastPort + @staticmethod def loadProps(): if SdrService.sdrProps is None: pm = PropertyManager.getSharedInstance() featureDetector = FeatureDetector() + def loadIntoPropertyManager(dict: dict): propertyManager = PropertyManager() for (name, value) in dict.items(): propertyManager[name] = value return propertyManager + def sdrTypeAvailable(value): try: if not featureDetector.is_available(value["type"]): - logger.error("The RTL source type \"{0}\" is not available. please check requirements.".format(value["type"])) + logger.error( + 'The RTL source type "{0}" is not available. please check requirements.'.format( + value["type"] + ) + ) return False return True except UnknownFeatureException: - logger.error("The RTL source type \"{0}\" is invalid. Please check your configuration".format(value["type"])) + logger.error( + 'The RTL source type "{0}" is invalid. Please check your configuration'.format(value["type"]) + ) return False + # transform all dictionary items into PropertyManager object, filtering out unavailable ones SdrService.sdrProps = { name: loadIntoPropertyManager(value) for (name, value) in pm["sdrs"].items() if sdrTypeAvailable(value) } - logger.info("SDR sources loaded. Availables SDRs: {0}".format(", ".join(map(lambda x: x["name"], SdrService.sdrProps.values())))) + logger.info( + "SDR sources loaded. Availables SDRs: {0}".format( + ", ".join(map(lambda x: x["name"], SdrService.sdrProps.values())) + ) + ) + @staticmethod - def getSource(id = None): + def getSource(id=None): SdrService.loadProps() if id is None: # TODO: configure default sdr in config? right now it will pick the first one off the list. id = list(SdrService.sdrProps.keys())[0] sources = SdrService.getSources() return sources[id] + @staticmethod def getSources(): SdrService.loadProps() for id in SdrService.sdrProps.keys(): if not id in SdrService.sources: props = SdrService.sdrProps[id] - className = ''.join(x for x in props["type"].title() if x.isalnum()) + "Source" + className = "".join(x for x in props["type"].title() if x.isalnum()) + "Source" cls = getattr(sys.modules[__name__], className) SdrService.sources[id] = cls(props, SdrService.getNextPort()) return SdrService.sources @@ -85,6 +103,7 @@ def restart(name, value): logger.debug("restarting sdr source due to property change: {0} changed to {1}".format(name, value)) self.stop() self.start() + self.rtlProps.wire(restart) self.port = port self.monitor = None @@ -102,7 +121,7 @@ def getCommand(self): def getFormatConversion(self): return None - def activateProfile(self, id = None): + def activateProfile(self, id=None): profiles = self.props["profiles"] if id is None: id = list(profiles.keys())[0] @@ -110,7 +129,8 @@ def activateProfile(self, id = None): profile = profiles[id] for (key, value) in profile.items(): # skip the name, that would overwrite the source name. - if key == "name": continue + if key == "name": + continue self.props[key] = value def getProfiles(self): @@ -134,7 +154,9 @@ def start(self): props = self.rtlProps start_sdr_command = self.getCommand().format( - **props.collect("samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain").__dict__() + **props.collect( + "samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain" + ).__dict__() ) format_conversion = self.getFormatConversion() @@ -142,14 +164,22 @@ def start(self): start_sdr_command += " | " + format_conversion nmux_bufcnt = nmux_bufsize = 0 - while nmux_bufsize < props["samp_rate"]/4: nmux_bufsize += 4096 - while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6: nmux_bufcnt += 1 + while nmux_bufsize < props["samp_rate"] / 4: + nmux_bufsize += 4096 + while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6: + nmux_bufcnt += 1 if nmux_bufcnt == 0 or nmux_bufsize == 0: - logger.error("Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py") + logger.error( + "Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py" + ) self.modificationLock.release() return logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)) - cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, self.port) + cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % ( + nmux_bufsize, + nmux_bufcnt, + self.port, + ) self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp) logger.info("Started rtl source: " + cmd) @@ -158,7 +188,7 @@ def wait_for_process_to_end(): logger.debug("shut down with RC={0}".format(rc)) self.monitor = None - self.monitor = threading.Thread(target = wait_for_process_to_end) + self.monitor = threading.Thread(target=wait_for_process_to_end) self.monitor.start() while True: @@ -201,6 +231,7 @@ def sleepOnRestart(self): def addClient(self, c): self.clients.append(c) self.start() + def removeClient(self, c): try: self.clients.remove(c) @@ -236,6 +267,7 @@ def getCommand(self): def getFormatConversion(self): return "csdr convert_u8_f" + class HackrfSource(SdrSource): def getCommand(self): return "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-" @@ -243,39 +275,54 @@ def getCommand(self): def getFormatConversion(self): return "csdr convert_s8_f" + class SdrplaySource(SdrSource): def getCommand(self): command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}" - gainMap = { "rf_gain" : "RFGR", "if_gain" : "IFGR"} - gains = [ "{0}={{{1}}}".format(gainMap[name], name) for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items() if value is not None ] + gainMap = {"rf_gain": "RFGR", "if_gain": "IFGR"} + gains = [ + "{0}={{{1}}}".format(gainMap[name], name) + for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items() + if value is not None + ] if gains: - command += " -g {gains}".format(gains = ",".join(gains)) + command += " -g {gains}".format(gains=",".join(gains)) if self.rtlProps["antenna"] is not None: - command += " -a \"{antenna}\"" + command += ' -a "{antenna}"' command += " -" return command def sleepOnRestart(self): time.sleep(1) + class AirspySource(SdrSource): def getCommand(self): - frequency = self.props['center_freq'] / 1e6 + frequency = self.props["center_freq"] / 1e6 command = "airspy_rx" command += " -f{0}".format(frequency) command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}" return command + def getFormatConversion(self): return "csdr convert_s16_f" + class SpectrumThread(csdr.output): def __init__(self, sdrSource): self.sdrSource = sdrSource super().__init__() self.props = props = self.sdrSource.props.collect( - "samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression", - "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through", "temporary_directory" + "samp_rate", + "fft_size", + "fft_fps", + "fft_voverlap_factor", + "fft_compression", + "csdr_dynamic_bufsize", + "csdr_print_bufsizes", + "csdr_through", + "temporary_directory", ).defaults(PropertyManager.getSharedInstance()) self.dsp = dsp = csdr.dsp(self) @@ -288,7 +335,11 @@ def set_fft_averages(key, value): fft_fps = props["fft_fps"] fft_voverlap_factor = props["fft_voverlap_factor"] - dsp.set_fft_averages(int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor))) if fft_voverlap_factor>0 else 0) + dsp.set_fft_averages( + int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor))) + if fft_voverlap_factor > 0 + else 0 + ) self.subscriptions = [ props.getProperty("samp_rate").wire(dsp.set_samp_rate), @@ -296,7 +347,7 @@ def set_fft_averages(key, value): props.getProperty("fft_fps").wire(dsp.set_fft_fps), props.getProperty("fft_compression").wire(dsp.set_fft_compression), props.getProperty("temporary_directory").wire(dsp.set_temporary_directory), - props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages) + props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages), ] set_fft_averages(None, None) @@ -317,7 +368,7 @@ def add_output(self, type, read_fn): return if self.props["csdr_dynamic_bufsize"]: - read_fn(8) #dummy read to skip bufsize & preamble + read_fn(8) # dummy read to skip bufsize & preamble logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1") def pipe(): @@ -329,7 +380,7 @@ def pipe(): else: self.sdrSource.writeSpectrumData(data) - threading.Thread(target = pipe).start() + threading.Thread(target=pipe).start() def stop(self): self.dsp.stop() @@ -340,9 +391,11 @@ def stop(self): def onSdrAvailable(self): self.dsp.start() + def onSdrUnavailable(self): self.dsp.stop() + class DspManager(csdr.output): def __init__(self, handler, sdrSource): self.handler = handler @@ -350,11 +403,24 @@ def __init__(self, handler, sdrSource): self.metaParser = MetaParser(self.handler) self.wsjtParser = WsjtParser(self.handler) - self.localProps = self.sdrSource.getProps().collect( - "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", - "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality", - "dmr_filter", "temporary_directory", "center_freq" - ).defaults(PropertyManager.getSharedInstance()) + self.localProps = ( + self.sdrSource.getProps() + .collect( + "audio_compression", + "fft_compression", + "digimodes_fft_size", + "csdr_dynamic_bufsize", + "csdr_print_bufsizes", + "csdr_through", + "digimodes_enable", + "samp_rate", + "digital_voice_unvoiced_quality", + "dmr_filter", + "temporary_directory", + "center_freq", + ) + .defaults(PropertyManager.getSharedInstance()) + ) self.dsp = csdr.dsp(self) self.dsp.nc_port = self.sdrSource.getPort() @@ -386,28 +452,33 @@ def set_dial_freq(key, value): self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality), self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter), self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory), - self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq) + self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq), ] self.dsp.set_offset_freq(0) - self.dsp.set_bpf(-4000,4000) + self.dsp.set_bpf(-4000, 4000) self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"] self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"] self.dsp.csdr_through = self.localProps["csdr_through"] - if (self.localProps["digimodes_enable"]): + if self.localProps["digimodes_enable"]: + def set_secondary_mod(mod): - if mod == False: mod = None + if mod == False: + mod = None self.dsp.set_secondary_demodulator(mod) if mod is not None: - self.handler.write_secondary_dsp_config({ - "secondary_fft_size":self.localProps["digimodes_fft_size"], - "if_samp_rate":self.dsp.if_samp_rate(), - "secondary_bw":self.dsp.secondary_bw() - }) + self.handler.write_secondary_dsp_config( + { + "secondary_fft_size": self.localProps["digimodes_fft_size"], + "if_samp_rate": self.dsp.if_samp_rate(), + "secondary_bw": self.dsp.secondary_bw(), + } + ) + self.subscriptions += [ self.localProps.getProperty("secondary_mod").wire(set_secondary_mod), - self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq) + self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq), ] self.sdrSource.addClient(self) @@ -426,7 +497,7 @@ def add_output(self, t, read_fn): "secondary_fft": self.handler.write_secondary_fft, "secondary_demod": self.handler.write_secondary_demod, "meta": self.metaParser.parse, - "wsjt_demod": self.wsjtParser.parse + "wsjt_demod": self.wsjtParser.parse, } write = writers[t] @@ -440,6 +511,7 @@ def copy(): run = False else: write(data) + return copy threading.Thread(target=pump(read_fn, write)).start() @@ -462,8 +534,10 @@ def onSdrUnavailable(self): logger.debug("received onSdrUnavailable, shutting down DspSource") self.dsp.stop() + class CpuUsageThread(threading.Thread): sharedInstance = None + @staticmethod def getSharedInstance(): if CpuUsageThread.sharedInstance is None: @@ -491,21 +565,23 @@ def run(self): def get_cpu_usage(self): try: - f = open("/proc/stat","r") + f = open("/proc/stat", "r") except: - return 0 #Workaround, possibly we're on a Mac + return 0 # Workaround, possibly we're on a Mac line = "" - while not "cpu " in line: line=f.readline() + while not "cpu " in line: + line = f.readline() f.close() spl = line.split(" ") worktime = int(spl[2]) + int(spl[3]) + int(spl[4]) idletime = int(spl[5]) - dworktime = (worktime - self.last_worktime) - didletime = (idletime - self.last_idletime) - rate = float(dworktime) / (didletime+dworktime) + dworktime = worktime - self.last_worktime + didletime = idletime - self.last_idletime + rate = float(dworktime) / (didletime + dworktime) self.last_worktime = worktime self.last_idletime = idletime - if (self.last_worktime==0): return 0 + if self.last_worktime == 0: + return 0 return rate def add_client(self, c): @@ -523,11 +599,14 @@ def shutdown(self): CpuUsageThread.sharedInstance = None self.doRun = False + class TooManyClientsException(Exception): pass + class ClientRegistry(object): sharedInstance = None + @staticmethod def getSharedInstance(): if ClientRegistry.sharedInstance is None: @@ -558,4 +637,4 @@ def removeClient(self, client): self.clients.remove(client) except ValueError: pass - self.broadcast() \ No newline at end of file + self.broadcast() diff --git a/owrx/version.py b/owrx/version.py index 7437edac2..73f2d9971 100644 --- a/owrx/version.py +++ b/owrx/version.py @@ -1 +1 @@ -openwebrx_version = "v0.18" \ No newline at end of file +openwebrx_version = "v0.18" diff --git a/owrx/websocket.py b/owrx/websocket.py index 360f28b5f..c773cf992 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -3,69 +3,76 @@ import json import logging + logger = logging.getLogger(__name__) + class WebSocketConnection(object): def __init__(self, handler, messageHandler): self.handler = handler self.messageHandler = messageHandler my_headers = self.handler.headers.items() - my_header_keys = list(map(lambda x:x[0],my_headers)) - h_key_exists = lambda x:my_header_keys.count(x) - h_value = lambda x:my_headers[my_header_keys.index(x)][1] - if (not h_key_exists("Upgrade")) or not (h_value("Upgrade")=="websocket") or (not h_key_exists("Sec-WebSocket-Key")): + my_header_keys = list(map(lambda x: x[0], my_headers)) + h_key_exists = lambda x: my_header_keys.count(x) + h_value = lambda x: my_headers[my_header_keys.index(x)][1] + if ( + (not h_key_exists("Upgrade")) + or not (h_value("Upgrade") == "websocket") + or (not h_key_exists("Sec-WebSocket-Key")) + ): raise WebSocketException ws_key = h_value("Sec-WebSocket-Key") shakey = hashlib.sha1() - shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key = ws_key).encode()) + shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key=ws_key).encode()) ws_key_toreturn = base64.b64encode(shakey.digest()) - self.handler.wfile.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format(ws_key_toreturn.decode()).encode()) + self.handler.wfile.write( + "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format( + ws_key_toreturn.decode() + ).encode() + ) def get_header(self, size, opcode): ws_first_byte = 0b10000000 | (opcode & 0x0F) - if (size > 2**16 - 1): + if size > 2 ** 16 - 1: # frame size can be increased up to 2^64 by setting the size to 127 # anything beyond that would need to be segmented into frames. i don't really think we'll need more. - return bytes([ - ws_first_byte, - 127, - (size >> 56) & 0xff, - (size >> 48) & 0xff, - (size >> 40) & 0xff, - (size >> 32) & 0xff, - (size >> 24) & 0xff, - (size >> 16) & 0xff, - (size >> 8) & 0xff, - size & 0xff - ]) - elif (size > 125): + return bytes( + [ + ws_first_byte, + 127, + (size >> 56) & 0xFF, + (size >> 48) & 0xFF, + (size >> 40) & 0xFF, + (size >> 32) & 0xFF, + (size >> 24) & 0xFF, + (size >> 16) & 0xFF, + (size >> 8) & 0xFF, + size & 0xFF, + ] + ) + elif size > 125: # up to 2^16 can be sent using the extended payload size field by putting the size to 126 - return bytes([ - ws_first_byte, - 126, - (size >> 8) & 0xff, - size & 0xff - ]) + return bytes([ws_first_byte, 126, (size >> 8) & 0xFF, size & 0xFF]) else: # 125 bytes binary message in a single unmasked frame return bytes([ws_first_byte, size]) def send(self, data): # convenience - if (type(data) == dict): + if type(data) == dict: # allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway. - data = json.dumps(data, allow_nan = False) + data = json.dumps(data, allow_nan=False) # string-type messages are sent as text frames - if (type(data) == str): + if type(data) == str: header = self.get_header(len(data), 1) - data_to_send = header + data.encode('utf-8') + data_to_send = header + data.encode("utf-8") # anything else as binary else: header = self.get_header(len(data), 2) data_to_send = header + data written = self.handler.wfile.write(data_to_send) - if (written != len(data_to_send)): + if written != len(data_to_send): logger.error("incomplete write! closing socket!") self.close() else: @@ -73,25 +80,25 @@ def send(self, data): def read_loop(self): open = True - while (open): + while open: header = self.handler.rfile.read(2) opcode = header[0] & 0x0F length = header[1] & 0x7F mask = (header[1] & 0x80) >> 7 - if (length == 126): + if length == 126: header = self.handler.rfile.read(2) length = (header[0] << 8) + header[1] - if (mask): + if mask: masking_key = self.handler.rfile.read(4) data = self.handler.rfile.read(length) - if (mask): + if mask: data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) - if (opcode == 1): - message = data.decode('utf-8') + if opcode == 1: + message = data.decode("utf-8") self.messageHandler.handleTextMessage(self, message) - elif (opcode == 2): + elif opcode == 2: self.messageHandler.handleBinaryMessage(self, data) - elif (opcode == 8): + elif opcode == 8: open = False self.messageHandler.handleClose(self) else: diff --git a/owrx/wsjt.py b/owrx/wsjt.py index fb97ac8c9..0a401a4db 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -12,6 +12,7 @@ from owrx.bands import Bandplan import logging + logger = logging.getLogger(__name__) @@ -29,9 +30,7 @@ def __init__(self, source): def getWaveFile(self): filename = "{tmp_dir}/openwebrx-wsjtchopper-{id}-{timestamp}.wav".format( - tmp_dir = self.tmp_dir, - id = id(self), - timestamp = datetime.utcnow().strftime(self.fileTimestampFormat) + tmp_dir=self.tmp_dir, id=id(self), timestamp=datetime.utcnow().strftime(self.fileTimestampFormat) ) wavefile = wave.open(filename, "wb") wavefile.setnchannels(1) @@ -44,13 +43,13 @@ def getNextDecodingTime(self): zeroed = t.replace(minute=0, second=0, microsecond=0) delta = t - zeroed seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval - t = zeroed + timedelta(seconds = seconds) + t = zeroed + timedelta(seconds=seconds) logger.debug("scheduling: {0}".format(t)) return t.timestamp() def startScheduler(self): self._scheduleNextSwitch() - threading.Thread(target = self.scheduler.run).start() + threading.Thread(target=self.scheduler.run).start() def emptyScheduler(self): for event in self.scheduler.queue: @@ -132,7 +131,7 @@ def __init__(self, source): super().__init__(source) def decoder_commandline(self, file): - #TODO expose decoding quality parameters through config + # TODO expose decoding quality parameters through config return ["jt9", "--ft8", "-d", "3", file] @@ -143,7 +142,7 @@ def __init__(self, source): super().__init__(source) def decoder_commandline(self, file): - #TODO expose decoding quality parameters through config + # TODO expose decoding quality parameters through config return ["wsprd", "-d", file] @@ -154,7 +153,7 @@ def __init__(self, source): super().__init__(source) def decoder_commandline(self, file): - #TODO expose decoding quality parameters through config + # TODO expose decoding quality parameters through config return ["jt9", "--jt65", "-d", "3", file] @@ -165,7 +164,7 @@ def __init__(self, source): super().__init__(source) def decoder_commandline(self, file): - #TODO expose decoding quality parameters through config + # TODO expose decoding quality parameters through config return ["jt9", "--jt9", "-d", "3", file] @@ -176,7 +175,7 @@ def __init__(self, source): super().__init__(source) def decoder_commandline(self, file): - #TODO expose decoding quality parameters through config + # TODO expose decoding quality parameters through config return ["jt9", "--ft4", "-d", "3", file] @@ -189,12 +188,7 @@ def __init__(self, handler): self.dial_freq = None self.band = None - modes = { - "~": "FT8", - "#": "JT65", - "@": "JT9", - "+": "FT4" - } + modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"} def parse(self, data): try: @@ -230,8 +224,8 @@ def parse_from_jt9(self, msg): dateformat = "%H%M" else: dateformat = "%H%M%S" - timestamp = self.parse_timestamp(msg[0:len(dateformat)], dateformat) - msg = msg[len(dateformat) + 1:] + timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat) + msg = msg[len(dateformat) + 1 :] modeChar = msg[14:15] mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" wsjt_msg = msg[17:53].strip() @@ -242,7 +236,7 @@ def parse_from_jt9(self, msg): "dt": float(msg[4:8]), "freq": int(msg[9:13]), "mode": mode, - "msg": wsjt_msg + "msg": wsjt_msg, } def parseLocator(self, msg, mode): @@ -268,7 +262,7 @@ def parse_from_wsprd(self, msg): "freq": float(msg[14:24]), "drift": int(msg[25:28]), "mode": "WSPR", - "msg": wsjt_msg + "msg": wsjt_msg, } def parseWsprMessage(self, msg): diff --git a/sdrhu.py b/sdrhu.py index 3060789a5..87459b359 100755 --- a/sdrhu.py +++ b/sdrhu.py @@ -23,10 +23,9 @@ from owrx.sdrhu import SdrHuUpdater from owrx.config import PropertyManager -if __name__=="__main__": +if __name__ == "__main__": pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") if not "sdrhu_key" in pm: exit(1) SdrHuUpdater().update() - From 35f8daee292defebd1212992b41ca546a3091139 Mon Sep 17 00:00:00 2001 From: D0han Date: Sun, 21 Jul 2019 20:19:33 +0200 Subject: [PATCH 0270/2616] Allow openwebrx.py to be run as normal executable --- README.md | 6 +----- csdr.py | 0 openwebrx.py | 2 ++ 3 files changed, 3 insertions(+), 5 deletions(-) mode change 100755 => 100644 csdr.py mode change 100644 => 100755 openwebrx.py diff --git a/README.md b/README.md index 1df5b19b3..68178af70 100644 --- a/README.md +++ b/README.md @@ -90,12 +90,8 @@ Optional Dependency if you want to decode WSJT-X modes: After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server: - python openwebrx.py + ./openwebrx.py -You may need to specify the Python version explicitly if your distribution still defaults to Python 2: - - python3 openwebrx.py - You can now open the GUI at http://localhost:8073. Please note that the server is also listening on the following ports (on localhost only): diff --git a/csdr.py b/csdr.py old mode 100755 new mode 100644 diff --git a/openwebrx.py b/openwebrx.py old mode 100644 new mode 100755 index 99b1419d8..42ea3bad9 --- a/openwebrx.py +++ b/openwebrx.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + from http.server import HTTPServer from owrx.http import RequestHandler from owrx.config import PropertyManager From 6c2488f05225f1ab102c5aa44d1a6be50eb4fb0c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 21 Jul 2019 22:12:41 +0200 Subject: [PATCH 0271/2616] fix shadowing warning --- owrx/source.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/owrx/source.py b/owrx/source.py index da2e928a8..9bf56473f 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -121,12 +121,12 @@ def getCommand(self): def getFormatConversion(self): return None - def activateProfile(self, id=None): + def activateProfile(self, profile_id=None): profiles = self.props["profiles"] - if id is None: - id = list(profiles.keys())[0] - logger.debug("activating profile {0}".format(id)) - profile = profiles[id] + if profile_id is None: + profile_id = list(profiles.keys())[0] + logger.debug("activating profile {0}".format(profile_id)) + profile = profiles[profile_id] for (key, value) in profile.items(): # skip the name, that would overwrite the source name. if key == "name": From 2d6b0f187764555213e89e4bbd59b4d4d89aff06 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 21 Jul 2019 22:13:20 +0200 Subject: [PATCH 0272/2616] try to catch a failing sdr device --- owrx/source.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/owrx/source.py b/owrx/source.py index 9bf56473f..86348eafb 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -91,6 +91,10 @@ def getSources(): return SdrService.sources +class SdrSourceException(Exception): + pass + + class SdrSource(object): def __init__(self, props, port): self.props = props @@ -183,6 +187,8 @@ def start(self): self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp) logger.info("Started rtl source: " + cmd) + available = False + def wait_for_process_to_end(): rc = self.process.wait() logger.debug("shut down with RC={0}".format(rc)) @@ -191,17 +197,25 @@ def wait_for_process_to_end(): self.monitor = threading.Thread(target=wait_for_process_to_end) self.monitor.start() - while True: + retries = 100 + while retries > 0: + retries -= 1 + if self.monitor is None: + break testsock = socket.socket() try: testsock.connect(("127.0.0.1", self.getPort())) testsock.close() + available = True break except: time.sleep(0.1) self.modificationLock.release() + if not available: + raise SdrSourceException("rtl source failed to start up") + for c in self.clients: c.onSdrAvailable() From 9c927d90018ec45f6906fe3feda9fe8c45b15b0e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 21 Jul 2019 23:39:11 +0200 Subject: [PATCH 0273/2616] first iteration of background services --- openwebrx.py | 3 ++ owrx/bands.py | 2 +- owrx/connection.py | 2 +- owrx/service.py | 117 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 owrx/service.py diff --git a/openwebrx.py b/openwebrx.py index de51f665c..9fa685bd8 100755 --- a/openwebrx.py +++ b/openwebrx.py @@ -7,6 +7,7 @@ from owrx.source import SdrService, ClientRegistry from socketserver import ThreadingMixIn from owrx.sdrhu import SdrHuUpdater +from owrx.service import ServiceManager import logging @@ -47,6 +48,8 @@ def main(): updater = SdrHuUpdater() updater.start() + ServiceManager.getSharedInstance().start() + server = ThreadedHttpServer(("0.0.0.0", pm.getPropertyValue("web_port")), RequestHandler) server.serve_forever() diff --git a/owrx/bands.py b/owrx/bands.py index bc76b2abe..c32caec55 100644 --- a/owrx/bands.py +++ b/owrx/bands.py @@ -54,5 +54,5 @@ def __init__(self): def findBand(self, freq): return next(band for band in self.bands if band.inBand(freq)) - def collectDialFrequencis(self, range): + def collectDialFrequencies(self, range): return [e for b in self.bands for e in b.getDialFrequencies(range)] diff --git a/owrx/connection.py b/owrx/connection.py index b68cbaf37..5008e363f 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -118,7 +118,7 @@ def sendConfig(key, value): cf = configProps["center_freq"] srh = configProps["samp_rate"] / 2 frequencyRange = (cf - srh, cf + srh) - self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencis(frequencyRange)) + self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencies(frequencyRange)) self.configSub = configProps.wire(sendConfig) sendConfig(None, None) diff --git a/owrx/service.py b/owrx/service.py new file mode 100644 index 000000000..84a436e73 --- /dev/null +++ b/owrx/service.py @@ -0,0 +1,117 @@ +import threading +from owrx.source import SdrService +from owrx.bands import Bandplan +from csdr import dsp, output +from owrx.wsjt import WsjtParser + +import logging + +logger = logging.getLogger(__name__) + + +class ServiceOutput(output): + def __init__(self, frequency): + self.frequency = frequency + + def add_output(self, t, read_fn): + logger.debug("got output of type {0}".format(t)) + + def pump(read, write): + def copy(): + run = True + while run: + data = read() + if data is None or (isinstance(data, bytes) and len(data) == 0): + logger.warning("zero read on {0}".format(t)) + run = False + else: + write(data) + + return copy + + if t == "wsjt_demod": + parser = WsjtParser(WsjtHandler()) + parser.setDialFrequency(self.frequency) + target = pump(read_fn, parser.parse) + else: + # dump everything else + # TODO rewrite the output mechanism in a way that avoids producing unnecessary data + target = pump(read_fn, lambda x: None) + threading.Thread(target=target).start() + + +class ServiceHandler(object): + def __init__(self, source): + self.services = [] + self.source = source + self.source.addClient(self) + self.source.getProps().collect("center_freq", "samp_rate").wire(self.onFrequencyChange) + self.onFrequencyChange("", "") + + def onSdrAvailable(self): + logger.debug("sdr {0} is available".format(self.source.getName())) + self.onFrequencyChange("", "") + + def onSdrUnavailable(self): + logger.debug("sdr {0} is unavailable".format(self.source.getName())) + self.stopServices() + + def isSupported(self, mode): + return mode in ["ft8", "ft4", "wspr"] + + def stopServices(self): + for service in self.services: + service.stop() + self.services = [] + + def startServices(self): + for service in self.services: + service.start() + + def onFrequencyChange(self, key, value): + if not self.source.isAvailable(): + return + logger.debug("sdr {0} is changing frequency".format(self.source.getName())) + self.stopServices() + cf = self.source.getProps()["center_freq"] + srh = self.source.getProps()["samp_rate"] / 2 + frequency_range = (cf - srh, cf + srh) + self.services = [self.setupService(dial["mode"], dial["frequency"]) for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range) if self.isSupported(dial["mode"])] + + def setupService(self, mode, frequency): + logger.debug("setting up service {0} on frequency {1}".format(mode, frequency)) + d = dsp(ServiceOutput(frequency)) + d.set_offset_freq(frequency - self.source.getProps()["center_freq"]) + d.set_demodulator("usb") + d.set_bpf(0, 3000) + d.set_secondary_demodulator(mode) + d.set_audio_compression("none") + d.set_samp_rate(self.source.getProps()["samp_rate"]) + d.start() + return d + + +class WsjtHandler(object): + def write_wsjt_message(self, msg): + pass + + +class ServiceManager(object): + sharedInstance = None + @staticmethod + def getSharedInstance(): + if ServiceManager.sharedInstance is None: + ServiceManager.sharedInstance = ServiceManager() + return ServiceManager.sharedInstance + + def start(self): + for source in SdrService.getSources().values(): + ServiceHandler(source) + + +class Service(object): + pass + + +class WsjtService(Service): + pass From eb9bc5f8dcd9a96108ad76c2dc3dc4e27ff943cc Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 22 Jul 2019 23:24:46 +0200 Subject: [PATCH 0274/2616] add ft4 frequencies, if available --- bands.json | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/bands.json b/bands.json index 487ef29a2..df7bce72e 100644 --- a/bands.json +++ b/bands.json @@ -20,7 +20,8 @@ "ft8": 3573000, "wspr": 3592600, "jt65": 3570000, - "jt9": 3572000 + "jt9": 3572000, + "ft4": 3568000 } }, { @@ -41,7 +42,8 @@ "ft8": 7074000, "wspr": 7038600, "jt65": 7076000, - "jt9": 7078000 + "jt9": 7078000, + "ft4": 7047500 } }, { @@ -53,7 +55,8 @@ "ft8": 10136000, "wspr": 10138700, "jt65": 10138000, - "jt9": 10140000 + "jt9": 10140000, + "ft4": 10140000 } }, { @@ -78,7 +81,8 @@ "ft8": 18100000, "wspr": 18104600, "jt65": 18102000, - "jt9": 18104000 + "jt9": 18104000, + "ft4": 18104000 } }, { @@ -90,7 +94,8 @@ "ft8": 21074000, "wspr": 21094600, "jt65": 21076000, - "jt9": 21078000 + "jt9": 21078000, + "ft4": 21140000 } }, { @@ -102,7 +107,8 @@ "ft8": 24915000, "wspr": 24924600, "jt65": 24917000, - "jt9": 24919000 + "jt9": 24919000, + "ft4": 24919000 } }, { @@ -114,7 +120,8 @@ "ft8": 28074000, "wspr": 28124600, "jt65": 28076000, - "jt9": 28078000 + "jt9": 28078000, + "ft4": 28180000 } }, { @@ -126,7 +133,8 @@ "ft8": 50313000, "wspr": 50293000, "jt65": 50310000, - "jt9": 50312000 + "jt9": 50312000, + "ft4": 50318000 } }, { From 8c2cefe304889ed0f5f48f133838f7686df14391 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 23 Jul 2019 16:43:46 +0100 Subject: [PATCH 0275/2616] pass the nmux port on (defaults are bad...) --- owrx/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/owrx/service.py b/owrx/service.py index 84a436e73..641ddb45f 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -81,6 +81,7 @@ def onFrequencyChange(self, key, value): def setupService(self, mode, frequency): logger.debug("setting up service {0} on frequency {1}".format(mode, frequency)) d = dsp(ServiceOutput(frequency)) + d.nc_port = self.source.getPort() d.set_offset_freq(frequency - self.source.getProps()["center_freq"]) d.set_demodulator("usb") d.set_bpf(0, 3000) From 7689e31640b8fcc21f461086b65be7fd443d8923 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 23 Jul 2019 20:28:51 +0100 Subject: [PATCH 0276/2616] increase timeout --- owrx/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/source.py b/owrx/source.py index 86348eafb..99f29ec08 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -197,7 +197,7 @@ def wait_for_process_to_end(): self.monitor = threading.Thread(target=wait_for_process_to_end) self.monitor.start() - retries = 100 + retries = 1000 while retries > 0: retries -= 1 if self.monitor is None: From a15e6256924a2695c12944223d52cdf0a5df4786 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 28 Jul 2019 11:40:58 +0200 Subject: [PATCH 0277/2616] de-duplicate; better logging --- csdr.py | 25 +++++++++++++++++-------- owrx/service.py | 17 ++--------------- owrx/source.py | 15 +-------------- 3 files changed, 20 insertions(+), 37 deletions(-) diff --git a/csdr.py b/csdr.py index 54fd13c09..197f62cf4 100644 --- a/csdr.py +++ b/csdr.py @@ -39,6 +39,18 @@ def add_output(self, type, read_fn): def reset(self): pass + def pump(self, read, write): + def copy(): + run = True + while run: + data = read() + if data is None or (isinstance(data, bytes) and len(data) == 0): + run = False + else: + write(data) + + return copy + class dsp(object): def __init__(self, output): @@ -233,7 +245,7 @@ def secondary_bw(self): def start_secondary_demodulator(self): if not self.secondary_demodulator: return - logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate()) + logger.debug("starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate()) secondary_command_fft = self.secondary_chain("fft") secondary_command_demod = self.secondary_chain(self.secondary_demodulator) self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft) @@ -255,8 +267,8 @@ def start_secondary_demodulator(self): last_decimation=self.last_decimation, ) - logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft) - logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (demod) = %s", secondary_command_demod) + logger.debug("secondary command (fft) = %s", secondary_command_fft) + logger.debug("secondary command (demod) = %s", secondary_command_demod) my_env = os.environ.copy() # if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; if self.csdr_print_bufsizes: @@ -264,11 +276,9 @@ def start_secondary_demodulator(self): self.secondary_process_fft = subprocess.Popen( secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env ) - logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)") self.secondary_process_demod = subprocess.Popen( secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env - ) # TODO digimodes - logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") # TODO digimodes + ) self.secondary_processes_running = True self.output.add_output( @@ -500,7 +510,6 @@ def start(self): self.running = True command_base = " | ".join(self.chain(self.demodulator)) - logger.debug(command_base) # create control pipes for csdr self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self)) @@ -533,7 +542,7 @@ def start(self): audio_rate=self.get_audio_rate(), ) - logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) + logger.debug("Command = %s", command) my_env = os.environ.copy() if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1" diff --git a/owrx/service.py b/owrx/service.py index 641ddb45f..8c997e3ce 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -16,27 +16,14 @@ def __init__(self, frequency): def add_output(self, t, read_fn): logger.debug("got output of type {0}".format(t)) - def pump(read, write): - def copy(): - run = True - while run: - data = read() - if data is None or (isinstance(data, bytes) and len(data) == 0): - logger.warning("zero read on {0}".format(t)) - run = False - else: - write(data) - - return copy - if t == "wsjt_demod": parser = WsjtParser(WsjtHandler()) parser.setDialFrequency(self.frequency) - target = pump(read_fn, parser.parse) + target = self.pump(read_fn, parser.parse) else: # dump everything else # TODO rewrite the output mechanism in a way that avoids producing unnecessary data - target = pump(read_fn, lambda x: None) + target = self.pump(read_fn, lambda x: None) threading.Thread(target=target).start() diff --git a/owrx/source.py b/owrx/source.py index 99f29ec08..3191cc2e8 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -515,20 +515,7 @@ def add_output(self, t, read_fn): } write = writers[t] - def pump(read, write): - def copy(): - run = True - while run: - data = read() - if data is None or (isinstance(data, bytes) and len(data) == 0): - logger.warning("zero read on {0}".format(t)) - run = False - else: - write(data) - - return copy - - threading.Thread(target=pump(read_fn, write)).start() + threading.Thread(target=self.pump(read_fn, write)).start() def stop(self): self.dsp.stop() From accf2a34ff16e27d8dc409e46b4606def335614b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 28 Jul 2019 11:45:28 +0200 Subject: [PATCH 0278/2616] fix exception when outside of band --- owrx/bands.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/owrx/bands.py b/owrx/bands.py index bc76b2abe..e4cb5ef1c 100644 --- a/owrx/bands.py +++ b/owrx/bands.py @@ -51,8 +51,15 @@ def __init__(self): f.close() self.bands = [Band(d) for d in bands_json] + def findBands(self, freq): + return [band for band in self.bands if band.inBand(freq)] + def findBand(self, freq): - return next(band for band in self.bands if band.inBand(freq)) + bands = self.findBands(freq) + if bands: + return bands[0] + else: + return None def collectDialFrequencis(self, range): return [e for b in self.bands for e in b.getDialFrequencies(range)] From fa08009c501a5a2b6b1af5480a3f0bf6f01dfb19 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 28 Jul 2019 12:11:22 +0200 Subject: [PATCH 0279/2616] more logging improvements --- csdr.py | 3 +++ owrx/service.py | 14 +++++++------- owrx/wsjt.py | 6 ++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/csdr.py b/csdr.py index 197f62cf4..a67631d11 100644 --- a/csdr.py +++ b/csdr.py @@ -499,6 +499,9 @@ def try_delete_pipes(self, pipe_names): if pipe_path: try: os.unlink(pipe_path) + except FileNotFoundError: + # it seems like we keep calling this twice. no idea why, but we don't need the resulting error. + pass except Exception: logger.exception("try_delete_pipes()") diff --git a/owrx/service.py b/owrx/service.py index 8c997e3ce..577d29af9 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -14,8 +14,6 @@ def __init__(self, frequency): self.frequency = frequency def add_output(self, t, read_fn): - logger.debug("got output of type {0}".format(t)) - if t == "wsjt_demod": parser = WsjtParser(WsjtHandler()) parser.setDialFrequency(self.frequency) @@ -33,17 +31,16 @@ def __init__(self, source): self.source = source self.source.addClient(self) self.source.getProps().collect("center_freq", "samp_rate").wire(self.onFrequencyChange) - self.onFrequencyChange("", "") + self.updateServices() def onSdrAvailable(self): - logger.debug("sdr {0} is available".format(self.source.getName())) - self.onFrequencyChange("", "") + self.updateServices() def onSdrUnavailable(self): - logger.debug("sdr {0} is unavailable".format(self.source.getName())) self.stopServices() def isSupported(self, mode): + # TODO make configurable return mode in ["ft8", "ft4", "wspr"] def stopServices(self): @@ -58,7 +55,10 @@ def startServices(self): def onFrequencyChange(self, key, value): if not self.source.isAvailable(): return - logger.debug("sdr {0} is changing frequency".format(self.source.getName())) + self.updateServices() + + def updateServices(self): + logger.debug("re-scheduling services due to sdr changes") self.stopServices() cf = self.source.getProps()["center_freq"] srh = self.source.getProps()["samp_rate"] / 2 diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 0a401a4db..2c26b2fae 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -84,11 +84,10 @@ def decode_and_unlink(file): break self.outputWriter.send(line) rc = decoder.wait() - logger.debug("decoder return code: %i", rc) + if rc != 0: + logger.warning("decoder return code: %i", rc) os.unlink(file) - self.decoder = decoder - if self.fileQueue: file = self.fileQueue.pop() logger.debug("processing file {0}".format(file)) @@ -100,7 +99,6 @@ def run(self) -> None: while self.doRun: data = self.source.read(256) if data is None or (isinstance(data, bytes) and len(data) == 0): - logger.warning("zero read on WSJT chopper") self.doRun = False else: self.switchingLock.acquire() From 98c5e9e15baaa7e5fbb93ebb377cafc2a5ebeed2 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 28 Jul 2019 13:29:45 +0200 Subject: [PATCH 0280/2616] allow service configuration --- config_webrx.py | 3 +++ owrx/service.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/config_webrx.py b/config_webrx.py index 15569de5e..044dc851b 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -251,3 +251,6 @@ map_position_retention_time = 2 * 60 * 60 temporary_directory = "/tmp" + +services_enabled = True +services_decoders = ["ft8", "ft4", "wspr"] diff --git a/owrx/service.py b/owrx/service.py index 577d29af9..b6c2731af 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -3,6 +3,7 @@ from owrx.bands import Bandplan from csdr import dsp, output from owrx.wsjt import WsjtParser +from owrx.config import PropertyManager import logging @@ -41,7 +42,7 @@ def onSdrUnavailable(self): def isSupported(self, mode): # TODO make configurable - return mode in ["ft8", "ft4", "wspr"] + return mode in PropertyManager.getSharedInstance()["services_decoders"] def stopServices(self): for service in self.services: @@ -93,6 +94,8 @@ def getSharedInstance(): return ServiceManager.sharedInstance def start(self): + if not PropertyManager.getSharedInstance()["services_enabled"]: + return for source in SdrService.getSources().values(): ServiceHandler(source) From 6e7d99376d139f61c9e8fcef404bdb90c67f3c6e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 28 Jul 2019 15:28:39 +0200 Subject: [PATCH 0281/2616] color by band --- htdocs/lib/chroma.min.js | 58 ++++++++++++++++++++++++++++++++++++++++ htdocs/map.html | 1 + htdocs/map.js | 33 +++++++++++++++++++++-- 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 htdocs/lib/chroma.min.js diff --git a/htdocs/lib/chroma.min.js b/htdocs/lib/chroma.min.js new file mode 100644 index 000000000..76dd1f840 --- /dev/null +++ b/htdocs/lib/chroma.min.js @@ -0,0 +1,58 @@ +/** + * chroma.js - JavaScript library for color conversions + * + * Copyright (c) 2011-2019, Gregor Aisch + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name Gregor Aisch may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL GREGOR AISCH OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ------------------------------------------------------- + * + * chroma.js includes colors from colorbrewer2.org, which are released under + * the following license: + * + * Copyright (c) 2002 Cynthia Brewer, Mark Harrower, + * and The Pennsylvania State University. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + * ------------------------------------------------------ + * + * Named colors are taken from X11 Color Names. + * http://www.w3.org/TR/css3-color/#svg-color + * + * @preserve + */ + +!function(r,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):r.chroma=e()}(this,function(){"use strict";for(var t=function(r,e,t){return void 0===e&&(e=0),void 0===t&&(t=1),r>16,e>>8&255,255&e,1]}if(r.match(fr)){9===r.length&&(r=r.substr(1));var t=parseInt(r,16);return[t>>24&255,t>>16&255,t>>8&255,Math.round((255&t)/255*100)/100]}throw new Error("unknown hex color: "+r)},ur=o.type;A.prototype.hex=function(r){return nr(this._rgb,r)},N.hex=function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];return new(Function.prototype.bind.apply(A,[null].concat(r,["hex"])))},b.format.hex=or,b.autodetect.push({p:4,test:function(r){for(var e=[],t=arguments.length-1;0>16,r>>8&255,255&r,1];throw new Error("unknown num color: "+r)},xe=o.type;A.prototype.num=function(){return Me(this._rgb)},N.num=function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];return new(Function.prototype.bind.apply(A,[null].concat(r,["num"])))},b.format.num=_e,b.autodetect.push({p:5,test:function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];if(1===r.length&&"number"===xe(r[0])&&0<=r[0]&&r[0]<=16777215)return"num"}});var Ae=o.unpack,Ee=o.type,Pe=Math.round;A.prototype.rgb=function(r){return void 0===r&&(r=!0),!1===r?this._rgb.slice(0,3):this._rgb.slice(0,3).map(Pe)},A.prototype.rgba=function(t){return void 0===t&&(t=!0),this._rgb.slice(0,4).map(function(r,e){return e<3?!1===t?r:Pe(r):r})},N.rgb=function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];return new(Function.prototype.bind.apply(A,[null].concat(r,["rgb"])))},b.format.rgb=function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];var t=Ae(r,"rgba");return void 0===t[3]&&(t[3]=1),t},b.autodetect.push({p:3,test:function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];if(r=Ae(r,"rgba"),"array"===Ee(r)&&(3===r.length||4===r.length&&"number"==Ee(r[3])&&0<=r[3]&&r[3]<=1))return"rgb"}});var Fe=Math.log,Oe=function(r){var e,t,n,a=r/100;return n=a<66?(e=255,t=-155.25485562709179-.44596950469579133*(t=a-2)+104.49216199393888*Fe(t),a<20?0:.8274096064007395*(n=a-10)-254.76935184120902+115.67994401066147*Fe(n)):(e=351.97690566805693+.114206453784165*(e=a-55)-40.25366309332127*Fe(e),t=325.4494125711974+.07943456536662342*(t=a-50)-28.0852963507957*Fe(t),255),[e,t,n,1]},je=o.unpack,Ge=Math.round,qe=function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];for(var t,n=je(r,"rgb"),a=n[0],f=n[2],o=1e3,u=4e4;.4=f/a?u=t:o=t}return Ge(t)};A.prototype.temp=A.prototype.kelvin=A.prototype.temperature=function(){return qe(this._rgb)},N.temp=N.kelvin=N.temperature=function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];return new(Function.prototype.bind.apply(A,[null].concat(r,["temp"])))},b.format.temp=b.format.kelvin=b.format.temperature=Oe;var Le=o.type;A.prototype.alpha=function(r,e){return void 0===e&&(e=!1),void 0!==r&&"number"===Le(r)?e?(this._rgb[3]=r,this):new A([this._rgb[0],this._rgb[1],this._rgb[2],r],"rgb"):this._rgb[3]},A.prototype.clipped=function(){return this._rgb._clipped||!1},A.prototype.darken=function(r){void 0===r&&(r=1);var e=this.lab();return e[0]-=qr*r,new A(e,"lab").alpha(this.alpha(),!0)},A.prototype.brighten=function(r){return void 0===r&&(r=1),this.darken(-r)},A.prototype.darker=A.prototype.darken,A.prototype.brighter=A.prototype.brighten,A.prototype.get=function(r){var e=r.split("."),t=e[0],n=e[1],a=this[t]();if(n){var f=t.indexOf(n);if(-1=s[t];)t++;return t-1}(r)/(s.length-2):g!==p?(r-p)/(g-p):1;e||(n=w(n)),1!==y&&(n=tt(n,y)),n=d[0]+n*(1-d[0]-d[1]),n=Math.min(1,Math.max(0,n));var a=Math.floor(1e4*n);if(m&&v[a])t=v[a];else{if("array"===et(b))for(var f=0;ft.max&&(t.max=r),t.count+=1)}),t.domain=[t.min,t.max],t.limits=function(r,e){return Mt(t,r,e)},t},Mt=function(r,e,t){void 0===e&&(e="equal"),void 0===t&&(t=7),"array"==Y(r)&&(r=kt(r));var n=r.min,a=r.max,f=r.values.sort(function(r,e){return r-e});if(1===t)return[n,a];var o=[];if("c"===e.substr(0,1)&&(o.push(n),o.push(a)),"e"===e.substr(0,1)){o.push(n);for(var u=1;u 0");var c=Math.LOG10E*vt(n),i=Math.LOG10E*vt(a);o.push(n);for(var l=1;l OpenWebRX Map + diff --git a/htdocs/map.js b/htdocs/map.js index 6af68ca3e..ba7e15172 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -31,6 +31,34 @@ var strokeOpacity = 0.8; var fillOpacity = 0.35; + var colorKeys = {}; + var getColor = function(id){ + if (!id) return "#000000"; + if (!colorKeys[id]) { + var keys = Object.keys(colorKeys); + keys.push(id); + keys.sort(); + var colors = chroma.scale(['#FF0000', '#0000FF']).colors(keys.length); + colorKeys = {}; + keys.forEach(function(key, index) { + colorKeys[key] = colors[index]; + }); + reColor(); + } + return colorKeys[id]; + } + + // when the color palette changes, update all grid squares with new color + var reColor = function() { + $.each(rectangles, function(_, r) { + var color = getColor(r.band); + r.setOptions({ + strokeColor: color, + fillColor: color + }); + }); + } + var processUpdates = function(updates) { if (!map) { updateQueue = updateQueue.concat(updates); @@ -73,6 +101,7 @@ var lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]) * 2; var center = new google.maps.LatLng({lat: lat + .5, lng: lon + 1}); var rectangle; + var color = getColor(update.band); if (rectangles[update.callsign]) { rectangle = rectangles[update.callsign]; } else { @@ -83,9 +112,9 @@ rectangles[update.callsign] = rectangle; } rectangle.setOptions($.extend({ - strokeColor: '#FF0000', + strokeColor: color, strokeWeight: 2, - fillColor: '#FF0000', + fillColor: color, map: map, bounds:{ north: lat, From 74dddcb8ad66bee5260bfba5014d7bd53ffe1276 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 28 Jul 2019 15:57:33 +0200 Subject: [PATCH 0282/2616] add simple legend with colors --- htdocs/css/map.css | 19 ++++++++++++++++++- htdocs/map.html | 1 + htdocs/map.js | 9 +++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/htdocs/css/map.css b/htdocs/css/map.css index 4b27afe13..61702184f 100644 --- a/htdocs/css/map.css +++ b/htdocs/css/map.css @@ -28,4 +28,21 @@ ul { margin-block-start: 5px; margin-block-end: 5px; padding-inline-start: 25px; -} \ No newline at end of file +} + +.openwebrx-map-legend { + background-color: #fff; + padding: 10px; +} + +.openwebrx-map-legend ul { + list-style-type: none; + padding: 0; +} + +.openwebrx-map-legend li.square .illustration { + display: inline-block; + width: 30px; + height: 20px; + margin-right: 10px; +} diff --git a/htdocs/map.html b/htdocs/map.html index bd8b60a7d..9e03c407f 100644 --- a/htdocs/map.html +++ b/htdocs/map.html @@ -12,5 +12,6 @@ ${header}
    +

    Colors

    diff --git a/htdocs/map.js b/htdocs/map.js index ba7e15172..c958f940a 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -44,6 +44,7 @@ colorKeys[key] = colors[index]; }); reColor(); + updateLegend(); } return colorKeys[id]; } @@ -59,6 +60,13 @@ }); } + var updateLegend = function() { + var lis = $.map(colorKeys, function(value, key) { + return '
  • ' + key + '
  • '; + }); + $(".openwebrx-map-legend .content").html('
      ' + lis.join('') + '
    '); + } + var processUpdates = function(updates) { if (!map) { updateQueue = updateQueue.concat(updates); @@ -183,6 +191,7 @@ nite.init(map); setInterval(function() { nite.refresh() }, 10000); // every 10s }); + map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($(".openwebrx-map-legend")[0]); }); retention_time = config.map_position_retention_time * 1000; break; From 30d8b1327b0ded8067a2fa772eb12d6abb3f0cbf Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 28 Jul 2019 15:59:54 +0200 Subject: [PATCH 0283/2616] give it some space --- htdocs/css/map.css | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/css/map.css b/htdocs/css/map.css index 61702184f..73d556daa 100644 --- a/htdocs/css/map.css +++ b/htdocs/css/map.css @@ -33,6 +33,7 @@ ul { .openwebrx-map-legend { background-color: #fff; padding: 10px; + margin: 10px; } .openwebrx-map-legend ul { From ff98b172c43f9d3ebda970cd41523f9c4f6decf0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 28 Jul 2019 16:17:23 +0200 Subject: [PATCH 0284/2616] add option to select coloring by mode, too --- htdocs/css/map.css | 6 ++++++ htdocs/map.html | 9 ++++++++- htdocs/map.js | 24 ++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/htdocs/css/map.css b/htdocs/css/map.css index 73d556daa..6d09b702a 100644 --- a/htdocs/css/map.css +++ b/htdocs/css/map.css @@ -47,3 +47,9 @@ ul { height: 20px; margin-right: 10px; } + +.openwebrx-map-legend select { + background-color: #FFF; + border-color: #DDD; + padding: 5px; +} diff --git a/htdocs/map.html b/htdocs/map.html index 9e03c407f..2aed0103d 100644 --- a/htdocs/map.html +++ b/htdocs/map.html @@ -12,6 +12,13 @@ ${header}
    -

    Colors

    +
    +

    Colors

    + +
    +
    diff --git a/htdocs/map.js b/htdocs/map.js index c958f940a..4d068f5b3 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -52,7 +52,7 @@ // when the color palette changes, update all grid squares with new color var reColor = function() { $.each(rectangles, function(_, r) { - var color = getColor(r.band); + var color = getColor(colorAccessor(r)); r.setOptions({ strokeColor: color, fillColor: color @@ -60,6 +60,25 @@ }); } + var colorMode = 'byband'; + var colorAccessor = function(r) { + switch (colorMode) { + case 'byband': + return r.band; + case 'bymode': + return r.mode; + } + }; + + $(function(){ + $('#openwebrx-map-colormode').on('change', function(){ + colorMode = $(this).val(); + colorKeys = {}; + reColor(); + updateLegend(); + }); + }); + var updateLegend = function() { var lis = $.map(colorKeys, function(value, key) { return '
  • ' + key + '
  • '; @@ -109,7 +128,8 @@ var lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]) * 2; var center = new google.maps.LatLng({lat: lat + .5, lng: lon + 1}); var rectangle; - var color = getColor(update.band); + // the accessor is designed to work on the rectangle... but it should work on the update object, too + var color = getColor(colorAccessor(update)); if (rectangles[update.callsign]) { rectangle = rectangles[update.callsign]; } else { From 785d43960596b5214b116da566aa54320edfe166 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 28 Jul 2019 16:26:03 +0200 Subject: [PATCH 0285/2616] play with the colors --- htdocs/map.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/htdocs/map.js b/htdocs/map.js index 4d068f5b3..01672ef9a 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -32,13 +32,14 @@ var fillOpacity = 0.35; var colorKeys = {}; + var colorScale = chroma.bezier(['red', 'blue', 'green']).scale(); var getColor = function(id){ if (!id) return "#000000"; if (!colorKeys[id]) { var keys = Object.keys(colorKeys); keys.push(id); keys.sort(); - var colors = chroma.scale(['#FF0000', '#0000FF']).colors(keys.length); + var colors = colorScale.colors(keys.length); colorKeys = {}; keys.forEach(function(key, index) { colorKeys[key] = colors[index]; From 3b5883dd55eb9caf089606436aa01eb0a0f9d97b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 28 Jul 2019 16:33:19 +0200 Subject: [PATCH 0286/2616] improved legend with opacity --- htdocs/css/map.css | 2 ++ htdocs/map.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/htdocs/css/map.css b/htdocs/css/map.css index 6d09b702a..5d478cdad 100644 --- a/htdocs/css/map.css +++ b/htdocs/css/map.css @@ -46,6 +46,8 @@ ul { width: 30px; height: 20px; margin-right: 10px; + border-width: 2px; + border-style: solid; } .openwebrx-map-legend select { diff --git a/htdocs/map.js b/htdocs/map.js index 01672ef9a..83801f0cf 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -82,7 +82,7 @@ var updateLegend = function() { var lis = $.map(colorKeys, function(value, key) { - return '
  • ' + key + '
  • '; + return '
  • ' + key + '
  • '; }); $(".openwebrx-map-legend .content").html('
      ' + lis.join('') + '
    '); } From e40b400f6f2e70febe44fd322083b44a980d63ac Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 28 Jul 2019 16:36:12 +0200 Subject: [PATCH 0287/2616] try to improve "moving" callsigns --- htdocs/map.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/htdocs/map.js b/htdocs/map.js index 83801f0cf..098b90924 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -136,7 +136,7 @@ } else { rectangle = new google.maps.Rectangle(); rectangle.addListener('click', function(){ - showLocatorInfoWindow(update.location.locator, center); + showLocatorInfoWindow(this.locator, this.center); }); rectangles[update.callsign] = rectangle; } @@ -156,6 +156,7 @@ rectangle.locator = update.location.locator; rectangle.mode = update.mode; rectangle.band = update.band; + rectangle.center = center; if (expectedLocator && expectedLocator == update.location.locator) { map.panTo(center); From 8f7f34c190853e9479efa7cd7655c85daab1163f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 28 Jul 2019 22:13:55 +0200 Subject: [PATCH 0288/2616] better colors (?) --- htdocs/map.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/map.js b/htdocs/map.js index 098b90924..7d98f4907 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -32,7 +32,7 @@ var fillOpacity = 0.35; var colorKeys = {}; - var colorScale = chroma.bezier(['red', 'blue', 'green']).scale(); + var colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl'); var getColor = function(id){ if (!id) return "#000000"; if (!colorKeys[id]) { From d1eaab771110ae2ce2ebe9cf1689e6c6d06ea1e3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 3 Aug 2019 23:44:56 +0200 Subject: [PATCH 0289/2616] delay startup of background services to increase user interface response --- owrx/service.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/owrx/service.py b/owrx/service.py index b6c2731af..d04ee0b61 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -30,12 +30,13 @@ class ServiceHandler(object): def __init__(self, source): self.services = [] self.source = source + self.startupTimer = None self.source.addClient(self) self.source.getProps().collect("center_freq", "samp_rate").wire(self.onFrequencyChange) - self.updateServices() + self.scheduleServiceStartup() def onSdrAvailable(self): - self.updateServices() + self.scheduleServiceStartup() def onSdrUnavailable(self): self.stopServices() @@ -54,9 +55,16 @@ def startServices(self): service.start() def onFrequencyChange(self, key, value): + self.stopServices() if not self.source.isAvailable(): return - self.updateServices() + self.scheduleServiceStartup() + + def scheduleServiceStartup(self): + if self.startupTimer: + self.startupTimer.cancel() + self.startupTimer = threading.Timer(10, self.updateServices) + self.startupTimer.start() def updateServices(self): logger.debug("re-scheduling services due to sdr changes") From 5337ddba8d52dba77e4ed696beec9bc16d06c206 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 3 Aug 2019 23:58:08 +0200 Subject: [PATCH 0290/2616] add 2m frequencies from wsjt-x --- bands.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bands.json b/bands.json index df7bce72e..a122916eb 100644 --- a/bands.json +++ b/bands.json @@ -150,7 +150,10 @@ "lower_bound": 144000000, "upper_bound": 146000000, "frequencies": { - "wspr": 144489000 + "wspr": 144489000, + "ft8": 144174000, + "ft4": 144170000, + "jt65": 144120000 } }, { From 441738e56948e81b34b63cca9d2fb00f79557570 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 4 Aug 2019 00:21:53 +0200 Subject: [PATCH 0291/2616] additional ft4 frequency on 80m --- bands.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bands.json b/bands.json index a122916eb..30a26bbdf 100644 --- a/bands.json +++ b/bands.json @@ -21,7 +21,7 @@ "wspr": 3592600, "jt65": 3570000, "jt9": 3572000, - "ft4": 3568000 + "ft4": [3568000, 3568000] } }, { From 42aae4c03a042b37d6a61d172ac03fac32c004ee Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 4 Aug 2019 14:55:56 +0200 Subject: [PATCH 0292/2616] save some cpu cycles by only running necessary stuff for services --- csdr.py | 83 +++++++++++++++++++++++++++++++------------------ owrx/service.py | 16 +++++----- owrx/source.py | 4 +-- 3 files changed, 62 insertions(+), 41 deletions(-) diff --git a/csdr.py b/csdr.py index a67631d11..08a0c4538 100644 --- a/csdr.py +++ b/csdr.py @@ -33,10 +33,15 @@ class output(object): - def add_output(self, type, read_fn): - pass + def send_output(self, t, read_fn): + if not self.supports_type(t): + # TODO rewrite the output mechanism in a way that avoids producing unnecessary data + logger.warning('dumping output of type %s since it is not supported.', t) + threading.Thread(target=self.pump(read_fn, lambda x: None)).start() + return + self.receive_output(t, read_fn) - def reset(self): + def receive_output(self, t, read_fn): pass def pump(self, read, write): @@ -51,6 +56,9 @@ def copy(): return copy + def supports_type(self, t): + return True + class dsp(object): def __init__(self, output): @@ -123,10 +131,19 @@ def chain(self, which): "csdr shift_addition_cc --fifo {shift_pipe}", "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING", "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING", - "csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}", + ] + if self.output.supports_type('smeter'): + chain += [ + "csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}", + ] if self.secondary_demodulator: - chain += ["csdr tee {iqtee_pipe}", "csdr tee {iqtee2_pipe}"] + if self.output.supports_type('secondary_fft'): + chain += ["csdr tee {iqtee_pipe}"] + chain += ["csdr tee {iqtee2_pipe}"] + # early exit if we don't want audio + if not self.output.supports_type('audio'): + return chain # safe some cpu cycles... no need to decimate if decimation factor is 1 last_decimation_block = ( ["csdr fractional_decimator_ff {last_decimation}"] if self.last_decimation != 1.0 else [] @@ -246,16 +263,9 @@ def start_secondary_demodulator(self): if not self.secondary_demodulator: return logger.debug("starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate()) - secondary_command_fft = self.secondary_chain("fft") secondary_command_demod = self.secondary_chain(self.secondary_demodulator) - self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft) + self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod) - secondary_command_fft = secondary_command_fft.format( - input_pipe=self.iqtee_pipe, - secondary_fft_input_size=self.secondary_fft_size, - secondary_fft_size=self.secondary_fft_size, - secondary_fft_block_size=self.secondary_fft_block_size(), - ) secondary_command_demod = secondary_command_demod.format( input_pipe=self.iqtee2_pipe, secondary_shift_pipe=self.secondary_shift_pipe, @@ -267,24 +277,34 @@ def start_secondary_demodulator(self): last_decimation=self.last_decimation, ) - logger.debug("secondary command (fft) = %s", secondary_command_fft) logger.debug("secondary command (demod) = %s", secondary_command_demod) my_env = os.environ.copy() # if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"] = "1" - self.secondary_process_fft = subprocess.Popen( - secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env - ) + if self.output.supports_type('secondary_fft'): + secondary_command_fft = self.secondary_chain("fft") + secondary_command_fft = secondary_command_fft.format( + input_pipe=self.iqtee_pipe, + secondary_fft_input_size=self.secondary_fft_size, + secondary_fft_size=self.secondary_fft_size, + secondary_fft_block_size=self.secondary_fft_block_size(), + ) + logger.debug("secondary command (fft) = %s", secondary_command_fft) + + self.secondary_process_fft = subprocess.Popen( + secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env + ) + self.output.send_output( + "secondary_fft", + partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())), + ) + self.secondary_process_demod = subprocess.Popen( secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env ) self.secondary_processes_running = True - self.output.add_output( - "secondary_fft", - partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())), - ) if self.isWsjtMode(): smd = self.get_secondary_demodulator() if smd == "ft8": @@ -298,9 +318,9 @@ def start_secondary_demodulator(self): elif smd == "ft4": chopper = Ft4Chopper(self.secondary_process_demod.stdout) chopper.start() - self.output.add_output("wsjt_demod", chopper.read) + self.output.send_output("wsjt_demod", chopper.read) else: - self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) + self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) # open control pipes for csdr and send initialization data if self.secondary_shift_pipe != None: # TODO digimodes @@ -551,7 +571,9 @@ def start(self): my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1" if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"] = "1" - self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) + + out = subprocess.PIPE if self.output.supports_type('audio') else subprocess.DEVNULL + self.process = subprocess.Popen(command, stdout=out, shell=True, preexec_fn=os.setpgrp, env=my_env) def watch_thread(): rc = self.process.wait() @@ -562,10 +584,11 @@ def watch_thread(): threading.Thread(target=watch_thread).start() - self.output.add_output( - "audio", - partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256), - ) + if self.output.supports_type('audio'): + self.output.send_output( + "audio", + partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256), + ) # open control pipes for csdr if self.bpf_pipe: @@ -595,7 +618,7 @@ def read_smeter(): else: return float(raw.rstrip("\n")) - self.output.add_output("smeter", read_smeter) + self.output.send_output("smeter", read_smeter) if self.meta_pipe != None: # TODO make digiham output unicode and then change this here self.meta_pipe_file = open(self.meta_pipe, "r", encoding="cp437") @@ -607,7 +630,7 @@ def read_meta(): else: return raw.rstrip("\n") - self.output.add_output("meta", read_meta) + self.output.send_output("meta", read_meta) if self.dmr_control_pipe: self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w") diff --git a/owrx/service.py b/owrx/service.py index d04ee0b61..f90b2bb18 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -14,17 +14,15 @@ class ServiceOutput(output): def __init__(self, frequency): self.frequency = frequency - def add_output(self, t, read_fn): - if t == "wsjt_demod": - parser = WsjtParser(WsjtHandler()) - parser.setDialFrequency(self.frequency) - target = self.pump(read_fn, parser.parse) - else: - # dump everything else - # TODO rewrite the output mechanism in a way that avoids producing unnecessary data - target = self.pump(read_fn, lambda x: None) + def receive_output(self, t, read_fn): + parser = WsjtParser(WsjtHandler()) + parser.setDialFrequency(self.frequency) + target = self.pump(read_fn, parser.parse) threading.Thread(target=target).start() + def supports_type(self, t): + return t == 'wsjt_demod' + class ServiceHandler(object): def __init__(self, source): diff --git a/owrx/source.py b/owrx/source.py index 3191cc2e8..ca8227012 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -376,7 +376,7 @@ def start(self): if self.sdrSource.isAvailable(): self.dsp.start() - def add_output(self, type, read_fn): + def receive_output(self, type, read_fn): if type != "audio": logger.error("unsupported output type received by FFT: %s", type) return @@ -503,7 +503,7 @@ def start(self): if self.sdrSource.isAvailable(): self.dsp.start() - def add_output(self, t, read_fn): + def receive_output(self, t, read_fn): logger.debug("adding new output of type %s", t) writers = { "audio": self.handler.write_dsp_data, From 8214fdb24dff843c2937678f2a1cc357eaa44125 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 4 Aug 2019 15:17:03 +0200 Subject: [PATCH 0293/2616] looks configurable to me, at least for now --- owrx/service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/owrx/service.py b/owrx/service.py index f90b2bb18..fe6a4eb2c 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -40,7 +40,6 @@ def onSdrUnavailable(self): self.stopServices() def isSupported(self, mode): - # TODO make configurable return mode in PropertyManager.getSharedInstance()["services_decoders"] def stopServices(self): From 766300bdfffa0d473222bd3335731e544ab6f470 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 4 Aug 2019 17:31:50 +0200 Subject: [PATCH 0294/2616] use latest improvementes for fft, too --- owrx/source.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/owrx/source.py b/owrx/source.py index ca8227012..488ff47c1 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -376,25 +376,15 @@ def start(self): if self.sdrSource.isAvailable(): self.dsp.start() - def receive_output(self, type, read_fn): - if type != "audio": - logger.error("unsupported output type received by FFT: %s", type) - return + def supports_type(self, t): + return t == 'audio' + def receive_output(self, type, read_fn): if self.props["csdr_dynamic_bufsize"]: read_fn(8) # dummy read to skip bufsize & preamble logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1") - def pipe(): - run = True - while run: - data = read_fn() - if len(data) == 0: - run = False - else: - self.sdrSource.writeSpectrumData(data) - - threading.Thread(target=pipe).start() + threading.Thread(target=self.pump(read_fn, self.sdrSource.writeSpectrumData)).start() def stop(self): self.dsp.stop() From 92321a3b4e537593ac62509ae68385e5156890d3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 4 Aug 2019 18:36:03 +0200 Subject: [PATCH 0295/2616] simple metrics api to interface with collectd and grafana --- owrx/controllers.py | 7 +++++++ owrx/http.py | 2 ++ owrx/metrics.py | 32 ++++++++++++++++++++++++++++++++ owrx/wsjt.py | 3 +++ 4 files changed, 44 insertions(+) create mode 100644 owrx/metrics.py diff --git a/owrx/controllers.py b/owrx/controllers.py index c6c0da55d..f7ce7e0df 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -9,6 +9,7 @@ from owrx.connection import WebSocketMessageHandler from owrx.version import openwebrx_version from owrx.feature import FeatureDetector +from owrx.metrics import Metrics import logging @@ -124,6 +125,12 @@ def handle_request(self): self.send_response(data, content_type="application/json") +class MetricsController(Controller): + def handle_request(self): + data = json.dumps(Metrics.getSharedInstance().getMetrics()) + self.send_response(data, content_type="application/json") + + class WebSocketController(Controller): def handle_request(self): conn = WebSocketConnection(self.handler, WebSocketMessageHandler()) diff --git a/owrx/http.py b/owrx/http.py index 99c100344..ce96acc16 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -6,6 +6,7 @@ MapController, FeatureController, ApiController, + MetricsController, ) from http.server import BaseHTTPRequestHandler import re @@ -43,6 +44,7 @@ class Router(object): {"route": "/map", "controller": MapController}, {"route": "/features", "controller": FeatureController}, {"route": "/api/features", "controller": ApiController}, + {"route": "/metrics", "controller": MetricsController}, ] def find_controller(self, path): diff --git a/owrx/metrics.py b/owrx/metrics.py new file mode 100644 index 000000000..a8923d0de --- /dev/null +++ b/owrx/metrics.py @@ -0,0 +1,32 @@ +class Metrics(object): + sharedInstance = None + + @staticmethod + def getSharedInstance(): + if Metrics.sharedInstance is None: + Metrics.sharedInstance = Metrics() + return Metrics.sharedInstance + + def __init__(self): + self.metrics = {} + + def pushDecodes(self, band, mode, count = 1): + if band is None: + band = 'unknown' + else: + band = band.getName() + + if mode is None: + mode = 'unknown' + + if not band in self.metrics: + self.metrics[band] = {} + if not mode in self.metrics[band]: + self.metrics[band][mode] = { + "count": 0 + } + + self.metrics[band][mode]["count"] += count + + def getMetrics(self): + return self.metrics diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 2c26b2fae..082e59fbc 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -10,6 +10,7 @@ import re from owrx.config import PropertyManager from owrx.bands import Bandplan +from owrx.metrics import Metrics import logging @@ -228,6 +229,7 @@ def parse_from_jt9(self, msg): mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" wsjt_msg = msg[17:53].strip() self.parseLocator(wsjt_msg, mode) + Metrics.getSharedInstance().pushDecodes(self.band, mode) return { "timestamp": timestamp, "db": float(msg[0:3]), @@ -253,6 +255,7 @@ def parse_from_wsprd(self, msg): # '0052 -29 2.6 0.001486 0 G02CWT IO92 23' wsjt_msg = msg[29:].strip() self.parseWsprMessage(wsjt_msg) + Metrics.getSharedInstance().pushDecodes(self.band, 'WSPR') return { "timestamp": self.parse_timestamp(msg[0:4], "%H%M"), "db": float(msg[5:8]), From d467d79bdf5a175cccee5dc621dd6127a823425b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 11:37:45 +0200 Subject: [PATCH 0296/2616] code format with black --- csdr.py | 21 +++++++++++---------- owrx/metrics.py | 10 ++++------ owrx/service.py | 9 +++++++-- owrx/source.py | 2 +- owrx/wsjt.py | 2 +- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/csdr.py b/csdr.py index 08a0c4538..832d06925 100644 --- a/csdr.py +++ b/csdr.py @@ -36,7 +36,7 @@ class output(object): def send_output(self, t, read_fn): if not self.supports_type(t): # TODO rewrite the output mechanism in a way that avoids producing unnecessary data - logger.warning('dumping output of type %s since it is not supported.', t) + logger.warning("dumping output of type %s since it is not supported.", t) threading.Thread(target=self.pump(read_fn, lambda x: None)).start() return self.receive_output(t, read_fn) @@ -131,18 +131,17 @@ def chain(self, which): "csdr shift_addition_cc --fifo {shift_pipe}", "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING", "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING", - ] - if self.output.supports_type('smeter'): + if self.output.supports_type("smeter"): chain += [ - "csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}", + "csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}" ] if self.secondary_demodulator: - if self.output.supports_type('secondary_fft'): + if self.output.supports_type("secondary_fft"): chain += ["csdr tee {iqtee_pipe}"] chain += ["csdr tee {iqtee2_pipe}"] # early exit if we don't want audio - if not self.output.supports_type('audio'): + if not self.output.supports_type("audio"): return chain # safe some cpu cycles... no need to decimate if decimation factor is 1 last_decimation_block = ( @@ -282,7 +281,7 @@ def start_secondary_demodulator(self): # if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"] = "1" - if self.output.supports_type('secondary_fft'): + if self.output.supports_type("secondary_fft"): secondary_command_fft = self.secondary_chain("fft") secondary_command_fft = secondary_command_fft.format( input_pipe=self.iqtee_pipe, @@ -572,7 +571,7 @@ def start(self): if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"] = "1" - out = subprocess.PIPE if self.output.supports_type('audio') else subprocess.DEVNULL + out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL self.process = subprocess.Popen(command, stdout=out, shell=True, preexec_fn=os.setpgrp, env=my_env) def watch_thread(): @@ -584,10 +583,12 @@ def watch_thread(): threading.Thread(target=watch_thread).start() - if self.output.supports_type('audio'): + if self.output.supports_type("audio"): self.output.send_output( "audio", - partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256), + partial( + self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256 + ), ) # open control pipes for csdr diff --git a/owrx/metrics.py b/owrx/metrics.py index a8923d0de..11f503f92 100644 --- a/owrx/metrics.py +++ b/owrx/metrics.py @@ -10,21 +10,19 @@ def getSharedInstance(): def __init__(self): self.metrics = {} - def pushDecodes(self, band, mode, count = 1): + def pushDecodes(self, band, mode, count=1): if band is None: - band = 'unknown' + band = "unknown" else: band = band.getName() if mode is None: - mode = 'unknown' + mode = "unknown" if not band in self.metrics: self.metrics[band] = {} if not mode in self.metrics[band]: - self.metrics[band][mode] = { - "count": 0 - } + self.metrics[band][mode] = {"count": 0} self.metrics[band][mode]["count"] += count diff --git a/owrx/service.py b/owrx/service.py index fe6a4eb2c..b64e504bc 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -21,7 +21,7 @@ def receive_output(self, t, read_fn): threading.Thread(target=target).start() def supports_type(self, t): - return t == 'wsjt_demod' + return t == "wsjt_demod" class ServiceHandler(object): @@ -69,7 +69,11 @@ def updateServices(self): cf = self.source.getProps()["center_freq"] srh = self.source.getProps()["samp_rate"] / 2 frequency_range = (cf - srh, cf + srh) - self.services = [self.setupService(dial["mode"], dial["frequency"]) for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range) if self.isSupported(dial["mode"])] + self.services = [ + self.setupService(dial["mode"], dial["frequency"]) + for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range) + if self.isSupported(dial["mode"]) + ] def setupService(self, mode, frequency): logger.debug("setting up service {0} on frequency {1}".format(mode, frequency)) @@ -92,6 +96,7 @@ def write_wsjt_message(self, msg): class ServiceManager(object): sharedInstance = None + @staticmethod def getSharedInstance(): if ServiceManager.sharedInstance is None: diff --git a/owrx/source.py b/owrx/source.py index 488ff47c1..8f5dc6534 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -377,7 +377,7 @@ def start(self): self.dsp.start() def supports_type(self, t): - return t == 'audio' + return t == "audio" def receive_output(self, type, read_fn): if self.props["csdr_dynamic_bufsize"]: diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 082e59fbc..e18257e8e 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -255,7 +255,7 @@ def parse_from_wsprd(self, msg): # '0052 -29 2.6 0.001486 0 G02CWT IO92 23' wsjt_msg = msg[29:].strip() self.parseWsprMessage(wsjt_msg) - Metrics.getSharedInstance().pushDecodes(self.band, 'WSPR') + Metrics.getSharedInstance().pushDecodes(self.band, "WSPR") return { "timestamp": self.parse_timestamp(msg[0:4], "%H%M"), "db": float(msg[5:8]), From b0056a4677f9438a557c06baa1e4864739ce319b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 11:39:35 +0200 Subject: [PATCH 0297/2616] disable services by default --- config_webrx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_webrx.py b/config_webrx.py index 044dc851b..f75d3e3b1 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -252,5 +252,5 @@ temporary_directory = "/tmp" -services_enabled = True +services_enabled = False services_decoders = ["ft8", "ft4", "wspr"] From ef90e3e0489ed9662aa47e59ce5c97ca9b07201d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 13:05:36 +0200 Subject: [PATCH 0298/2616] disable colors --- csdr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csdr.py b/csdr.py index 6b80da5ed..ee007f251 100644 --- a/csdr.py +++ b/csdr.py @@ -184,7 +184,7 @@ def chain(self, which): chain += last_decimation_block chain += [ "csdr convert_f_s16", - "direwolf -r {audio_rate} - 1>&2" + "direwolf -r {audio_rate} -t 0 - 1>&2" ] elif which == "am": chain += ["csdr amdemod_cf", "csdr fastdcblock_ff"] From 2fed83659f5a702cb1a9816cc944bb63e02a3d2c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 13:09:34 +0200 Subject: [PATCH 0299/2616] these should not be in here --- htdocs/libmbe.js | 2322 -------------------------------------------- htdocs/libmbe.wasm | Bin 31678 -> 0 bytes 2 files changed, 2322 deletions(-) delete mode 100644 htdocs/libmbe.js delete mode 100644 htdocs/libmbe.wasm diff --git a/htdocs/libmbe.js b/htdocs/libmbe.js deleted file mode 100644 index fb29253a0..000000000 --- a/htdocs/libmbe.js +++ /dev/null @@ -1,2322 +0,0 @@ -// Copyright 2010 The Emscripten Authors. All rights reserved. -// Emscripten is available under two separate licenses, the MIT license and the -// University of Illinois/NCSA Open Source License. Both these licenses can be -// found in the LICENSE file. - -// The Module object: Our interface to the outside world. We import -// and export values on it. There are various ways Module can be used: -// 1. Not defined. We create it here -// 2. A function parameter, function(Module) { ..generated code.. } -// 3. pre-run appended it, var Module = {}; ..generated code.. -// 4. External script tag defines var Module. -// We need to check if Module already exists (e.g. case 3 above). -// Substitution will be replaced with actual code on later stage of the build, -// this way Closure Compiler will not mangle it (e.g. case 4. above). -// Note that if you want to run closure, and also to use Module -// after the generated code, you will need to define var Module = {}; -// before the code. Then that object will be used in the code, and you -// can continue to use Module afterwards as well. -var Module = typeof Module !== 'undefined' ? Module : {}; - -// --pre-jses are emitted after the Module integration code, so that they can -// refer to Module (if they choose; they can also define Module) -// {{PRE_JSES}} - -// Sometimes an existing Module object exists with properties -// meant to overwrite the default module functionality. Here -// we collect those properties and reapply _after_ we configure -// the current environment's defaults to avoid having to be so -// defensive during initialization. -var moduleOverrides = {}; -var key; -for (key in Module) { - if (Module.hasOwnProperty(key)) { - moduleOverrides[key] = Module[key]; - } -} - -Module['arguments'] = []; -Module['thisProgram'] = './this.program'; -Module['quit'] = function(status, toThrow) { - throw toThrow; -}; -Module['preRun'] = []; -Module['postRun'] = []; - -// Determine the runtime environment we are in. You can customize this by -// setting the ENVIRONMENT setting at compile time (see settings.js). - -var ENVIRONMENT_IS_WEB = false; -var ENVIRONMENT_IS_WORKER = false; -var ENVIRONMENT_IS_NODE = false; -var ENVIRONMENT_IS_SHELL = false; -ENVIRONMENT_IS_WEB = typeof window === 'object'; -ENVIRONMENT_IS_WORKER = typeof importScripts === 'function'; -ENVIRONMENT_IS_NODE = typeof process === 'object' && typeof require === 'function' && !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_WORKER; -ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; - -if (Module['ENVIRONMENT']) { - throw new Error('Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -s ENVIRONMENT=web or -s ENVIRONMENT=node)'); -} - - -// Three configurations we can be running in: -// 1) We could be the application main() thread running in the main JS UI thread. (ENVIRONMENT_IS_WORKER == false and ENVIRONMENT_IS_PTHREAD == false) -// 2) We could be the application main() thread proxied to worker. (with Emscripten -s PROXY_TO_WORKER=1) (ENVIRONMENT_IS_WORKER == true, ENVIRONMENT_IS_PTHREAD == false) -// 3) We could be an application pthread running in a worker. (ENVIRONMENT_IS_WORKER == true and ENVIRONMENT_IS_PTHREAD == true) - - - - -// `/` should be present at the end if `scriptDirectory` is not empty -var scriptDirectory = ''; -function locateFile(path) { - if (Module['locateFile']) { - return Module['locateFile'](path, scriptDirectory); - } else { - return scriptDirectory + path; - } -} - -if (ENVIRONMENT_IS_NODE) { - scriptDirectory = __dirname + '/'; - - // Expose functionality in the same simple way that the shells work - // Note that we pollute the global namespace here, otherwise we break in node - var nodeFS; - var nodePath; - - Module['read'] = function shell_read(filename, binary) { - var ret; - if (!nodeFS) nodeFS = require('fs'); - if (!nodePath) nodePath = require('path'); - filename = nodePath['normalize'](filename); - ret = nodeFS['readFileSync'](filename); - return binary ? ret : ret.toString(); - }; - - Module['readBinary'] = function readBinary(filename) { - var ret = Module['read'](filename, true); - if (!ret.buffer) { - ret = new Uint8Array(ret); - } - assert(ret.buffer); - return ret; - }; - - if (process['argv'].length > 1) { - Module['thisProgram'] = process['argv'][1].replace(/\\/g, '/'); - } - - Module['arguments'] = process['argv'].slice(2); - - if (typeof module !== 'undefined') { - module['exports'] = Module; - } - - process['on']('uncaughtException', function(ex) { - // suppress ExitStatus exceptions from showing an error - if (!(ex instanceof ExitStatus)) { - throw ex; - } - }); - // Currently node will swallow unhandled rejections, but this behavior is - // deprecated, and in the future it will exit with error status. - process['on']('unhandledRejection', abort); - - Module['quit'] = function(status) { - process['exit'](status); - }; - - Module['inspect'] = function () { return '[Emscripten Module object]'; }; -} else -if (ENVIRONMENT_IS_SHELL) { - - - if (typeof read != 'undefined') { - Module['read'] = function shell_read(f) { - return read(f); - }; - } - - Module['readBinary'] = function readBinary(f) { - var data; - if (typeof readbuffer === 'function') { - return new Uint8Array(readbuffer(f)); - } - data = read(f, 'binary'); - assert(typeof data === 'object'); - return data; - }; - - if (typeof scriptArgs != 'undefined') { - Module['arguments'] = scriptArgs; - } else if (typeof arguments != 'undefined') { - Module['arguments'] = arguments; - } - - if (typeof quit === 'function') { - Module['quit'] = function(status) { - quit(status); - } - } -} else -if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { - if (ENVIRONMENT_IS_WORKER) { // Check worker, not web, since window could be polyfilled - scriptDirectory = self.location.href; - } else if (document.currentScript) { // web - scriptDirectory = document.currentScript.src; - } - // blob urls look like blob:http://site.com/etc/etc and we cannot infer anything from them. - // otherwise, slice off the final part of the url to find the script directory. - // if scriptDirectory does not contain a slash, lastIndexOf will return -1, - // and scriptDirectory will correctly be replaced with an empty string. - if (scriptDirectory.indexOf('blob:') !== 0) { - scriptDirectory = scriptDirectory.substr(0, scriptDirectory.lastIndexOf('/')+1); - } else { - scriptDirectory = ''; - } - - - Module['read'] = function shell_read(url) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - xhr.send(null); - return xhr.responseText; - }; - - if (ENVIRONMENT_IS_WORKER) { - Module['readBinary'] = function readBinary(url) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - xhr.responseType = 'arraybuffer'; - xhr.send(null); - return new Uint8Array(xhr.response); - }; - } - - Module['readAsync'] = function readAsync(url, onload, onerror) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.responseType = 'arraybuffer'; - xhr.onload = function xhr_onload() { - if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0 - onload(xhr.response); - return; - } - onerror(); - }; - xhr.onerror = onerror; - xhr.send(null); - }; - - Module['setWindowTitle'] = function(title) { document.title = title }; -} else -{ - throw new Error('environment detection error'); -} - -// Set up the out() and err() hooks, which are how we can print to stdout or -// stderr, respectively. -// If the user provided Module.print or printErr, use that. Otherwise, -// console.log is checked first, as 'print' on the web will open a print dialogue -// printErr is preferable to console.warn (works better in shells) -// bind(console) is necessary to fix IE/Edge closed dev tools panel behavior. -var out = Module['print'] || (typeof console !== 'undefined' ? console.log.bind(console) : (typeof print !== 'undefined' ? print : null)); -var err = Module['printErr'] || (typeof printErr !== 'undefined' ? printErr : ((typeof console !== 'undefined' && console.warn.bind(console)) || out)); - -// Merge back in the overrides -for (key in moduleOverrides) { - if (moduleOverrides.hasOwnProperty(key)) { - Module[key] = moduleOverrides[key]; - } -} -// Free the object hierarchy contained in the overrides, this lets the GC -// reclaim data used e.g. in memoryInitializerRequest, which is a large typed array. -moduleOverrides = undefined; - -// perform assertions in shell.js after we set up out() and err(), as otherwise if an assertion fails it cannot print the message -assert(typeof Module['memoryInitializerPrefixURL'] === 'undefined', 'Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['pthreadMainPrefixURL'] === 'undefined', 'Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['cdInitializerPrefixURL'] === 'undefined', 'Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['filePackagePrefixURL'] === 'undefined', 'Module.filePackagePrefixURL option was removed, use Module.locateFile instead'); - - - -// Copyright 2017 The Emscripten Authors. All rights reserved. -// Emscripten is available under two separate licenses, the MIT license and the -// University of Illinois/NCSA Open Source License. Both these licenses can be -// found in the LICENSE file. - -// {{PREAMBLE_ADDITIONS}} - -var STACK_ALIGN = 16; - -// stack management, and other functionality that is provided by the compiled code, -// should not be used before it is ready -stackSave = stackRestore = stackAlloc = function() { - abort('cannot use the stack before compiled code is ready to run, and has provided stack access'); -}; - -function staticAlloc(size) { - abort('staticAlloc is no longer available at runtime; instead, perform static allocations at compile time (using makeStaticAlloc)'); -} - -function dynamicAlloc(size) { - assert(DYNAMICTOP_PTR); - var ret = HEAP32[DYNAMICTOP_PTR>>2]; - var end = (ret + size + 15) & -16; - if (end <= _emscripten_get_heap_size()) { - HEAP32[DYNAMICTOP_PTR>>2] = end; - } else { - return 0; - } - return ret; -} - -function alignMemory(size, factor) { - if (!factor) factor = STACK_ALIGN; // stack alignment (16-byte) by default - return Math.ceil(size / factor) * factor; -} - -function getNativeTypeSize(type) { - switch (type) { - case 'i1': case 'i8': return 1; - case 'i16': return 2; - case 'i32': return 4; - case 'i64': return 8; - case 'float': return 4; - case 'double': return 8; - default: { - if (type[type.length-1] === '*') { - return 4; // A pointer - } else if (type[0] === 'i') { - var bits = parseInt(type.substr(1)); - assert(bits % 8 === 0, 'getNativeTypeSize invalid bits ' + bits + ', type ' + type); - return bits / 8; - } else { - return 0; - } - } - } -} - -function warnOnce(text) { - if (!warnOnce.shown) warnOnce.shown = {}; - if (!warnOnce.shown[text]) { - warnOnce.shown[text] = 1; - err(text); - } -} - -var asm2wasmImports = { // special asm2wasm imports - "f64-rem": function(x, y) { - return x % y; - }, - "debugger": function() { - debugger; - } -}; - - - -var jsCallStartIndex = 1; -var functionPointers = new Array(0); - -// Wraps a JS function as a wasm function with a given signature. -// In the future, we may get a WebAssembly.Function constructor. Until then, -// we create a wasm module that takes the JS function as an import with a given -// signature, and re-exports that as a wasm function. -function convertJsFunctionToWasm(func, sig) { - // The module is static, with the exception of the type section, which is - // generated based on the signature passed in. - var typeSection = [ - 0x01, // id: section, - 0x00, // length: 0 (placeholder) - 0x01, // count: 1 - 0x60, // form: func - ]; - var sigRet = sig.slice(0, 1); - var sigParam = sig.slice(1); - var typeCodes = { - 'i': 0x7f, // i32 - 'j': 0x7e, // i64 - 'f': 0x7d, // f32 - 'd': 0x7c, // f64 - }; - - // Parameters, length + signatures - typeSection.push(sigParam.length); - for (var i = 0; i < sigParam.length; ++i) { - typeSection.push(typeCodes[sigParam[i]]); - } - - // Return values, length + signatures - // With no multi-return in MVP, either 0 (void) or 1 (anything else) - if (sigRet == 'v') { - typeSection.push(0x00); - } else { - typeSection = typeSection.concat([0x01, typeCodes[sigRet]]); - } - - // Write the overall length of the type section back into the section header - // (excepting the 2 bytes for the section id and length) - typeSection[1] = typeSection.length - 2; - - // Rest of the module is static - var bytes = new Uint8Array([ - 0x00, 0x61, 0x73, 0x6d, // magic ("\0asm") - 0x01, 0x00, 0x00, 0x00, // version: 1 - ].concat(typeSection, [ - 0x02, 0x07, // import section - // (import "e" "f" (func 0 (type 0))) - 0x01, 0x01, 0x65, 0x01, 0x66, 0x00, 0x00, - 0x07, 0x05, // export section - // (export "f" (func 0 (type 0))) - 0x01, 0x01, 0x66, 0x00, 0x00, - ])); - - // We can compile this wasm module synchronously because it is very small. - // This accepts an import (at "e.f"), that it reroutes to an export (at "f") - var module = new WebAssembly.Module(bytes); - var instance = new WebAssembly.Instance(module, { - e: { - f: func - } - }); - var wrappedFunc = instance.exports.f; - return wrappedFunc; -} - -// Add a wasm function to the table. -function addFunctionWasm(func, sig) { - var table = wasmTable; - var ret = table.length; - - // Grow the table - try { - table.grow(1); - } catch (err) { - if (!err instanceof RangeError) { - throw err; - } - throw 'Unable to grow wasm table. Use a higher value for RESERVED_FUNCTION_POINTERS or set ALLOW_TABLE_GROWTH.'; - } - - // Insert new element - try { - // Attempting to call this with JS function will cause of table.set() to fail - table.set(ret, func); - } catch (err) { - if (!err instanceof TypeError) { - throw err; - } - assert(typeof sig !== 'undefined', 'Missing signature argument to addFunction'); - var wrapped = convertJsFunctionToWasm(func, sig); - table.set(ret, wrapped); - } - - return ret; -} - -function removeFunctionWasm(index) { - // TODO(sbc): Look into implementing this to allow re-using of table slots -} - -// 'sig' parameter is required for the llvm backend but only when func is not -// already a WebAssembly function. -function addFunction(func, sig) { - - - var base = 0; - for (var i = base; i < base + 0; i++) { - if (!functionPointers[i]) { - functionPointers[i] = func; - return jsCallStartIndex + i; - } - } - throw 'Finished up all reserved function pointers. Use a higher value for RESERVED_FUNCTION_POINTERS.'; - -} - -function removeFunction(index) { - - functionPointers[index-jsCallStartIndex] = null; -} - -var funcWrappers = {}; - -function getFuncWrapper(func, sig) { - if (!func) return; // on null pointer, return undefined - assert(sig); - if (!funcWrappers[sig]) { - funcWrappers[sig] = {}; - } - var sigCache = funcWrappers[sig]; - if (!sigCache[func]) { - // optimize away arguments usage in common cases - if (sig.length === 1) { - sigCache[func] = function dynCall_wrapper() { - return dynCall(sig, func); - }; - } else if (sig.length === 2) { - sigCache[func] = function dynCall_wrapper(arg) { - return dynCall(sig, func, [arg]); - }; - } else { - // general case - sigCache[func] = function dynCall_wrapper() { - return dynCall(sig, func, Array.prototype.slice.call(arguments)); - }; - } - } - return sigCache[func]; -} - - -function makeBigInt(low, high, unsigned) { - return unsigned ? ((+((low>>>0)))+((+((high>>>0)))*4294967296.0)) : ((+((low>>>0)))+((+((high|0)))*4294967296.0)); -} - -function dynCall(sig, ptr, args) { - if (args && args.length) { - assert(args.length == sig.length-1); - assert(('dynCall_' + sig) in Module, 'bad function pointer type - no table for sig \'' + sig + '\''); - return Module['dynCall_' + sig].apply(null, [ptr].concat(args)); - } else { - assert(sig.length == 1); - assert(('dynCall_' + sig) in Module, 'bad function pointer type - no table for sig \'' + sig + '\''); - return Module['dynCall_' + sig].call(null, ptr); - } -} - -var tempRet0 = 0; - -var setTempRet0 = function(value) { - tempRet0 = value; -} - -var getTempRet0 = function() { - return tempRet0; -} - -function getCompilerSetting(name) { - throw 'You must build with -s RETAIN_COMPILER_SETTINGS=1 for getCompilerSetting or emscripten_get_compiler_setting to work'; -} - -var Runtime = { - // helpful errors - getTempRet0: function() { abort('getTempRet0() is now a top-level function, after removing the Runtime object. Remove "Runtime."') }, - staticAlloc: function() { abort('staticAlloc() is now a top-level function, after removing the Runtime object. Remove "Runtime."') }, - stackAlloc: function() { abort('stackAlloc() is now a top-level function, after removing the Runtime object. Remove "Runtime."') }, -}; - -// The address globals begin at. Very low in memory, for code size and optimization opportunities. -// Above 0 is static memory, starting with globals. -// Then the stack. -// Then 'dynamic' memory for sbrk. -var GLOBAL_BASE = 1024; - - - - -// === Preamble library stuff === - -// Documentation for the public APIs defined in this file must be updated in: -// site/source/docs/api_reference/preamble.js.rst -// A prebuilt local version of the documentation is available at: -// site/build/text/docs/api_reference/preamble.js.txt -// You can also build docs locally as HTML or other formats in site/ -// An online HTML version (which may be of a different version of Emscripten) -// is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html - - -if (typeof WebAssembly !== 'object') { - abort('No WebAssembly support found. Build with -s WASM=0 to target JavaScript instead.'); -} - - -/** @type {function(number, string, boolean=)} */ -function getValue(ptr, type, noSafe) { - type = type || 'i8'; - if (type.charAt(type.length-1) === '*') type = 'i32'; // pointers are 32-bit - switch(type) { - case 'i1': return HEAP8[((ptr)>>0)]; - case 'i8': return HEAP8[((ptr)>>0)]; - case 'i16': return HEAP16[((ptr)>>1)]; - case 'i32': return HEAP32[((ptr)>>2)]; - case 'i64': return HEAP32[((ptr)>>2)]; - case 'float': return HEAPF32[((ptr)>>2)]; - case 'double': return HEAPF64[((ptr)>>3)]; - default: abort('invalid type for getValue: ' + type); - } - return null; -} - - - - -// Wasm globals - -var wasmMemory; - -// Potentially used for direct table calls. -var wasmTable; - - -//======================================== -// Runtime essentials -//======================================== - -// whether we are quitting the application. no code should run after this. -// set in exit() and abort() -var ABORT = false; - -// set by exit() and abort(). Passed to 'onExit' handler. -// NOTE: This is also used as the process return code code in shell environments -// but only when noExitRuntime is false. -var EXITSTATUS = 0; - -/** @type {function(*, string=)} */ -function assert(condition, text) { - if (!condition) { - abort('Assertion failed: ' + text); - } -} - -// Returns the C function with a specified identifier (for C++, you need to do manual name mangling) -function getCFunc(ident) { - var func = Module['_' + ident]; // closure exported function - assert(func, 'Cannot call unknown function ' + ident + ', make sure it is exported'); - return func; -} - -// C calling interface. -function ccall(ident, returnType, argTypes, args, opts) { - // For fast lookup of conversion functions - var toC = { - 'string': function(str) { - var ret = 0; - if (str !== null && str !== undefined && str !== 0) { // null string - // at most 4 bytes per UTF-8 code point, +1 for the trailing '\0' - var len = (str.length << 2) + 1; - ret = stackAlloc(len); - stringToUTF8(str, ret, len); - } - return ret; - }, - 'array': function(arr) { - var ret = stackAlloc(arr.length); - writeArrayToMemory(arr, ret); - return ret; - } - }; - - function convertReturnValue(ret) { - if (returnType === 'string') return UTF8ToString(ret); - if (returnType === 'boolean') return Boolean(ret); - return ret; - } - - var func = getCFunc(ident); - var cArgs = []; - var stack = 0; - assert(returnType !== 'array', 'Return type should not be "array".'); - if (args) { - for (var i = 0; i < args.length; i++) { - var converter = toC[argTypes[i]]; - if (converter) { - if (stack === 0) stack = stackSave(); - cArgs[i] = converter(args[i]); - } else { - cArgs[i] = args[i]; - } - } - } - var ret = func.apply(null, cArgs); - ret = convertReturnValue(ret); - if (stack !== 0) stackRestore(stack); - return ret; -} - -function cwrap(ident, returnType, argTypes, opts) { - return function() { - return ccall(ident, returnType, argTypes, arguments, opts); - } -} - -/** @type {function(number, number, string, boolean=)} */ -function setValue(ptr, value, type, noSafe) { - type = type || 'i8'; - if (type.charAt(type.length-1) === '*') type = 'i32'; // pointers are 32-bit - switch(type) { - case 'i1': HEAP8[((ptr)>>0)]=value; break; - case 'i8': HEAP8[((ptr)>>0)]=value; break; - case 'i16': HEAP16[((ptr)>>1)]=value; break; - case 'i32': HEAP32[((ptr)>>2)]=value; break; - case 'i64': (tempI64 = [value>>>0,(tempDouble=value,(+(Math_abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? ((Math_min((+(Math_floor((tempDouble)/4294967296.0))), 4294967295.0))|0)>>>0 : (~~((+(Math_ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)],HEAP32[((ptr)>>2)]=tempI64[0],HEAP32[(((ptr)+(4))>>2)]=tempI64[1]); break; - case 'float': HEAPF32[((ptr)>>2)]=value; break; - case 'double': HEAPF64[((ptr)>>3)]=value; break; - default: abort('invalid type for setValue: ' + type); - } -} - -var ALLOC_NORMAL = 0; // Tries to use _malloc() -var ALLOC_STACK = 1; // Lives for the duration of the current function call -var ALLOC_DYNAMIC = 2; // Cannot be freed except through sbrk -var ALLOC_NONE = 3; // Do not allocate - -// allocate(): This is for internal use. You can use it yourself as well, but the interface -// is a little tricky (see docs right below). The reason is that it is optimized -// for multiple syntaxes to save space in generated code. So you should -// normally not use allocate(), and instead allocate memory using _malloc(), -// initialize it with setValue(), and so forth. -// @slab: An array of data, or a number. If a number, then the size of the block to allocate, -// in *bytes* (note that this is sometimes confusing: the next parameter does not -// affect this!) -// @types: Either an array of types, one for each byte (or 0 if no type at that position), -// or a single type which is used for the entire block. This only matters if there -// is initial data - if @slab is a number, then this does not matter at all and is -// ignored. -// @allocator: How to allocate memory, see ALLOC_* -/** @type {function((TypedArray|Array|number), string, number, number=)} */ -function allocate(slab, types, allocator, ptr) { - var zeroinit, size; - if (typeof slab === 'number') { - zeroinit = true; - size = slab; - } else { - zeroinit = false; - size = slab.length; - } - - var singleType = typeof types === 'string' ? types : null; - - var ret; - if (allocator == ALLOC_NONE) { - ret = ptr; - } else { - ret = [_malloc, - stackAlloc, - dynamicAlloc][allocator](Math.max(size, singleType ? 1 : types.length)); - } - - if (zeroinit) { - var stop; - ptr = ret; - assert((ret & 3) == 0); - stop = ret + (size & ~3); - for (; ptr < stop; ptr += 4) { - HEAP32[((ptr)>>2)]=0; - } - stop = ret + size; - while (ptr < stop) { - HEAP8[((ptr++)>>0)]=0; - } - return ret; - } - - if (singleType === 'i8') { - if (slab.subarray || slab.slice) { - HEAPU8.set(/** @type {!Uint8Array} */ (slab), ret); - } else { - HEAPU8.set(new Uint8Array(slab), ret); - } - return ret; - } - - var i = 0, type, typeSize, previousType; - while (i < size) { - var curr = slab[i]; - - type = singleType || types[i]; - if (type === 0) { - i++; - continue; - } - assert(type, 'Must know what type to store in allocate!'); - - if (type == 'i64') type = 'i32'; // special case: we have one i32 here, and one i32 later - - setValue(ret+i, curr, type); - - // no need to look up size unless type changes, so cache it - if (previousType !== type) { - typeSize = getNativeTypeSize(type); - previousType = type; - } - i += typeSize; - } - - return ret; -} - -// Allocate memory during any stage of startup - static memory early on, dynamic memory later, malloc when ready -function getMemory(size) { - if (!runtimeInitialized) return dynamicAlloc(size); - return _malloc(size); -} - - - - -/** @type {function(number, number=)} */ -function Pointer_stringify(ptr, length) { - abort("this function has been removed - you should use UTF8ToString(ptr, maxBytesToRead) instead!"); -} - -// Given a pointer 'ptr' to a null-terminated ASCII-encoded string in the emscripten HEAP, returns -// a copy of that string as a Javascript String object. - -function AsciiToString(ptr) { - var str = ''; - while (1) { - var ch = HEAPU8[((ptr++)>>0)]; - if (!ch) return str; - str += String.fromCharCode(ch); - } -} - -// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', -// null-terminated and encoded in ASCII form. The copy will require at most str.length+1 bytes of space in the HEAP. - -function stringToAscii(str, outPtr) { - return writeAsciiToMemory(str, outPtr, false); -} - - -// Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the given array that contains uint8 values, returns -// a copy of that string as a Javascript String object. - -var UTF8Decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf8') : undefined; - -/** - * @param {number} idx - * @param {number=} maxBytesToRead - * @return {string} - */ -function UTF8ArrayToString(u8Array, idx, maxBytesToRead) { - var endIdx = idx + maxBytesToRead; - var endPtr = idx; - // TextDecoder needs to know the byte length in advance, it doesn't stop on null terminator by itself. - // Also, use the length info to avoid running tiny strings through TextDecoder, since .subarray() allocates garbage. - // (As a tiny code save trick, compare endPtr against endIdx using a negation, so that undefined means Infinity) - while (u8Array[endPtr] && !(endPtr >= endIdx)) ++endPtr; - - if (endPtr - idx > 16 && u8Array.subarray && UTF8Decoder) { - return UTF8Decoder.decode(u8Array.subarray(idx, endPtr)); - } else { - var str = ''; - // If building with TextDecoder, we have already computed the string length above, so test loop end condition against that - while (idx < endPtr) { - // For UTF8 byte structure, see: - // http://en.wikipedia.org/wiki/UTF-8#Description - // https://www.ietf.org/rfc/rfc2279.txt - // https://tools.ietf.org/html/rfc3629 - var u0 = u8Array[idx++]; - if (!(u0 & 0x80)) { str += String.fromCharCode(u0); continue; } - var u1 = u8Array[idx++] & 63; - if ((u0 & 0xE0) == 0xC0) { str += String.fromCharCode(((u0 & 31) << 6) | u1); continue; } - var u2 = u8Array[idx++] & 63; - if ((u0 & 0xF0) == 0xE0) { - u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; - } else { - if ((u0 & 0xF8) != 0xF0) warnOnce('Invalid UTF-8 leading byte 0x' + u0.toString(16) + ' encountered when deserializing a UTF-8 string on the asm.js/wasm heap to a JS string!'); - u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (u8Array[idx++] & 63); - } - - if (u0 < 0x10000) { - str += String.fromCharCode(u0); - } else { - var ch = u0 - 0x10000; - str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); - } - } - } - return str; -} - -// Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the emscripten HEAP, returns a -// copy of that string as a Javascript String object. -// maxBytesToRead: an optional length that specifies the maximum number of bytes to read. You can omit -// this parameter to scan the string until the first \0 byte. If maxBytesToRead is -// passed, and the string at [ptr, ptr+maxBytesToReadr[ contains a null byte in the -// middle, then the string will cut short at that byte index (i.e. maxBytesToRead will -// not produce a string of exact length [ptr, ptr+maxBytesToRead[) -// N.B. mixing frequent uses of UTF8ToString() with and without maxBytesToRead may -// throw JS JIT optimizations off, so it is worth to consider consistently using one -// style or the other. -/** - * @param {number} ptr - * @param {number=} maxBytesToRead - * @return {string} - */ -function UTF8ToString(ptr, maxBytesToRead) { - return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : ''; -} - -// Copies the given Javascript String object 'str' to the given byte array at address 'outIdx', -// encoded in UTF8 form and null-terminated. The copy will require at most str.length*4+1 bytes of space in the HEAP. -// Use the function lengthBytesUTF8 to compute the exact number of bytes (excluding null terminator) that this function will write. -// Parameters: -// str: the Javascript string to copy. -// outU8Array: the array to copy to. Each index in this array is assumed to be one 8-byte element. -// outIdx: The starting offset in the array to begin the copying. -// maxBytesToWrite: The maximum number of bytes this function can write to the array. -// This count should include the null terminator, -// i.e. if maxBytesToWrite=1, only the null terminator will be written and nothing else. -// maxBytesToWrite=0 does not write any bytes to the output, not even the null terminator. -// Returns the number of bytes written, EXCLUDING the null terminator. - -function stringToUTF8Array(str, outU8Array, outIdx, maxBytesToWrite) { - if (!(maxBytesToWrite > 0)) // Parameter maxBytesToWrite is not optional. Negative values, 0, null, undefined and false each don't write out any bytes. - return 0; - - var startIdx = outIdx; - var endIdx = outIdx + maxBytesToWrite - 1; // -1 for string null terminator. - for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! So decode UTF16->UTF32->UTF8. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description and https://www.ietf.org/rfc/rfc2279.txt and https://tools.ietf.org/html/rfc3629 - var u = str.charCodeAt(i); // possibly a lead surrogate - if (u >= 0xD800 && u <= 0xDFFF) { - var u1 = str.charCodeAt(++i); - u = 0x10000 + ((u & 0x3FF) << 10) | (u1 & 0x3FF); - } - if (u <= 0x7F) { - if (outIdx >= endIdx) break; - outU8Array[outIdx++] = u; - } else if (u <= 0x7FF) { - if (outIdx + 1 >= endIdx) break; - outU8Array[outIdx++] = 0xC0 | (u >> 6); - outU8Array[outIdx++] = 0x80 | (u & 63); - } else if (u <= 0xFFFF) { - if (outIdx + 2 >= endIdx) break; - outU8Array[outIdx++] = 0xE0 | (u >> 12); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); - outU8Array[outIdx++] = 0x80 | (u & 63); - } else { - if (outIdx + 3 >= endIdx) break; - if (u >= 0x200000) warnOnce('Invalid Unicode code point 0x' + u.toString(16) + ' encountered when serializing a JS string to an UTF-8 string on the asm.js/wasm heap! (Valid unicode code points should be in range 0-0x1FFFFF).'); - outU8Array[outIdx++] = 0xF0 | (u >> 18); - outU8Array[outIdx++] = 0x80 | ((u >> 12) & 63); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); - outU8Array[outIdx++] = 0x80 | (u & 63); - } - } - // Null-terminate the pointer to the buffer. - outU8Array[outIdx] = 0; - return outIdx - startIdx; -} - -// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', -// null-terminated and encoded in UTF8 form. The copy will require at most str.length*4+1 bytes of space in the HEAP. -// Use the function lengthBytesUTF8 to compute the exact number of bytes (excluding null terminator) that this function will write. -// Returns the number of bytes written, EXCLUDING the null terminator. - -function stringToUTF8(str, outPtr, maxBytesToWrite) { - assert(typeof maxBytesToWrite == 'number', 'stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); - return stringToUTF8Array(str, HEAPU8,outPtr, maxBytesToWrite); -} - -// Returns the number of bytes the given Javascript string takes if encoded as a UTF8 byte array, EXCLUDING the null terminator byte. -function lengthBytesUTF8(str) { - var len = 0; - for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! So decode UTF16->UTF32->UTF8. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - var u = str.charCodeAt(i); // possibly a lead surrogate - if (u >= 0xD800 && u <= 0xDFFF) u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); - if (u <= 0x7F) ++len; - else if (u <= 0x7FF) len += 2; - else if (u <= 0xFFFF) len += 3; - else len += 4; - } - return len; -} - - -// Given a pointer 'ptr' to a null-terminated UTF16LE-encoded string in the emscripten HEAP, returns -// a copy of that string as a Javascript String object. - -var UTF16Decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-16le') : undefined; -function UTF16ToString(ptr) { - assert(ptr % 2 == 0, 'Pointer passed to UTF16ToString must be aligned to two bytes!'); - var endPtr = ptr; - // TextDecoder needs to know the byte length in advance, it doesn't stop on null terminator by itself. - // Also, use the length info to avoid running tiny strings through TextDecoder, since .subarray() allocates garbage. - var idx = endPtr >> 1; - while (HEAP16[idx]) ++idx; - endPtr = idx << 1; - - if (endPtr - ptr > 32 && UTF16Decoder) { - return UTF16Decoder.decode(HEAPU8.subarray(ptr, endPtr)); - } else { - var i = 0; - - var str = ''; - while (1) { - var codeUnit = HEAP16[(((ptr)+(i*2))>>1)]; - if (codeUnit == 0) return str; - ++i; - // fromCharCode constructs a character from a UTF-16 code unit, so we can pass the UTF16 string right through. - str += String.fromCharCode(codeUnit); - } - } -} - -// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', -// null-terminated and encoded in UTF16 form. The copy will require at most str.length*4+2 bytes of space in the HEAP. -// Use the function lengthBytesUTF16() to compute the exact number of bytes (excluding null terminator) that this function will write. -// Parameters: -// str: the Javascript string to copy. -// outPtr: Byte address in Emscripten HEAP where to write the string to. -// maxBytesToWrite: The maximum number of bytes this function can write to the array. This count should include the null -// terminator, i.e. if maxBytesToWrite=2, only the null terminator will be written and nothing else. -// maxBytesToWrite<2 does not write any bytes to the output, not even the null terminator. -// Returns the number of bytes written, EXCLUDING the null terminator. - -function stringToUTF16(str, outPtr, maxBytesToWrite) { - assert(outPtr % 2 == 0, 'Pointer passed to stringToUTF16 must be aligned to two bytes!'); - assert(typeof maxBytesToWrite == 'number', 'stringToUTF16(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); - // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. - if (maxBytesToWrite === undefined) { - maxBytesToWrite = 0x7FFFFFFF; - } - if (maxBytesToWrite < 2) return 0; - maxBytesToWrite -= 2; // Null terminator. - var startPtr = outPtr; - var numCharsToWrite = (maxBytesToWrite < str.length*2) ? (maxBytesToWrite / 2) : str.length; - for (var i = 0; i < numCharsToWrite; ++i) { - // charCodeAt returns a UTF-16 encoded code unit, so it can be directly written to the HEAP. - var codeUnit = str.charCodeAt(i); // possibly a lead surrogate - HEAP16[((outPtr)>>1)]=codeUnit; - outPtr += 2; - } - // Null-terminate the pointer to the HEAP. - HEAP16[((outPtr)>>1)]=0; - return outPtr - startPtr; -} - -// Returns the number of bytes the given Javascript string takes if encoded as a UTF16 byte array, EXCLUDING the null terminator byte. - -function lengthBytesUTF16(str) { - return str.length*2; -} - -function UTF32ToString(ptr) { - assert(ptr % 4 == 0, 'Pointer passed to UTF32ToString must be aligned to four bytes!'); - var i = 0; - - var str = ''; - while (1) { - var utf32 = HEAP32[(((ptr)+(i*4))>>2)]; - if (utf32 == 0) - return str; - ++i; - // Gotcha: fromCharCode constructs a character from a UTF-16 encoded code (pair), not from a Unicode code point! So encode the code point to UTF-16 for constructing. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - if (utf32 >= 0x10000) { - var ch = utf32 - 0x10000; - str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); - } else { - str += String.fromCharCode(utf32); - } - } -} - -// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', -// null-terminated and encoded in UTF32 form. The copy will require at most str.length*4+4 bytes of space in the HEAP. -// Use the function lengthBytesUTF32() to compute the exact number of bytes (excluding null terminator) that this function will write. -// Parameters: -// str: the Javascript string to copy. -// outPtr: Byte address in Emscripten HEAP where to write the string to. -// maxBytesToWrite: The maximum number of bytes this function can write to the array. This count should include the null -// terminator, i.e. if maxBytesToWrite=4, only the null terminator will be written and nothing else. -// maxBytesToWrite<4 does not write any bytes to the output, not even the null terminator. -// Returns the number of bytes written, EXCLUDING the null terminator. - -function stringToUTF32(str, outPtr, maxBytesToWrite) { - assert(outPtr % 4 == 0, 'Pointer passed to stringToUTF32 must be aligned to four bytes!'); - assert(typeof maxBytesToWrite == 'number', 'stringToUTF32(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); - // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. - if (maxBytesToWrite === undefined) { - maxBytesToWrite = 0x7FFFFFFF; - } - if (maxBytesToWrite < 4) return 0; - var startPtr = outPtr; - var endPtr = startPtr + maxBytesToWrite - 4; - for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! We must decode the string to UTF-32 to the heap. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - var codeUnit = str.charCodeAt(i); // possibly a lead surrogate - if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) { - var trailSurrogate = str.charCodeAt(++i); - codeUnit = 0x10000 + ((codeUnit & 0x3FF) << 10) | (trailSurrogate & 0x3FF); - } - HEAP32[((outPtr)>>2)]=codeUnit; - outPtr += 4; - if (outPtr + 4 > endPtr) break; - } - // Null-terminate the pointer to the HEAP. - HEAP32[((outPtr)>>2)]=0; - return outPtr - startPtr; -} - -// Returns the number of bytes the given Javascript string takes if encoded as a UTF16 byte array, EXCLUDING the null terminator byte. - -function lengthBytesUTF32(str) { - var len = 0; - for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! We must decode the string to UTF-32 to the heap. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - var codeUnit = str.charCodeAt(i); - if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) ++i; // possibly a lead surrogate, so skip over the tail surrogate. - len += 4; - } - - return len; -} - -// Allocate heap space for a JS string, and write it there. -// It is the responsibility of the caller to free() that memory. -function allocateUTF8(str) { - var size = lengthBytesUTF8(str) + 1; - var ret = _malloc(size); - if (ret) stringToUTF8Array(str, HEAP8, ret, size); - return ret; -} - -// Allocate stack space for a JS string, and write it there. -function allocateUTF8OnStack(str) { - var size = lengthBytesUTF8(str) + 1; - var ret = stackAlloc(size); - stringToUTF8Array(str, HEAP8, ret, size); - return ret; -} - -// Deprecated: This function should not be called because it is unsafe and does not provide -// a maximum length limit of how many bytes it is allowed to write. Prefer calling the -// function stringToUTF8Array() instead, which takes in a maximum length that can be used -// to be secure from out of bounds writes. -/** @deprecated */ -function writeStringToMemory(string, buffer, dontAddNull) { - warnOnce('writeStringToMemory is deprecated and should not be called! Use stringToUTF8() instead!'); - - var /** @type {number} */ lastChar, /** @type {number} */ end; - if (dontAddNull) { - // stringToUTF8Array always appends null. If we don't want to do that, remember the - // character that existed at the location where the null will be placed, and restore - // that after the write (below). - end = buffer + lengthBytesUTF8(string); - lastChar = HEAP8[end]; - } - stringToUTF8(string, buffer, Infinity); - if (dontAddNull) HEAP8[end] = lastChar; // Restore the value under the null character. -} - -function writeArrayToMemory(array, buffer) { - assert(array.length >= 0, 'writeArrayToMemory array must have a length (should be an array or typed array)') - HEAP8.set(array, buffer); -} - -function writeAsciiToMemory(str, buffer, dontAddNull) { - for (var i = 0; i < str.length; ++i) { - assert(str.charCodeAt(i) === str.charCodeAt(i)&0xff); - HEAP8[((buffer++)>>0)]=str.charCodeAt(i); - } - // Null-terminate the pointer to the HEAP. - if (!dontAddNull) HEAP8[((buffer)>>0)]=0; -} - - - - - -function demangle(func) { - warnOnce('warning: build with -s DEMANGLE_SUPPORT=1 to link in libcxxabi demangling'); - return func; -} - -function demangleAll(text) { - var regex = - /__Z[\w\d_]+/g; - return text.replace(regex, - function(x) { - var y = demangle(x); - return x === y ? x : (y + ' [' + x + ']'); - }); -} - -function jsStackTrace() { - var err = new Error(); - if (!err.stack) { - // IE10+ special cases: It does have callstack info, but it is only populated if an Error object is thrown, - // so try that as a special-case. - try { - throw new Error(0); - } catch(e) { - err = e; - } - if (!err.stack) { - return '(no stack trace available)'; - } - } - return err.stack.toString(); -} - -function stackTrace() { - var js = jsStackTrace(); - if (Module['extraStackTrace']) js += '\n' + Module['extraStackTrace'](); - return demangleAll(js); -} - - - -// Memory management - -var PAGE_SIZE = 16384; -var WASM_PAGE_SIZE = 65536; -var ASMJS_PAGE_SIZE = 16777216; - -function alignUp(x, multiple) { - if (x % multiple > 0) { - x += multiple - (x % multiple); - } - return x; -} - -var HEAP, -/** @type {ArrayBuffer} */ - buffer, -/** @type {Int8Array} */ - HEAP8, -/** @type {Uint8Array} */ - HEAPU8, -/** @type {Int16Array} */ - HEAP16, -/** @type {Uint16Array} */ - HEAPU16, -/** @type {Int32Array} */ - HEAP32, -/** @type {Uint32Array} */ - HEAPU32, -/** @type {Float32Array} */ - HEAPF32, -/** @type {Float64Array} */ - HEAPF64; - -function updateGlobalBufferViews() { - Module['HEAP8'] = HEAP8 = new Int8Array(buffer); - Module['HEAP16'] = HEAP16 = new Int16Array(buffer); - Module['HEAP32'] = HEAP32 = new Int32Array(buffer); - Module['HEAPU8'] = HEAPU8 = new Uint8Array(buffer); - Module['HEAPU16'] = HEAPU16 = new Uint16Array(buffer); - Module['HEAPU32'] = HEAPU32 = new Uint32Array(buffer); - Module['HEAPF32'] = HEAPF32 = new Float32Array(buffer); - Module['HEAPF64'] = HEAPF64 = new Float64Array(buffer); -} - - -var STATIC_BASE = 1024, - STACK_BASE = 12240, - STACKTOP = STACK_BASE, - STACK_MAX = 5255120, - DYNAMIC_BASE = 5255120, - DYNAMICTOP_PTR = 12208; - -assert(STACK_BASE % 16 === 0, 'stack must start aligned'); -assert(DYNAMIC_BASE % 16 === 0, 'heap must start aligned'); - - - -var TOTAL_STACK = 5242880; -if (Module['TOTAL_STACK']) assert(TOTAL_STACK === Module['TOTAL_STACK'], 'the stack size can no longer be determined at runtime') - -var INITIAL_TOTAL_MEMORY = Module['TOTAL_MEMORY'] || 16777216; -if (INITIAL_TOTAL_MEMORY < TOTAL_STACK) err('TOTAL_MEMORY should be larger than TOTAL_STACK, was ' + INITIAL_TOTAL_MEMORY + '! (TOTAL_STACK=' + TOTAL_STACK + ')'); - -// Initialize the runtime's memory -// check for full engine support (use string 'subarray' to avoid closure compiler confusion) -assert(typeof Int32Array !== 'undefined' && typeof Float64Array !== 'undefined' && Int32Array.prototype.subarray !== undefined && Int32Array.prototype.set !== undefined, - 'JS engine does not provide full typed array support'); - - - - - - - -// Use a provided buffer, if there is one, or else allocate a new one -if (Module['buffer']) { - buffer = Module['buffer']; - assert(buffer.byteLength === INITIAL_TOTAL_MEMORY, 'provided buffer should be ' + INITIAL_TOTAL_MEMORY + ' bytes, but it is ' + buffer.byteLength); -} else { - // Use a WebAssembly memory where available - if (typeof WebAssembly === 'object' && typeof WebAssembly.Memory === 'function') { - assert(INITIAL_TOTAL_MEMORY % WASM_PAGE_SIZE === 0); - wasmMemory = new WebAssembly.Memory({ 'initial': INITIAL_TOTAL_MEMORY / WASM_PAGE_SIZE, 'maximum': INITIAL_TOTAL_MEMORY / WASM_PAGE_SIZE }); - buffer = wasmMemory.buffer; - } else - { - buffer = new ArrayBuffer(INITIAL_TOTAL_MEMORY); - } - assert(buffer.byteLength === INITIAL_TOTAL_MEMORY); -} -updateGlobalBufferViews(); - - -HEAP32[DYNAMICTOP_PTR>>2] = DYNAMIC_BASE; - - -// Initializes the stack cookie. Called at the startup of main and at the startup of each thread in pthreads mode. -function writeStackCookie() { - assert((STACK_MAX & 3) == 0); - HEAPU32[(STACK_MAX >> 2)-1] = 0x02135467; - HEAPU32[(STACK_MAX >> 2)-2] = 0x89BACDFE; -} - -function checkStackCookie() { - if (HEAPU32[(STACK_MAX >> 2)-1] != 0x02135467 || HEAPU32[(STACK_MAX >> 2)-2] != 0x89BACDFE) { - abort('Stack overflow! Stack cookie has been overwritten, expected hex dwords 0x89BACDFE and 0x02135467, but received 0x' + HEAPU32[(STACK_MAX >> 2)-2].toString(16) + ' ' + HEAPU32[(STACK_MAX >> 2)-1].toString(16)); - } - // Also test the global address 0 for integrity. - if (HEAP32[0] !== 0x63736d65 /* 'emsc' */) throw 'Runtime error: The application has corrupted its heap memory area (address zero)!'; -} - -function abortStackOverflow(allocSize) { - abort('Stack overflow! Attempted to allocate ' + allocSize + ' bytes on the stack, but stack has only ' + (STACK_MAX - stackSave() + allocSize) + ' bytes available!'); -} - - - HEAP32[0] = 0x63736d65; /* 'emsc' */ - - - -// Endianness check (note: assumes compiler arch was little-endian) -HEAP16[1] = 0x6373; -if (HEAPU8[2] !== 0x73 || HEAPU8[3] !== 0x63) throw 'Runtime error: expected the system to be little-endian!'; - -function callRuntimeCallbacks(callbacks) { - while(callbacks.length > 0) { - var callback = callbacks.shift(); - if (typeof callback == 'function') { - callback(); - continue; - } - var func = callback.func; - if (typeof func === 'number') { - if (callback.arg === undefined) { - Module['dynCall_v'](func); - } else { - Module['dynCall_vi'](func, callback.arg); - } - } else { - func(callback.arg === undefined ? null : callback.arg); - } - } -} - -var __ATPRERUN__ = []; // functions called before the runtime is initialized -var __ATINIT__ = []; // functions called during startup -var __ATMAIN__ = []; // functions called when main() is to be run -var __ATEXIT__ = []; // functions called during shutdown -var __ATPOSTRUN__ = []; // functions called after the main() is called - -var runtimeInitialized = false; -var runtimeExited = false; - - -function preRun() { - // compatibility - merge in anything from Module['preRun'] at this time - if (Module['preRun']) { - if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; - while (Module['preRun'].length) { - addOnPreRun(Module['preRun'].shift()); - } - } - callRuntimeCallbacks(__ATPRERUN__); -} - -function ensureInitRuntime() { - checkStackCookie(); - if (runtimeInitialized) return; - runtimeInitialized = true; - - callRuntimeCallbacks(__ATINIT__); -} - -function preMain() { - checkStackCookie(); - - callRuntimeCallbacks(__ATMAIN__); -} - -function exitRuntime() { - checkStackCookie(); - runtimeExited = true; -} - -function postRun() { - checkStackCookie(); - // compatibility - merge in anything from Module['postRun'] at this time - if (Module['postRun']) { - if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; - while (Module['postRun'].length) { - addOnPostRun(Module['postRun'].shift()); - } - } - callRuntimeCallbacks(__ATPOSTRUN__); -} - -function addOnPreRun(cb) { - __ATPRERUN__.unshift(cb); -} - -function addOnInit(cb) { - __ATINIT__.unshift(cb); -} - -function addOnPreMain(cb) { - __ATMAIN__.unshift(cb); -} - -function addOnExit(cb) { -} - -function addOnPostRun(cb) { - __ATPOSTRUN__.unshift(cb); -} - -function unSign(value, bits, ignore) { - if (value >= 0) { - return value; - } - return bits <= 32 ? 2*Math.abs(1 << (bits-1)) + value // Need some trickery, since if bits == 32, we are right at the limit of the bits JS uses in bitshifts - : Math.pow(2, bits) + value; -} -function reSign(value, bits, ignore) { - if (value <= 0) { - return value; - } - var half = bits <= 32 ? Math.abs(1 << (bits-1)) // abs is needed if bits == 32 - : Math.pow(2, bits-1); - if (value >= half && (bits <= 32 || value > half)) { // for huge values, we can hit the precision limit and always get true here. so don't do that - // but, in general there is no perfect solution here. With 64-bit ints, we get rounding and errors - // TODO: In i64 mode 1, resign the two parts separately and safely - value = -2*half + value; // Cannot bitshift half, as it may be at the limit of the bits JS uses in bitshifts - } - return value; -} - - -assert(Math.imul, 'This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -assert(Math.fround, 'This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -assert(Math.clz32, 'This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -assert(Math.trunc, 'This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); - -var Math_abs = Math.abs; -var Math_cos = Math.cos; -var Math_sin = Math.sin; -var Math_tan = Math.tan; -var Math_acos = Math.acos; -var Math_asin = Math.asin; -var Math_atan = Math.atan; -var Math_atan2 = Math.atan2; -var Math_exp = Math.exp; -var Math_log = Math.log; -var Math_sqrt = Math.sqrt; -var Math_ceil = Math.ceil; -var Math_floor = Math.floor; -var Math_pow = Math.pow; -var Math_imul = Math.imul; -var Math_fround = Math.fround; -var Math_round = Math.round; -var Math_min = Math.min; -var Math_max = Math.max; -var Math_clz32 = Math.clz32; -var Math_trunc = Math.trunc; - - - -// A counter of dependencies for calling run(). If we need to -// do asynchronous work before running, increment this and -// decrement it. Incrementing must happen in a place like -// Module.preRun (used by emcc to add file preloading). -// Note that you can add dependencies in preRun, even though -// it happens right before run - run will be postponed until -// the dependencies are met. -var runDependencies = 0; -var runDependencyWatcher = null; -var dependenciesFulfilled = null; // overridden to take different actions when all run dependencies are fulfilled -var runDependencyTracking = {}; - -function getUniqueRunDependency(id) { - var orig = id; - while (1) { - if (!runDependencyTracking[id]) return id; - id = orig + Math.random(); - } - return id; -} - -function addRunDependency(id) { - runDependencies++; - if (Module['monitorRunDependencies']) { - Module['monitorRunDependencies'](runDependencies); - } - if (id) { - assert(!runDependencyTracking[id]); - runDependencyTracking[id] = 1; - if (runDependencyWatcher === null && typeof setInterval !== 'undefined') { - // Check for missing dependencies every few seconds - runDependencyWatcher = setInterval(function() { - if (ABORT) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - return; - } - var shown = false; - for (var dep in runDependencyTracking) { - if (!shown) { - shown = true; - err('still waiting on run dependencies:'); - } - err('dependency: ' + dep); - } - if (shown) { - err('(end of list)'); - } - }, 10000); - } - } else { - err('warning: run dependency added without ID'); - } -} - -function removeRunDependency(id) { - runDependencies--; - if (Module['monitorRunDependencies']) { - Module['monitorRunDependencies'](runDependencies); - } - if (id) { - assert(runDependencyTracking[id]); - delete runDependencyTracking[id]; - } else { - err('warning: run dependency removed without ID'); - } - if (runDependencies == 0) { - if (runDependencyWatcher !== null) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - } - if (dependenciesFulfilled) { - var callback = dependenciesFulfilled; - dependenciesFulfilled = null; - callback(); // can add another dependenciesFulfilled - } - } -} - -Module["preloadedImages"] = {}; // maps url to image data -Module["preloadedAudios"] = {}; // maps url to audio data - - -var memoryInitializer = null; - - - -// show errors on likely calls to FS when it was not included -var FS = { - error: function() { - abort('Filesystem support (FS) was not included. The problem is that you are using files from JS, but files were not used from C/C++, so filesystem support was not auto-included. You can force-include filesystem support with -s FORCE_FILESYSTEM=1'); - }, - init: function() { FS.error() }, - createDataFile: function() { FS.error() }, - createPreloadedFile: function() { FS.error() }, - createLazyFile: function() { FS.error() }, - open: function() { FS.error() }, - mkdev: function() { FS.error() }, - registerDevice: function() { FS.error() }, - analyzePath: function() { FS.error() }, - loadFilesFromDB: function() { FS.error() }, - - ErrnoError: function ErrnoError() { FS.error() }, -}; -Module['FS_createDataFile'] = FS.createDataFile; -Module['FS_createPreloadedFile'] = FS.createPreloadedFile; - - - -// Copyright 2017 The Emscripten Authors. All rights reserved. -// Emscripten is available under two separate licenses, the MIT license and the -// University of Illinois/NCSA Open Source License. Both these licenses can be -// found in the LICENSE file. - -// Prefix of data URIs emitted by SINGLE_FILE and related options. -var dataURIPrefix = 'data:application/octet-stream;base64,'; - -// Indicates whether filename is a base64 data URI. -function isDataURI(filename) { - return String.prototype.startsWith ? - filename.startsWith(dataURIPrefix) : - filename.indexOf(dataURIPrefix) === 0; -} - - - - -var wasmBinaryFile = 'libmbe.wasm'; -if (!isDataURI(wasmBinaryFile)) { - wasmBinaryFile = locateFile(wasmBinaryFile); -} - -function getBinary() { - try { - if (Module['wasmBinary']) { - return new Uint8Array(Module['wasmBinary']); - } - if (Module['readBinary']) { - return Module['readBinary'](wasmBinaryFile); - } else { - throw "both async and sync fetching of the wasm failed"; - } - } - catch (err) { - abort(err); - } -} - -function getBinaryPromise() { - // if we don't have the binary yet, and have the Fetch api, use that - // in some environments, like Electron's render process, Fetch api may be present, but have a different context than expected, let's only use it on the Web - if (!Module['wasmBinary'] && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) && typeof fetch === 'function') { - return fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function(response) { - if (!response['ok']) { - throw "failed to load wasm binary file at '" + wasmBinaryFile + "'"; - } - return response['arrayBuffer'](); - }).catch(function () { - return getBinary(); - }); - } - // Otherwise, getBinary should be able to get it synchronously - return new Promise(function(resolve, reject) { - resolve(getBinary()); - }); -} - -// Create the wasm instance. -// Receives the wasm imports, returns the exports. -function createWasm(env) { - // prepare imports - var info = { - 'env': env - , - 'global': { - 'NaN': NaN, - 'Infinity': Infinity - }, - 'global.Math': Math, - 'asm2wasm': asm2wasmImports - }; - // Load the wasm module and create an instance of using native support in the JS engine. - // handle a generated wasm instance, receiving its exports and - // performing other necessary setup - function receiveInstance(instance, module) { - var exports = instance.exports; - Module['asm'] = exports; - removeRunDependency('wasm-instantiate'); - } - addRunDependency('wasm-instantiate'); - - // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback - // to manually instantiate the Wasm module themselves. This allows pages to run the instantiation parallel - // to any other async startup actions they are performing. - if (Module['instantiateWasm']) { - try { - return Module['instantiateWasm'](info, receiveInstance); - } catch(e) { - err('Module.instantiateWasm callback failed with error: ' + e); - return false; - } - } - - // Async compilation can be confusing when an error on the page overwrites Module - // (for example, if the order of elements is wrong, and the one defining Module is - // later), so we save Module and check it later. - var trueModule = Module; - function receiveInstantiatedSource(output) { - // 'output' is a WebAssemblyInstantiatedSource object which has both the module and instance. - // receiveInstance() will swap in the exports (to Module.asm) so they can be called - assert(Module === trueModule, 'the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?'); - trueModule = null; - // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. - // When the regression is fixed, can restore the above USE_PTHREADS-enabled path. - receiveInstance(output['instance']); - } - function instantiateArrayBuffer(receiver) { - getBinaryPromise().then(function(binary) { - return WebAssembly.instantiate(binary, info); - }).then(receiver, function(reason) { - err('failed to asynchronously prepare wasm: ' + reason); - abort(reason); - }); - } - // Prefer streaming instantiation if available. - if (!Module['wasmBinary'] && - typeof WebAssembly.instantiateStreaming === 'function' && - !isDataURI(wasmBinaryFile) && - typeof fetch === 'function') { - WebAssembly.instantiateStreaming(fetch(wasmBinaryFile, { credentials: 'same-origin' }), info) - .then(receiveInstantiatedSource, function(reason) { - // We expect the most common failure cause to be a bad MIME type for the binary, - // in which case falling back to ArrayBuffer instantiation should work. - err('wasm streaming compile failed: ' + reason); - err('falling back to ArrayBuffer instantiation'); - instantiateArrayBuffer(receiveInstantiatedSource); - }); - } else { - instantiateArrayBuffer(receiveInstantiatedSource); - } - return {}; // no exports yet; we'll fill them in later -} - -// Provide an "asm.js function" for the application, called to "link" the asm.js module. We instantiate -// the wasm module at that time, and it receives imports and provides exports and so forth, the app -// doesn't need to care that it is wasm or asm.js. - -Module['asm'] = function(global, env, providedBuffer) { - // memory was already allocated (so js could use the buffer) - env['memory'] = wasmMemory - ; - // import table - env['table'] = wasmTable = new WebAssembly.Table({ - 'initial': 14, - 'maximum': 14, - 'element': 'anyfunc' - }); - // With the wasm backend __memory_base and __table_base and only needed for - // relocatable output. - env['__memory_base'] = 1024; // tell the memory segments where to place themselves - // table starts at 0 by default (even in dynamic linking, for the main module) - env['__table_base'] = 0; - - var exports = createWasm(env); - assert(exports, 'binaryen setup failed (no wasm support?)'); - return exports; -}; - -// === Body === - -var ASM_CONSTS = []; - - - - - -// STATICTOP = STATIC_BASE + 11216; -/* global initializers */ /*__ATINIT__.push();*/ - - - - - - - - -/* no memory initializer */ -var tempDoublePtr = 12224 -assert(tempDoublePtr % 8 == 0); - -function copyTempFloat(ptr) { // functions, because inlining this code increases code size too much - HEAP8[tempDoublePtr] = HEAP8[ptr]; - HEAP8[tempDoublePtr+1] = HEAP8[ptr+1]; - HEAP8[tempDoublePtr+2] = HEAP8[ptr+2]; - HEAP8[tempDoublePtr+3] = HEAP8[ptr+3]; -} - -function copyTempDouble(ptr) { - HEAP8[tempDoublePtr] = HEAP8[ptr]; - HEAP8[tempDoublePtr+1] = HEAP8[ptr+1]; - HEAP8[tempDoublePtr+2] = HEAP8[ptr+2]; - HEAP8[tempDoublePtr+3] = HEAP8[ptr+3]; - HEAP8[tempDoublePtr+4] = HEAP8[ptr+4]; - HEAP8[tempDoublePtr+5] = HEAP8[ptr+5]; - HEAP8[tempDoublePtr+6] = HEAP8[ptr+6]; - HEAP8[tempDoublePtr+7] = HEAP8[ptr+7]; -} - -// {{PRE_LIBRARY}} - - - function ___lock() {} - - - var SYSCALLS={buffers:[null,[],[]],printChar:function(stream, curr) { - var buffer = SYSCALLS.buffers[stream]; - assert(buffer); - if (curr === 0 || curr === 10) { - (stream === 1 ? out : err)(UTF8ArrayToString(buffer, 0)); - buffer.length = 0; - } else { - buffer.push(curr); - } - },varargs:0,get:function(varargs) { - SYSCALLS.varargs += 4; - var ret = HEAP32[(((SYSCALLS.varargs)-(4))>>2)]; - return ret; - },getStr:function() { - var ret = UTF8ToString(SYSCALLS.get()); - return ret; - },get64:function() { - var low = SYSCALLS.get(), high = SYSCALLS.get(); - if (low >= 0) assert(high === 0); - else assert(high === -1); - return low; - },getZero:function() { - assert(SYSCALLS.get() === 0); - }};function ___syscall140(which, varargs) {SYSCALLS.varargs = varargs; - try { - // llseek - var stream = SYSCALLS.getStreamFromFD(), offset_high = SYSCALLS.get(), offset_low = SYSCALLS.get(), result = SYSCALLS.get(), whence = SYSCALLS.get(); - abort('it should not be possible to operate on streams when !SYSCALLS_REQUIRE_FILESYSTEM'); - return 0; - } catch (e) { - if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); - return -e.errno; - } - } - - - function flush_NO_FILESYSTEM() { - // flush anything remaining in the buffers during shutdown - var fflush = Module["_fflush"]; - if (fflush) fflush(0); - var buffers = SYSCALLS.buffers; - if (buffers[1].length) SYSCALLS.printChar(1, 10); - if (buffers[2].length) SYSCALLS.printChar(2, 10); - }function ___syscall146(which, varargs) {SYSCALLS.varargs = varargs; - try { - // writev - // hack to support printf in SYSCALLS_REQUIRE_FILESYSTEM=0 - var stream = SYSCALLS.get(), iov = SYSCALLS.get(), iovcnt = SYSCALLS.get(); - var ret = 0; - for (var i = 0; i < iovcnt; i++) { - var ptr = HEAP32[(((iov)+(i*8))>>2)]; - var len = HEAP32[(((iov)+(i*8 + 4))>>2)]; - for (var j = 0; j < len; j++) { - SYSCALLS.printChar(stream, HEAPU8[ptr+j]); - } - ret += len; - } - return ret; - } catch (e) { - if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); - return -e.errno; - } - } - - function ___syscall54(which, varargs) {SYSCALLS.varargs = varargs; - try { - // ioctl - return 0; - } catch (e) { - if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); - return -e.errno; - } - } - - function ___syscall6(which, varargs) {SYSCALLS.varargs = varargs; - try { - // close - var stream = SYSCALLS.getStreamFromFD(); - abort('it should not be possible to operate on streams when !SYSCALLS_REQUIRE_FILESYSTEM'); - return 0; - } catch (e) { - if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); - return -e.errno; - } - } - - function ___unlock() {} - - function _emscripten_get_heap_size() { - return HEAP8.length; - } - - - function abortOnCannotGrowMemory(requestedSize) { - abort('Cannot enlarge memory arrays to size ' + requestedSize + ' bytes (OOM). Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value ' + HEAP8.length + ', (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 '); - }function _emscripten_resize_heap(requestedSize) { - abortOnCannotGrowMemory(requestedSize); - } - - - function _emscripten_memcpy_big(dest, src, num) { - HEAPU8.set(HEAPU8.subarray(src, src+num), dest); - } - - - - - - - function ___setErrNo(value) { - if (Module['___errno_location']) HEAP32[((Module['___errno_location']())>>2)]=value; - else err('failed to set errno from JS'); - return value; - } -var ASSERTIONS = true; - -// Copyright 2017 The Emscripten Authors. All rights reserved. -// Emscripten is available under two separate licenses, the MIT license and the -// University of Illinois/NCSA Open Source License. Both these licenses can be -// found in the LICENSE file. - -/** @type {function(string, boolean=, number=)} */ -function intArrayFromString(stringy, dontAddNull, length) { - var len = length > 0 ? length : lengthBytesUTF8(stringy)+1; - var u8array = new Array(len); - var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); - if (dontAddNull) u8array.length = numBytesWritten; - return u8array; -} - -function intArrayToString(array) { - var ret = []; - for (var i = 0; i < array.length; i++) { - var chr = array[i]; - if (chr > 0xFF) { - if (ASSERTIONS) { - assert(false, 'Character code ' + chr + ' (' + String.fromCharCode(chr) + ') at offset ' + i + ' not in 0x00-0xFF.'); - } - chr &= 0xFF; - } - ret.push(String.fromCharCode(chr)); - } - return ret.join(''); -} - - -// ASM_LIBRARY EXTERN PRIMITIVES: Int8Array,Int32Array - - -function nullFunc_ii(x) { err("Invalid function pointer called with signature 'ii'. Perhaps this is an invalid value (e.g. caused by calling a virtual method on a NULL pointer)? Or calling a function with an incorrect type, which will fail? (it is worth building your source files with -Werror (warnings are errors), as warnings can indicate undefined behavior which can cause this)"); err("Build with ASSERTIONS=2 for more info.");abort(x) } - -function nullFunc_iiii(x) { err("Invalid function pointer called with signature 'iiii'. Perhaps this is an invalid value (e.g. caused by calling a virtual method on a NULL pointer)? Or calling a function with an incorrect type, which will fail? (it is worth building your source files with -Werror (warnings are errors), as warnings can indicate undefined behavior which can cause this)"); err("Build with ASSERTIONS=2 for more info.");abort(x) } - -function nullFunc_jiji(x) { err("Invalid function pointer called with signature 'jiji'. Perhaps this is an invalid value (e.g. caused by calling a virtual method on a NULL pointer)? Or calling a function with an incorrect type, which will fail? (it is worth building your source files with -Werror (warnings are errors), as warnings can indicate undefined behavior which can cause this)"); err("Build with ASSERTIONS=2 for more info.");abort(x) } - -var asmGlobalArg = {} - -var asmLibraryArg = { - "abort": abort, - "setTempRet0": setTempRet0, - "getTempRet0": getTempRet0, - "abortStackOverflow": abortStackOverflow, - "nullFunc_ii": nullFunc_ii, - "nullFunc_iiii": nullFunc_iiii, - "nullFunc_jiji": nullFunc_jiji, - "___lock": ___lock, - "___setErrNo": ___setErrNo, - "___syscall140": ___syscall140, - "___syscall146": ___syscall146, - "___syscall54": ___syscall54, - "___syscall6": ___syscall6, - "___unlock": ___unlock, - "_emscripten_get_heap_size": _emscripten_get_heap_size, - "_emscripten_memcpy_big": _emscripten_memcpy_big, - "_emscripten_resize_heap": _emscripten_resize_heap, - "abortOnCannotGrowMemory": abortOnCannotGrowMemory, - "flush_NO_FILESYSTEM": flush_NO_FILESYSTEM, - "tempDoublePtr": tempDoublePtr, - "DYNAMICTOP_PTR": DYNAMICTOP_PTR -} -// EMSCRIPTEN_START_ASM -var asm =Module["asm"]// EMSCRIPTEN_END_ASM -(asmGlobalArg, asmLibraryArg, buffer); - -var real____errno_location = asm["___errno_location"]; asm["___errno_location"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return real____errno_location.apply(null, arguments); -}; - -var real__fflush = asm["_fflush"]; asm["_fflush"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return real__fflush.apply(null, arguments); -}; - -var real__free = asm["_free"]; asm["_free"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return real__free.apply(null, arguments); -}; - -var real__malloc = asm["_malloc"]; asm["_malloc"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return real__malloc.apply(null, arguments); -}; - -var real__mbe_eccAmbe3600x2400C0 = asm["_mbe_eccAmbe3600x2400C0"]; asm["_mbe_eccAmbe3600x2400C0"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return real__mbe_eccAmbe3600x2400C0.apply(null, arguments); -}; - -var real__sbrk = asm["_sbrk"]; asm["_sbrk"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return real__sbrk.apply(null, arguments); -}; - -var real_establishStackSpace = asm["establishStackSpace"]; asm["establishStackSpace"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return real_establishStackSpace.apply(null, arguments); -}; - -var real_stackAlloc = asm["stackAlloc"]; asm["stackAlloc"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return real_stackAlloc.apply(null, arguments); -}; - -var real_stackRestore = asm["stackRestore"]; asm["stackRestore"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return real_stackRestore.apply(null, arguments); -}; - -var real_stackSave = asm["stackSave"]; asm["stackSave"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return real_stackSave.apply(null, arguments); -}; -Module["asm"] = asm; -var ___errno_location = Module["___errno_location"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["___errno_location"].apply(null, arguments) }; -var _fflush = Module["_fflush"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["_fflush"].apply(null, arguments) }; -var _free = Module["_free"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["_free"].apply(null, arguments) }; -var _malloc = Module["_malloc"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["_malloc"].apply(null, arguments) }; -var _mbe_eccAmbe3600x2400C0 = Module["_mbe_eccAmbe3600x2400C0"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["_mbe_eccAmbe3600x2400C0"].apply(null, arguments) }; -var _memcpy = Module["_memcpy"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["_memcpy"].apply(null, arguments) }; -var _memset = Module["_memset"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["_memset"].apply(null, arguments) }; -var _sbrk = Module["_sbrk"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["_sbrk"].apply(null, arguments) }; -var establishStackSpace = Module["establishStackSpace"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["establishStackSpace"].apply(null, arguments) }; -var stackAlloc = Module["stackAlloc"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["stackAlloc"].apply(null, arguments) }; -var stackRestore = Module["stackRestore"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["stackRestore"].apply(null, arguments) }; -var stackSave = Module["stackSave"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["stackSave"].apply(null, arguments) }; -var dynCall_ii = Module["dynCall_ii"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["dynCall_ii"].apply(null, arguments) }; -var dynCall_iiii = Module["dynCall_iiii"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["dynCall_iiii"].apply(null, arguments) }; -var dynCall_jiji = Module["dynCall_jiji"] = function() { - assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); - assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); - return Module["asm"]["dynCall_jiji"].apply(null, arguments) }; -; - - - -// === Auto-generated postamble setup entry stuff === - -Module['asm'] = asm; - -if (!Module["intArrayFromString"]) Module["intArrayFromString"] = function() { abort("'intArrayFromString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["intArrayToString"]) Module["intArrayToString"] = function() { abort("'intArrayToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["ccall"]) Module["ccall"] = function() { abort("'ccall' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["cwrap"]) Module["cwrap"] = function() { abort("'cwrap' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["setValue"]) Module["setValue"] = function() { abort("'setValue' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["getValue"]) Module["getValue"] = function() { abort("'getValue' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["allocate"]) Module["allocate"] = function() { abort("'allocate' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["getMemory"]) Module["getMemory"] = function() { abort("'getMemory' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; -if (!Module["AsciiToString"]) Module["AsciiToString"] = function() { abort("'AsciiToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["stringToAscii"]) Module["stringToAscii"] = function() { abort("'stringToAscii' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["UTF8ArrayToString"]) Module["UTF8ArrayToString"] = function() { abort("'UTF8ArrayToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["UTF8ToString"]) Module["UTF8ToString"] = function() { abort("'UTF8ToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["stringToUTF8Array"]) Module["stringToUTF8Array"] = function() { abort("'stringToUTF8Array' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["stringToUTF8"]) Module["stringToUTF8"] = function() { abort("'stringToUTF8' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["lengthBytesUTF8"]) Module["lengthBytesUTF8"] = function() { abort("'lengthBytesUTF8' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["UTF16ToString"]) Module["UTF16ToString"] = function() { abort("'UTF16ToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["stringToUTF16"]) Module["stringToUTF16"] = function() { abort("'stringToUTF16' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["lengthBytesUTF16"]) Module["lengthBytesUTF16"] = function() { abort("'lengthBytesUTF16' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["UTF32ToString"]) Module["UTF32ToString"] = function() { abort("'UTF32ToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["stringToUTF32"]) Module["stringToUTF32"] = function() { abort("'stringToUTF32' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["lengthBytesUTF32"]) Module["lengthBytesUTF32"] = function() { abort("'lengthBytesUTF32' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["allocateUTF8"]) Module["allocateUTF8"] = function() { abort("'allocateUTF8' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["stackTrace"]) Module["stackTrace"] = function() { abort("'stackTrace' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["addOnPreRun"]) Module["addOnPreRun"] = function() { abort("'addOnPreRun' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["addOnInit"]) Module["addOnInit"] = function() { abort("'addOnInit' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["addOnPreMain"]) Module["addOnPreMain"] = function() { abort("'addOnPreMain' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["addOnExit"]) Module["addOnExit"] = function() { abort("'addOnExit' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["addOnPostRun"]) Module["addOnPostRun"] = function() { abort("'addOnPostRun' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["writeStringToMemory"]) Module["writeStringToMemory"] = function() { abort("'writeStringToMemory' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["writeArrayToMemory"]) Module["writeArrayToMemory"] = function() { abort("'writeArrayToMemory' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["writeAsciiToMemory"]) Module["writeAsciiToMemory"] = function() { abort("'writeAsciiToMemory' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["addRunDependency"]) Module["addRunDependency"] = function() { abort("'addRunDependency' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; -if (!Module["removeRunDependency"]) Module["removeRunDependency"] = function() { abort("'removeRunDependency' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; -if (!Module["ENV"]) Module["ENV"] = function() { abort("'ENV' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["FS"]) Module["FS"] = function() { abort("'FS' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["FS_createFolder"]) Module["FS_createFolder"] = function() { abort("'FS_createFolder' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; -if (!Module["FS_createPath"]) Module["FS_createPath"] = function() { abort("'FS_createPath' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; -if (!Module["FS_createDataFile"]) Module["FS_createDataFile"] = function() { abort("'FS_createDataFile' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; -if (!Module["FS_createPreloadedFile"]) Module["FS_createPreloadedFile"] = function() { abort("'FS_createPreloadedFile' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; -if (!Module["FS_createLazyFile"]) Module["FS_createLazyFile"] = function() { abort("'FS_createLazyFile' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; -if (!Module["FS_createLink"]) Module["FS_createLink"] = function() { abort("'FS_createLink' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; -if (!Module["FS_createDevice"]) Module["FS_createDevice"] = function() { abort("'FS_createDevice' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; -if (!Module["FS_unlink"]) Module["FS_unlink"] = function() { abort("'FS_unlink' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") }; -if (!Module["GL"]) Module["GL"] = function() { abort("'GL' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["dynamicAlloc"]) Module["dynamicAlloc"] = function() { abort("'dynamicAlloc' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["warnOnce"]) Module["warnOnce"] = function() { abort("'warnOnce' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["loadDynamicLibrary"]) Module["loadDynamicLibrary"] = function() { abort("'loadDynamicLibrary' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["loadWebAssemblyModule"]) Module["loadWebAssemblyModule"] = function() { abort("'loadWebAssemblyModule' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["getLEB"]) Module["getLEB"] = function() { abort("'getLEB' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["getFunctionTables"]) Module["getFunctionTables"] = function() { abort("'getFunctionTables' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["alignFunctionTables"]) Module["alignFunctionTables"] = function() { abort("'alignFunctionTables' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["registerFunctions"]) Module["registerFunctions"] = function() { abort("'registerFunctions' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["addFunction"]) Module["addFunction"] = function() { abort("'addFunction' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["removeFunction"]) Module["removeFunction"] = function() { abort("'removeFunction' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["getFuncWrapper"]) Module["getFuncWrapper"] = function() { abort("'getFuncWrapper' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["prettyPrint"]) Module["prettyPrint"] = function() { abort("'prettyPrint' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["makeBigInt"]) Module["makeBigInt"] = function() { abort("'makeBigInt' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["dynCall"]) Module["dynCall"] = function() { abort("'dynCall' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["getCompilerSetting"]) Module["getCompilerSetting"] = function() { abort("'getCompilerSetting' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["stackSave"]) Module["stackSave"] = function() { abort("'stackSave' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["stackRestore"]) Module["stackRestore"] = function() { abort("'stackRestore' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["stackAlloc"]) Module["stackAlloc"] = function() { abort("'stackAlloc' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["establishStackSpace"]) Module["establishStackSpace"] = function() { abort("'establishStackSpace' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["print"]) Module["print"] = function() { abort("'print' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["printErr"]) Module["printErr"] = function() { abort("'printErr' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["getTempRet0"]) Module["getTempRet0"] = function() { abort("'getTempRet0' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["setTempRet0"]) Module["setTempRet0"] = function() { abort("'setTempRet0' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") }; -if (!Module["Pointer_stringify"]) Module["Pointer_stringify"] = function() { abort("'Pointer_stringify' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") };if (!Module["ALLOC_NORMAL"]) Object.defineProperty(Module, "ALLOC_NORMAL", { get: function() { abort("'ALLOC_NORMAL' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") } }); -if (!Module["ALLOC_STACK"]) Object.defineProperty(Module, "ALLOC_STACK", { get: function() { abort("'ALLOC_STACK' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") } }); -if (!Module["ALLOC_DYNAMIC"]) Object.defineProperty(Module, "ALLOC_DYNAMIC", { get: function() { abort("'ALLOC_DYNAMIC' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") } }); -if (!Module["ALLOC_NONE"]) Object.defineProperty(Module, "ALLOC_NONE", { get: function() { abort("'ALLOC_NONE' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") } }); - - - - -/** - * @constructor - * @extends {Error} - * @this {ExitStatus} - */ -function ExitStatus(status) { - this.name = "ExitStatus"; - this.message = "Program terminated with exit(" + status + ")"; - this.status = status; -}; -ExitStatus.prototype = new Error(); -ExitStatus.prototype.constructor = ExitStatus; - -var calledMain = false; - -dependenciesFulfilled = function runCaller() { - // If run has never been called, and we should call run (INVOKE_RUN is true, and Module.noInitialRun is not false) - if (!Module['calledRun']) run(); - if (!Module['calledRun']) dependenciesFulfilled = runCaller; // try this again later, after new deps are fulfilled -} - - - - - -/** @type {function(Array=)} */ -function run(args) { - args = args || Module['arguments']; - - if (runDependencies > 0) { - return; - } - - writeStackCookie(); - - preRun(); - - if (runDependencies > 0) return; // a preRun added a dependency, run will be called later - if (Module['calledRun']) return; // run may have just been called through dependencies being fulfilled just in this very frame - - function doRun() { - if (Module['calledRun']) return; // run may have just been called while the async setStatus time below was happening - Module['calledRun'] = true; - - if (ABORT) return; - - ensureInitRuntime(); - - preMain(); - - if (Module['onRuntimeInitialized']) Module['onRuntimeInitialized'](); - - assert(!Module['_main'], 'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]'); - - postRun(); - } - - if (Module['setStatus']) { - Module['setStatus']('Running...'); - setTimeout(function() { - setTimeout(function() { - Module['setStatus'](''); - }, 1); - doRun(); - }, 1); - } else { - doRun(); - } - checkStackCookie(); -} -Module['run'] = run; - -function checkUnflushedContent() { - // Compiler settings do not allow exiting the runtime, so flushing - // the streams is not possible. but in ASSERTIONS mode we check - // if there was something to flush, and if so tell the user they - // should request that the runtime be exitable. - // Normally we would not even include flush() at all, but in ASSERTIONS - // builds we do so just for this check, and here we see if there is any - // content to flush, that is, we check if there would have been - // something a non-ASSERTIONS build would have not seen. - // How we flush the streams depends on whether we are in SYSCALLS_REQUIRE_FILESYSTEM=0 - // mode (which has its own special function for this; otherwise, all - // the code is inside libc) - var print = out; - var printErr = err; - var has = false; - out = err = function(x) { - has = true; - } - try { // it doesn't matter if it fails - var flush = flush_NO_FILESYSTEM; - if (flush) flush(0); - } catch(e) {} - out = print; - err = printErr; - if (has) { - warnOnce('stdio streams had content in them that was not flushed. you should set EXIT_RUNTIME to 1 (see the FAQ), or make sure to emit a newline when you printf etc.'); - warnOnce('(this may also be due to not including full filesystem support - try building with -s FORCE_FILESYSTEM=1)'); - } -} - -function exit(status, implicit) { - checkUnflushedContent(); - - // if this is just main exit-ing implicitly, and the status is 0, then we - // don't need to do anything here and can just leave. if the status is - // non-zero, though, then we need to report it. - // (we may have warned about this earlier, if a situation justifies doing so) - if (implicit && Module['noExitRuntime'] && status === 0) { - return; - } - - if (Module['noExitRuntime']) { - // if exit() was called, we may warn the user if the runtime isn't actually being shut down - if (!implicit) { - err('exit(' + status + ') called, but EXIT_RUNTIME is not set, so halting execution but not exiting the runtime or preventing further async execution (build with EXIT_RUNTIME=1, if you want a true shutdown)'); - } - } else { - - ABORT = true; - EXITSTATUS = status; - - exitRuntime(); - - if (Module['onExit']) Module['onExit'](status); - } - - Module['quit'](status, new ExitStatus(status)); -} - -var abortDecorators = []; - -function abort(what) { - if (Module['onAbort']) { - Module['onAbort'](what); - } - - if (what !== undefined) { - out(what); - err(what); - what = JSON.stringify(what) - } else { - what = ''; - } - - ABORT = true; - EXITSTATUS = 1; - - var extra = ''; - var output = 'abort(' + what + ') at ' + stackTrace() + extra; - if (abortDecorators) { - abortDecorators.forEach(function(decorator) { - output = decorator(output, what); - }); - } - throw output; -} -Module['abort'] = abort; - -if (Module['preInit']) { - if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; - while (Module['preInit'].length > 0) { - Module['preInit'].pop()(); - } -} - - - Module["noExitRuntime"] = true; - -run(); - - - - - -// {{MODULE_ADDITIONS}} - - - diff --git a/htdocs/libmbe.wasm b/htdocs/libmbe.wasm deleted file mode 100644 index 5903c97845d357f2b62280fcff4f5865935df742..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31678 zcmbt+37A#IweH#Tfz#3rBH+khfQoU#M5EUs_C{<$;*iALB=6>3P=kq$Q#Wm2Gu&`M zKtx1T5Cjzj6i`q^96@aroClm|#d)4_p5Fhjy?1x(P40c)d#9_qR;{&a)v8sihCS3S z9XaNBuT&~|2UJD|W5V;y<-q3VLr=`*2=nJLM<03Ial7q4s1&NUO*-R#n&E0X zaC_{Yp(-7yoT&>jC!f$!-R2`k9Y5yi=3`H48Fj*lKaOe{@uyKoo-|_2v40s=id0dr z&h+C)9e?yme;#qvv41R;MC{#}*gQ(5tbEBgs+sDr6An1?gcD9|IjH%>|2SmS@h3L_ zIiptC$RkD_f6^~UwG3)+Co3Ssh@*}iGb$KsRj^SlM;>+DsLr@eT3Bw#i6;|%cuRB6 z*loz~4owdkcEAyb9X{glBYv4<<4mIORsE_d6`3^eB(Gbyz`>7#*bhQK@JpT_`P3{* ze7{uk!*Um*y2iijHrDI!SNJdBzqIr}{QH|Xgbrsvajt*C3ao#j&9D5YcYyHiX|mp{ z?as~^)!cl-iR%9&TaG>Pgwoc@h+~d9?&L9lDh-H69Me2%RH;ug;&^ubiAR@u_a1Tl zQKLqTI{N68=}&$-XwWG?-hI%Z0|u3PlE?a{w4Fh;ZE5>x#F(R+N0$b6{llNx2ge;J z$K0W&Bg5>E+Oy`4yKXvajO=slm_M0^|LUY8j~-Rpv@4NEk4{a8x*E&;l8h%dk1F-( zV%c9E**dD!y-QE8)ZeRg+iCNicka9M7CY~}^DevWvSruTuJAC>^Tzh?=DhyhP-+~| zt$(+)wlO`aNl^dVL&JKM>Pi)OwUwyUzgul3E}=A_TcxWd@CMXYzVW=ilF*k$*JmQE zATui2H$|iCgT8KalpOoX)dcl^J!;$**dpyc)NP3^4-D&TJl5g1B6@3VoqCPV$5!wg zU4USxQptrRMCgO4(Zz@n?p-PolOPdm8D&>P0TnWAUf0WYK^ND%Q$=0TRTWiG$t$XH zn_zQn(&)OOo21vW>N?8nj_#7z19j-p=r+ZssLd+siJl^ChRyPddb?ieJD?kS)mA?8 zz5Z%`e>T6)44AdPPK{@atyx)#mC?n2ouuWPs z)b&Mg2lQ>%ni;qzn#NB{2gx*Hdx}@kiSpQljAm1$-_iw`^ z>b20Yd%;&Fh74$?5dCP+F!ww8ihQn5OWNmTx?O;BBQ3cXx~Nb;Su-8mC`WVs%yxU# zZI*V_y$Lo+D~HPII@eu`+O%5NL^IaOWb(o9ye3+(H$AYQ>q#%{X|9JJaz;~)7L&ex zSaqMQ>dmpas^*x=s!qGMH<_kWlefZF?T)lH1#g3`u}vd=tgo5{R_BQA4~PDZZU6=V zs;-~g7TZelcGxbLTyi^iYSi}FUX2=vfw}bFZU>h75q7|j8r_cAQPOw9PPx%`p;31w zeGqoVphmYFb`$-__;F7E33kCx4syF=cg{RJyFIXnT=J**soS~2VtZmwDf2J*m)ycT zyS=eL2jbqfl?^9(ca`N8ca{IqP`s<`eveZ6i8svM>-qJ#*VEjVR))D7eWi_VX@I3c zdPpK!X%S9~(xBCS8l@XE?|n4}!>3WJdo;nLNg6b{|Be5hmKxlj z@FyBVICkO~j7hyQ?(g_Jc^lp79!~evaWlO1f`?0Bq5(V4!#ppgD_!Q{GEe?^jfZOt z)8?VgF!y<2nCCUPCp&f2ao}tTxc!pfBiAJc^eHh|H003{#b;^46`A^2E)7^ zf#H^@!F?9tv&h_IDNQ;)#`sw7ai%%!nK7L^Husnv%NJ(H?y*E=M6xy{*pSfU4BzbDPOR~7Cu$?h(W-|s<;;Dk`)!nCK27jxA{QT5 z#<((RpC<0N75oG1XI2>uT_uP86$lXU`q@4Xo94S{n3L|k; zt2-J;%gg_OKgh>NVHA#PcE{jojB0g%#2@9`$KqIdb|a2KW3wBLKccbK9f#xO(Z}O> z`SS@FjT4&Pi8u}?w7QdUl4wn6k{37QL^LUmq4inf(q*_L(sC%yX&4-Cn0H`T*b zX~~U2qdU{XnO5mE(&Pqbc{s}u(>*XG4eo4@%g@ zi%igb5A#(|KUw}_4;LGGfrkY~Ug%*V$ujQ(k2`~lJ+~d}xJdAYg3+IKZ0EK$qikzi zEw0h~(sCMD=Xf~B1fJ{RTw8@^dSIAttI%{?-_G;EaBhxq`*III&FWqx8!hs*Iwa{< zjqVZ;mzc0iJ##cB8v2VpEH?BKS{6&VJTCRHRBa+fut(9+!K# z+}6xxp2pKMy7Y1{<@m#LDN+-#k<@}V;pNd4CjLqf4D7oVTn;^1^C}N3o~_OtXe261~d9DieLJhik3L*Lk?k8oaOQ*Lz^N)(VhL z5jS{XP@^ie301h!!;L0lwTIOvVvUD2xd_s4^1!g#M37DqH+x_(5mc<4)(vyFc(_IO zDw|K=>fu&y3Yy(*Y^?_G?H(9z%k(C#(XI8c)`r(Q59_S@J3QRMZB4VgQyAPS=4{K< z?C#3t-0gwkE|XIh?>!nls^NYQ4EGp?|=O!~7W!3>jj*m3o$1dfD{^5oCGJ1H&UK zlPw5pqOAvK7%m$&&wF^j(=JMebX_WRNlPPcRc{$P+`Zu81vVoc%f%tvy>9TZ!6vyE z&6hQczU<*;YxFA~UQwf8^s=?&MQ^lw)%eS;?llju8Q1F`UYAT>sYYZ?zx(bx&Temb zcthB}!jiH2?f#AKO%HE!gtWT1JiNu$Ho3PY1#i>n?|68JY#aJEy7xT1XKe3#c%Qx9 z{LsYz)5P`t_}cZ%J!%-+5s8nnJ5Scsagx zrDC(n*5*Fre2lX#?sy;L%>gI)oQ)=M=Sth~`qBLiuX?UN-Ot@wj(EGGZ-blY>$XL! zo8+gPW0F7Go#tbrt|2rM{ED0GONq&Ti#weXd1XogTT(hN%TD**HblW@>a8>+rudj* z3Y_8N3||Y^R38lLwljS&Y&p!G<>M^vrOz}Cr~8<$+*WP~WesIIJiWC<{c56!nO(`| z2GNOn6Wo#zw$co8wvV%|qH}!Sap>FX&gE?v&OSJoTX7Dg89rtheWs6dte*1(V5Z;T zX8B-dmf!5IE#ulUr$R)eRd`J|(^zLqCOc_u8MCF@ZoK&3m8Kh%TdsyP=dQHGAR1z( z46(M%i_+a3Dq2rCKiyeQ_iJE5TD-x{@$oaIbA1q;OV3zU#wtzfw0w)ZfK}oG+WkVG z2`;2s7x8+*=KyGO^LXiFD$VyXU#e&v)Wj7Qna3g*i;RmYa)FNpzIIj%eJtd#ZgrRV zxWw1)>QZ5FDR)nceJu91#aiNHi7($@>Vx4@)_Iv|%(S}8eOyi>wz_3LmhsAoYm5() zmy_!X(FnD=6+Tw*BC6G0<>M-{H@THQNM1>Q?z!Wn9wPeDJulX7@XecyKMY>X0Gs01R~pVYoZkoc3Tj?JYiTv1;0Uw3*Z1>VrW}dz%l2 zgNC`=ecaAb!CZucD(=>DmOVJa0Tp+983$|WTkB)3Nm}P)o%P$DKJJuy2g_;i@^P1u z@Ah%G@9w3jbxJC@*O>3|agUg9FS9YXmR*A!jhO?KIfzW|;LO7?SWUmr$9;uQM8zfR z{XQ6^`U5^1@PI$eJ?P^>@eL-r(XIEvFnG9o*vG?$*82L0k4H?kM@iF^|Co=*4Dq-R zhQYji*HrRER)URr&c}1c@Vt-bP5l>qypWM;iVZ$C82LqNW8{~7yhJj4GS%kqMW6Rd zHu!E&j_v0KKPMQw^OnkaG-~EJS_fyvKir|+8<0q`+r*041{Oyr#{(kJzhz;kG`J|60P2f{pEp35( z+6TkqcAMaF4jV3+J{X?LF)pQxJ!*BoB^$jZE5MXMq%#7XVaiMmFg4JI?@VEErkHJaK{?ZMIcEi6 zm}YVs#5+CU=#rST129ZC3ddUmuPOpCG)Vmh-4mF>Oj_mmoEc!Ibe+N4THJYo-63Ei zADI;tH|}PWN|qLPet_8l&gb2uIRWOFx#k9#D+z{gb{AM7Tws+`0zLLum=!pOU%7vc z2nlmxfD5gi^8(Bh?ZUwQYX$#G{iPxYX``DTfFVP$SuAyN00t)xBFM5J0K+U*Rl|v_ zA?3boVSt6E*P;N6s8?EYeIt^xZP_IOE)lY?kmR#%*`)z4@k0xUDOjG`dt`9)$^>mCI0^DF7bE5&=maS&TVKsYjO@K8d zHMyGtFx*I)HwU;maDDS#SzqhJTg)do58N7np>J+Hj*Z&_+-6R9dw|<%F;R%Hrz;-% zat9LNjsR=f*XshTGn?KS;7;|m;dz-7*xdOZ-}0mD2P;6b~?yq?T#e}j8SW9%Vma)019kNC8Vo5_dSJZ%ad z3AmYjgrts2(W=wa^^-K4VEX3z~S~(fTxW0nE=n2qR$3kprX$Ocus0QO*L!62v4eB zX11`klr?nNgEo690E0cS$sGLU01TZ?_DTST%w)Pu-dqFY)c~)W0`mheUZhSxfE%$ori^z4X?n%mwA@K)vtticwhw*$N#xb~aWnu>UNl_%PEw%@jX zHvmJIVeY*E?*+OOq3me)eo$P8eh`3}+TrfQ03XUYM09C%9~lEal49CWkVzVml~dct z0STJgHU?n$*hnn*E(ggc0T?pwE}CdR4JcB~p9f(0RLr(V$()N8Z&uHKd4iYQ{Pt1Ou%xF%$|DMI?)CFezkK%{nav!z3eFgD0z$D5r;D zm~0ecsMC}XQ^JmptEBfmW_O3r2ysTJ)oyBtsnjWb^s!P}g;ZJJ4n0X|tWeJuzy<;~ zL#)UF{N7CqF)ie?Z+BLRv%>Vr%O1Nd8snyistVJsbyc_-VWc{=(*6>^WVV7X=^(CE zY;L+MSEvv(LY%`!oJ+k3k9IRdoEu^$C))EuFwlatLd*(vPovbiP0KR5b3)9qHq8w& z*SHpCgbPA2oJZLghG4iLJj7jO(426%n-_wad3*?QO&Ql{x6@g8C1q2k_52X??a{`? zAuhIR7KEII7RY#c?=1|m&?uLPa)~HaPglF?s>ez$vbn|{JX{)rVIeDD9AdFlg*^>e z5@JcH%9a|#Qfhoz2nMlU9^!I}r^%WWZUvp%MPo9KGChahw-yL4>`X4ca6t}o;I z%=PxFiP*vG%Cx~9AsANB%6EpiQ+8cfrbTIlc8f;jph1cMp+R$tPR=Zfy+^x8Li|fQ z_8|9Yh`kH#9}B_oD7C+-435RUhr7o^FxElpbPcsPHTjoz zMWPDt&gJ9#35{b__+$u%Cuq8-)Co_};7^BmD#X*Q@7WN~w)@|6A)Yf6eP1dkiD%`B z&xd&4oZ*EKFUSs$5W9t3LRVUpv^OvMLTr$~b?6jQr}p9dQizv0e4E|Np++=bwn=g? zcVK!}HbhGWNbA!R&ywBf3WcFw3Gs?*K2Q3*%H}Y6Jp>_I^obc^>xyic!WHo}1wcSjjF^X~5$ zcZ+1ID?Si|Fe}R-yt3TvJ_^Bb3!xj!xY2s;4mk^}%AJLSObt8R93pq_k3%s0oa<^R zf5%3fu9sxv>ywaqd_sjj4e_bEo`j60QcQWDh4_rdXmFov%KKbQ6SAf3ix6KJ<;xI< z5FF;d3PJcQ_Q7>!TqkcQD-PiTQ_%!zPR_`$gHp%JHYBAmKFAEVo4D=nOJ58PUrxq z6J%D{sU6^Kg3Jm#qXV2zkXd0b=m3`xWLDTk9pDOr%nG}#1H7Ifv%;?G0NV&ME9{yM z@GgSP3cI!gTu+c$VejhzpCrhvu#a|t8wfHh?6V!<8w8mZ_LUCs1A@#7`%VY=IYDNH z-Pi$EAGNHoUlU7;z{$iiD{w*wIGrG~!cOe~XA@*r*clz*e1gmhdqF#hpQpd&Z3>PM z+dzi+E_Brh)hOlT_;C@&ae3q&3H~RJTgC{d8fgNb-A38EH6g0Fi4lj`L_S@e6oJAf zlJ)e+HjtC_I)d#pPq!Bnh~`#uN`y(a;W#4#!xSSO!lHacA7QGI&WvzolnzvJZvV92 zPK(m%9D&osoo#9AY)exo;#m>j#A5mB5vJR&;p_;+olT|Ak#^@q4Q@t+84-s)AG>ll z!0V_wwR&cRnO5Sw2JyvWsM8yHSqb8JqwYG$iCUg(@3;e0DRC&C== zikjS9KHD`Z7eu&#Ql!OaTy-y$7N2o4n-_uM!l=ouE@L&PAu6dUa6TUdN0?t2OpUrY z0t0CaBtyzBe(tRskKA1#k?xpl-95g3*mi6RzI#FY`QG};PoiN4=SS7n`amDp=S zU6@VC=Hr!W|H^2XyP9uos27OOCST&&#A~uH9cW!TP_4f<0>d@zxa%TZry>iAjvFXj zTp!_jv;7SbZZIp{7~#ge<8FxD!1j(?U>!G59kx2cYAduR!W!$aneHgArO!lQc)IB3M`-qE zBd)2>%JBJ>eRR7rPDgC#@jSbe$J?qrqgp$Sd>+FNhQsm)|c%V37Ei zBfKo(`3q;S@IG}W{8hVJ#;Ycrm&=q!3Cv6QYlZOFBQU&XBsS|+YV`(hdkd29@rKSL zro@{O-n1@!E5ciR`>Dyj6X6}oYH;sHcvn46G#}T!m(g3@`v&Ot1> z0nB85q$bFpK92CQVKzo!_=xTLB*G_Z7oVQ;Wv-kh^%w2$A$}Qw;S0_HUl|Tx zWg4rvWPTliVWYKdqt*LO1cs0DhLx;x-YEH&)2&thU7=1jMm5&p85e^=W)CUla1dUbkq8*7-yNF=`p68ptEC~E%`g>=J+`=&N1@2G0rvej2JUW z)}H#@m@lKA6Z8F1&gX*95{w_Qj-B03_Vi+>>@Law~2+mc~??B}l(42E!7w7U`66c?QFFWj%W)EH)6jCIzSJ7U}++Pc{Nx`JQlQ{HBB z;yYt7WOE?rRm!<52E%6ZG$P1ycMOJ`Rh1^b?s^h&PmFs^uY2X5_fj(s;9fL>E-91Y z?!FlJ3E9{6QW^5v(}??H+|QT#THOON9-xj*ZoLFyJsbT{jEBgkrx6dwc-Yt;p;OWi zn%tu?7}m$l?lG}3)8ZbF@i+_UX~YvTp0EN>vMn@glY5FUp~iTM1)hodG~$_fw0l#oW2m_1?!v*2JmUbi|jnS7`M8YV!T9BlY2P^!v@NHCB`eU z>yji_8T!6dgTVBqrVyBP4nirV!TDGi9&>Jm-CVOwz|F--;{~*c8qt} z^Y8N2ZL{!ubY1nl;hWw2*+_ohdfw{HE28Ir5QE|M+#}U@AH`tk+`S*iU@(k&=5=$B zjWITwhM&av#4w-oUG`WD*Jm*px;MDb)zhC#lZ~<4G~#)Kz76gRHdN0gzKr=?;!7I& zs~BH!+R#fbUvbv>Mn?RGCjU0Zw=s_}uxwA8#e1ZDE-^7dPfi7xn7y_;DZ!-dHQNN^ z>}B1_2`2MpUG1lPXy|;$3D@32t0t%#>-YrY6DfLX0tPBNA;AQ_;5#mHo9aH{cdD0} zp4pa-m9*P~Hak55L(klZG})8{4DBo&5oaV|$V{f^5}Rt6OieJ=6gV@%nf6-ev;+)U zK~g(QI4c3enR@Y;cy-M51PnR{BLww0JHgp(G7+qSyCzmKzl`~1*ZyFF-lkivp_$L_ zS)QAKLHqt031;X8X&(6DtT{8mOw;W=V>plAJSzc%SZ61gEtBe2t8M$wPjJ3*&q**R zaiJ}Mvy$`|jc#s&x%TGf1qm)l+(ij4N_6!$FTp(PpZWYx+-P@kf{Tr`Ai?}3bASa& z#VwQsETjgDRK4c?OA-*igm-2yO>ilBiRT(a=EVsXTXjnfpj9qQva86;lk6&TS%PJW zTb^LKRdz*!D@>0orN?sWu_D0=BVCo?%3P1DD0(FYU?tU9m4L!llJ%OzHkYf^E4Jah z#`=e7&hpnLSY?iOT>^${jdTc$^3ou|^+viO!41jxt|D(t>?-m`<7QLX7poJjPSP#a z(MuC`;0}Ii&n#1JRGvMZq5ix3L~D5GXsJoPEO9p_<^ngF{51*Iu)&f~1ij{#1Prri z-nIm78VGAtBx1K(&(g@Bg$?f31h<;UEKb~QLb=-$5V)O|Su35jBCSiXF3~D@hcLLq zs#$9Wy3@Lo4ZJG>!<~7Pv9{ph`gc#ltSx}|Cb(Cs+{3ah?!N40eI{A}@3%MbRow%m zlBLBxnBai~57N`tCs=Pi{7{02B*E~_?&0jU{fDhKN?^ke$Nh=xhQr+vyv8xiBMBa{ zc08WoanT-0+z}NV!Pd%N-82xNNWhRGvdiEnId&5}*haKio=U)QzpCQ<8fp?(z^4;D zZHhgU;2A2$^{dSQ}!Sj^TEnBYaS zY5998!Ar*Wa)OuH$xZH+1Pm`E&F)pPG1KB+OYj;CX!(0R!RuDwjRbGds7>z81mt{^ z1>Q=y{Jq6aeMkI6wYYZ^yqjqGdoKa8@6o58V%?Ab$^P&00m3{Z)dm%&cE0_*(sK_-6M_Vvpm$vHrHIa`~JjzD>aJiIK7%`z`@P=T5Db!C)Bm z;V0%6Ed!wD6r<=E*<3RarLM z%_w7r)qhqQXW3bgnPtpmom%*~!EbSA8iA@#D`T3mPA_A+sd_daZe*&S!}BLn^emlz z<(zt^DrRPrY~-_+c9YO-=as>rQ=RH3`uS|Wmf4AgL+bo87&5DItB)FuvpHqVF%9OH zF}JLO7nH$}$tJbE5;9*{2E$x?bvvu&qB0n2I=XLO8S~g?B6LS5zZ-CI84P?LwXlqZ zYLjxK-6B3l<2Cytdrx|5cJt?wG8lOO;nK2A=$Be$w+ zqFh!6!&0LVL!B=#&(w*MFmZN3wLDSLr!ZJ^be8Cq8C-{0bn&Zsg$TIM2729Dc1nru6gd#V2yjG=RlKp^>)HMbbErsqTl^IT>nou=>DG zzDTKs9uf4hD~f{`Jn_J=kat+t>f8b6Se*jmnxP{Bx);K?`tviH_GUld@^5laWH0qU z;Yp?MH-_)AT`*Kcat8~sAqOZzhQ z3m!XBZ|0MMyC1$}|EisGculXzi_>1RSAXX3)srrC_Ch_;_CmcaFuir@{-(#1%&&T! zeR+>c@N0s3h_)+g^C_mQW?jC1|F#Lr-oB@xcRij4r5;_ShrNHVdhGpsRnM2|KO|Xq zNIvj5alG%jZmi=S!EXyjg>`guo7gsDlWaoo>Uu|P7<}_rHk&X0`&pjT3m5+Scej>t72k@LgllwY5@$>b6?eCCg zpq{iEPY!%zwc5b})yh)?B+=egzJ00VKi_x}4-i!Be0ql-<8*@H2MyHS6KY8}8n0so zlw+QzdQR2pTs3;44h^UwlYAZ`IFJ&0i_WtI3=?fUkxmhleK4rP3X5u3p6>G?fn?a> z0m-n#13yrL2LPmfcAQ{}37ldA&$MF&)`HHxqH`xxJ$BO%rydrzIO;_J=)2ECSlAuU2y61Zi#NH6D$Fi$Vvv&-`7-QLir zxCVEzUdbmRUm_Oh)qKtSd@o;%!a^x;D}c@A#BxsJsb!nxd8nNyn8m<*zA_4Uz6lL{ zJ=2-KLYqfx<}#pT&g_SNR)2r=XYKhN?kX6Hrz?6X2YF*o_1$n(T*HkyI>b7y{obuS%WZ0S)P^0 zt_?aXZyvr=3*W^4iQIdSVg}?fdF;{T_Ovsud$L}Q?p-BfFMcUtH|e#TLi4C$}7q;VS-fUInuFAvlBr z>ofok#i1e`hQmZS9EWEU*J1ox!lAs<=hGBI!wI1~W%lHq0DTQ)(`?eJbA7ClY_m!1?gWr(*i0qpTeWWT+ja(`x_VB9~_gnl{-SxX-Kdd*!f7j&x&Gx$grvA{> zvyU45d;DI4`Aq>6{O`olJ|pn&oN|62VMIlrbLdnLj{wsonqm5=GmpYi*hk$>%_)sX zf+g(__=9xfkzh5dk0{6B82mwue^4p+<9IgK4rwmj~r-fOp5sg_fD#EkEI9A1u zRWTkJ&Wh>w@QxY{CuF1HL>?4AL2MiiJ2vVRF?M9O7?e#MluZq;8O_;1Ga@gbcI3S_ zKKk@^DLRHT3dTqVzgMC&$2PuC22UO5f;pUc-Z=XNM;DglEh>(zQ)Q`R_H{&(&n`Eo=peH$H_xz>mAu@XW~ZH}vFT^& z403k4Ib8>E#dfyGGswE!JV)nrc{G(DB@i1k`ap#~PGOguGj(WJ1EymaqN(b*q zF7>cPCxiL@2z_KjroG(bq;@&&m{FSDvTRGR%=%hY%EQX?@Z~%x?D05qhTzn3rJV=P z8R!%%bQ;(S@ZOer!&M%S1#1+o)KOsh$JIIltgFpcI{J%V4elCgaE&y$%5%Jei*>Pi zE%nvK=5-z~Hm{>??HF)&v3b2$(W&2FJonq;R_kmpA84>_WOHbsYt7p{=y?oo%QjB8 z>-?^2zgb6r)$DaTq?=u92I`bGMxdfM>A0|1Z_$xpDca_Nfr{R$W5HDOW~!-6%{8i* znf#4RR?=asJ9HWt_GHMM`%aw*F6cDXUA&&k@^&1?Zc5&*)4{6x9-Rc%+k`hdn&3 zv&Gy1@V1-t&62i>@n8&(D5c(y>gcZQ#z*0v^X(#A((LW6+R9;`eS78~etM?tVqOsL z+vq4tt>g!s>b9W;W9X&plwY)}>*7L$bz8Xz`glrITbUbq^}5s?%x~EY>hy`DLHfke zV0}|-UnU#_`(QASO_nq~c%>-uyYr%OceQXE`QM(TafomSAexr`-A8k^-xGUhENmLT zTLd!+zdB`~FUn3W@2wAT)nuCU+eIqg4cqW*S^H%7%l46Y?GWtl&ptBJ&v;1F_FbG& z_NhzsQ6o00J8E>Rz6ZU%2dAxor^Hg94c0fkwxMmhyDiP^{7%tOeZna7&IntMb^|yV zSPMTwr0TcBme_8%+nz{PHW1r$IULAwQ{m=x`?POM+V^B`u|j=$sBFaT_&~P8?bepq zaWoCyMMC&7A_R?YXLP~NGC>zr(7xr`6~SokxOTxH(X7$Cp$m6i`(_)ceZ{>$_Q#J! z-&cCngU9EYOWEjs*=TOd*yugDW!$4dpDxOptZ`FjqX)3jUD)V7+?Lsmnv!~v%Xv0` z@mgPkvkzCnHm~|38k+f~rc!q5g&&-0Z~!Qm-=osBL6x|*Rpa`>N59dIn^%`;IZkqD_ZRICktN|t+t0RYNP|Bbv(NTB{h?26=Io5d*co)dNfwzfg z_kX0o{>a$^Or6!t!cXt|S zxEkv*&KK>YuFUq;W}~iFcGAbf&Rt?-wpPaGSHm)Ge~Q{Vc^S>nATwyoR!Z+J*?05O zv3v6~hZXt}ze`ZJh2tZstUrr}r2J}Am%LH6l}(GrzVTS8E=)gAlWRNUy4F_S^u6uX z?Cn_#YbRZYV6Tp3GObjtT+|P&YvH4v*f~^;65`And~q18nW02mzb2 zwTo?b;4us~qunIk(y<52C)mKS4zdWhWE>FOd)a+xnnW^tFvqEpU+`0IGxWe_!|C+B ztk|~n``+LqUi!U_B>6bs#%`eZZ!Xh?b<{0P_iEGyz698}_Zl=@NlhG|vqaETmTyNr^HGG|?N3tdA&ataHw`FEqj^J%6x}V9Ya{@Gu z*)fxSzc)XTN@HuVa!RKz60RAVEu<-w<97^EH(U-uDduea7M}>4GRHsqVbYZPjonM7 zEwb|ZxFKujuw*C6+c`mV18)Ddna<`9Ivu-5fg)nM>OKMr5GVpdP zl{hw7g!@%ym2ixt+%9lIx zHOkj?G<)utnH%BIe_|hFr%PqrwuFt&pLLghP^weU zunMoSJ?~2kzat+gA9Um?UP`ssQpuHyzo%5YxK16Pt;_U`4}CdWDf&OQ7$HP9FMA$TblX5&i?mlY1Y`G&Hjly*;P(SzLZ1sq8y_C=mR;#-p?WSdk$HTTl@Y|Pv=bPfNWCC znS$?G{=XOs{^;7uZ)x$miVOSO>_V%7PQ@XemJUV$Z)lSygQ2`*!1cb{7HLf#--zT8 z4iFw}xJ|-(&R{vZ(w|F+#w4fDI{w%AW`8wy*OhipA$*oBwa~j&%saNvazG)NA7kO*VQ6gT&diXdfaA|E4QV4$EDKN zL8-JVDV3gP`mR?h-ACLpgs&$2GU4G-sWdMvl}={-|M?C59|{jBf1kml4)$Zc(+T)_ z_Mas`lwahP7O`{Lx;(>XmVCCeDuBO9`d2P-=ra;m*R@ol2~37a|MVxOPQSuj{iwL~ z_b9XTZ*ZPbwW62v2A0XEyy{mvgbYd*JO8TGOFxwdV=qSi>WGl(R6eX>yM!b{HqcM} zHT);KV8wdYX(@FOzKE(q_(C6o|NQrZ_Rr5bH&aSMdGm5C^~|9*61osB=xYBE>YzMW zxzZx$MJy@G`{bWMj4K4NFV+5#LRG#>xQ_CQ_N!bGMXw=E?H5k@l<8#3D)Pc97UD_H zenoqg7LlOzSN(^xKt0ncBPaHZhl%JQZ6aZ+S{lTt`Xxg&#S-gN>XAX|bfSv-|3Fys z#ny+&D$}C8@X9MK;3BWI8d44kMSGR9D$5J*Nm%u%EXWHIt2sR^6kz@z{Z9P>lEt&1 z0QUbNRn+Mx``VB6dbPKZ&-{-_Xj21KmQm(4SZV$%%7;`I!qM0Db1YY70iW`EFkfX< z`Js&Zsaz2!llLg*MHjA~Wy;j0$SW=CH(2@nC)M;joHD?)&_7q%)a^o9Rni6%S@6#% z&ZDm4SN|(5qUzt2xDt8956TuWok=0TEAwEUlenc-!pbYEe3fY*mQ#7bipW#ZL(;}G zZ#H0~yqqriq8AYqAh=4p+U{$RcKWHjegpN7{X0Y{e}g5E`7SK4w8})OMJ85no~r#G z<<2Ls@Z%Ts&r1tx8+pIg;`$u6Es>?wA>EjuNNWLSNS4Z{}Qk9C%R%G zU+|GcX`-w$2}zJXf)$acf)zD>mHsg9KwRhc74kLqg)jPB^>vnC4l`L~`KQ0SqJwgd z`2{D-#+3KZ*1rEx6M!%JZ84L3~kO>z~SN{EuTCz-;IF zf1qTMF8(9(a)PZq@hNelE6Q!H-ke|A;x{-4rZNR%(LU8*reT$@&+&o_y2=%?RLSQ5 zkZloeTgIxdLRr43f4WeVp`YY~sj^DzS(j2p(G=I_u*w(wDqm$i8vopLT79CE-O5%8 z6#Nnn@+x1%AI|^tNz+gESN{#M9LF-#BGwZw(-~!d;q(hxkN8X7vUCtZ{i1It9!3dOImuW1MN^b;rO0tA zTeM%}zbEMorFur`KVHg3Sw1Fjp?^KWA{O=+zvzRR2v$`61-|q6ukVCc{nB6c7BQk6 z>5m)-c9!~EI8P^#<_a9?J)}&|YbophUcR6a-Npz;$yd5Lqf$lnzwkw@myRqens9-~ zEbAIdTpe*G;tG0|v=zdsF7k-K(1Bf>!A#WuU^=~!%W}#qj%T?)F)a}%F+DrUSO51R zPW6jEn8~e-Mg7J6Zv{ygPf>pXLnxM4`j4WgcCnmj(qCznQK_QJ7Uh4KieAq$!k4^K z>2xOQ|3bgR1@ou&`>baO;pya4T145uE-ycyaDPT?0|kj^BxzHGW4X@uS2u|+yzwh7 z;IIRhe6TadQBG05@PCyTy@+bR;3}j1-;+GzRIZ5D|HKt|@u!SG)bBQ;2C`1IT~TzU zWfo8hqEvr@SE^Xgq{=9Gi##pL3&*LuecV@byA@^IQ|ey8zZ96)Cw4Ga^HgxrUh&s6 zBbp-ev`Se;{}%QeN?y?e(p0V=6QzpXSxGg|YYzDDem`9QB~HI8JJ&Ju!{U>3231wh ze+B&{6=J>0*YKaxB8pV}`l%p)N+t+U zZy@6Y#zH^YzYhy3U!UUz7j&)vU`F{m_E|wwI_?MjooRyYNBOEx)hj)m@{}rC-;+Kv zhr5U==U4fI8Rrw{(awecv$&NNUF|m+7+lVOmwf~n6oqt5xZy^8It8hO4gfF7TU&?}$a~zYBPkV{q5mq`Q zPX&)7tnpXJK9dYcNdlr3^|uk#MH$v7`Bty$&hsiil3A4(y@;X@AX4=4#CPt0$roKT zMHxq_$|}|RKR;JMa*J4{o<8a7{{q)JmHwUcTL0^bQ2oM-uIg03*8d;!D_=zMtN#a) zuPFa>qEsG?(qHSp_=+g~tBmS@;Z#oiN(;EiE3Nml_KGI{LO=EY;mqqdkkMzpN|}Nc z)qbVp7`6V7WUBHZ4JW)d5)^IpdJ<~LQ`lehdRAW0B}1{uD=nhh&s)_dzjOHs#Hsxw zmFDALaM6Cj|2ONu>Q#S>zmPwKy0-g2>zDpTe;4gms@Ro^TDu7=t!Jz<7W^u!2=XbV zs_pWstVi_=uTD}T`_~2EuU|GO<^iQuW+i{HjFjg`5>)wW9;Lrv(d!A1Cs?$PRayH6 z5YTwaS?fHTCgH=*dtQ-BG%`4)i0dN7t!Ug$`}1FT2=Z8#{|-WKtP=8m;5&28AVcB z)Gt`{BC5RLGIbTL&_86s@yy#oO1Z46oi6@o|K0iSM=X>;>A1;BwpklZyZ7$0Ay8N>2{pKb9+gKj81GZ{!xV|JQWTfd2#Qfc&KZ From e63569e3e90d4e5cda7dac203acfc8d6cf042a25 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 13:52:19 +0200 Subject: [PATCH 0300/2616] packet decoding as secondary demodulator, finally displayin something on the webpage --- csdr.py | 24 +++++++++++++++--------- htdocs/css/openwebrx.css | 12 ++++++------ htdocs/index.html | 4 +--- htdocs/openwebrx.js | 40 ++++++++++++++++------------------------ 4 files changed, 38 insertions(+), 42 deletions(-) diff --git a/csdr.py b/csdr.py index ee007f251..249605f75 100644 --- a/csdr.py +++ b/csdr.py @@ -150,7 +150,13 @@ def chain(self, which): if which == "nfm": chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"] chain += last_decimation_block - chain += ["csdr deemphasis_nfm_ff {output_rate}", "csdr convert_f_s16"] + chain += ["csdr deemphasis_nfm_ff {output_rate}"] + if self.get_audio_rate() != self.get_output_rate(): + chain += [ + "sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + ] + else: + chain += ["csdr convert_f_s16"] elif self.isDigitalVoice(which): chain += ["csdr fmdemod_quadri_cf", "dc_block "] chain += last_decimation_block @@ -179,13 +185,6 @@ def chain(self, which): "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain}".format(max_gain=max_gain), "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ", ] - elif which == "packet": - chain += ["csdr fmdemod_quadri_cf"] - chain += last_decimation_block - chain += [ - "csdr convert_f_s16", - "direwolf -r {audio_rate} -t 0 - 1>&2" - ] elif which == "am": chain += ["csdr amdemod_cf", "csdr fastdcblock_ff"] chain += last_decimation_block @@ -195,7 +194,7 @@ def chain(self, which): chain += last_decimation_block chain += ["csdr agc_ff", "csdr limit_ff"] # fixed sample rate necessary for the wsjt-x tools. fix with sox... - if self.isWsjtMode() and self.get_audio_rate() != self.get_output_rate(): + if self.get_audio_rate() != self.get_output_rate(): chain += [ "sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " ] @@ -230,6 +229,12 @@ def secondary_chain(self, which): chain += "csdr fractional_decimator_ff {last_decimation} | " chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" return chain + elif which == "packet": + chain = secondary_chain_base + "csdr fmdemod_quadri_cf | " + if self.last_decimation != 1.0: + chain += "csdr fractional_decimator_ff {last_decimation} | " + chain += "csdr convert_f_s16 | direwolf -r {audio_rate} -t 0 -" + return chain def set_secondary_demodulator(self, what): if self.get_secondary_demodulator() == what: @@ -281,6 +286,7 @@ def start_secondary_demodulator(self): secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), if_samp_rate=self.if_samp_rate(), last_decimation=self.last_decimation, + audio_rate=self.get_audio_rate(), ) logger.debug("secondary command (demod) = %s", secondary_command_demod) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index cd1498ba0..5266bdebe 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -698,11 +698,6 @@ img.openwebrx-mirror-img transform-origin: 0% 50%; } -#openwebrx-digimode-content .part .subpart -{ -} - - @keyframes new-digimode-data { 0%{ opacity: 0; } @@ -862,7 +857,8 @@ img.openwebrx-mirror-img #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel, -#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel +#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel { display: none; } @@ -876,3 +872,7 @@ img.openwebrx-mirror-img height: 200px; margin: -10px; } + +#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container { + height: 250px; +} diff --git a/htdocs/index.html b/htdocs/index.html index 26296141a..328735cf9 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -74,9 +74,6 @@ -
    DIG
    @@ -88,6 +85,7 @@ +
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 400fa890c..081910571 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -444,11 +444,6 @@ function demodulator_default_analog(offset_frequency,subtype) { this.low_cut=-3250; this.high_cut=3250; - } - else if(subtype=="packet") - { - this.low_cut=-4000; - this.high_cut=4000; } else if(subtype=="am") { @@ -2748,6 +2743,11 @@ function demodulator_digital_replace(subtype) demodulator_analog_replace('usb', true); demodulator_buttons_update(); break; + case "packet": + secondary_demod_start(subtype); + demodulator_analog_replace('nfm', true); + demodulator_buttons_update(); + break; } $('#openwebrx-panel-digimodes').attr('data-mode', subtype); toggle_panel("openwebrx-panel-digimodes", true); @@ -2848,19 +2848,20 @@ function secondary_demod_push_binary_data(x) function secondary_demod_push_data(x) { - x=Array.from(x).map((y)=>{ + x=Array.from(x).filter((y) => { var c=y.charCodeAt(0); - if(y=="\r") return " "; - if(y=="\n") return " "; - //if(y=="\n") return "
    "; - if(c<32||c>126) return ""; + return (c == 10 || (c >= 32 && c <= 126)); + }).map((y) => { if(y=="&") return "&"; if(y=="<") return "<"; if(y==">") return ">"; if(y==" ") return " "; return y; + }).map((y) => { + if (y == "\n") return "
    "; + return ""+y+""; }).join(""); - $("#openwebrx-cursor-blink").before(""+x+""); + $("#openwebrx-cursor-blink").before(x); } function secondary_demod_data_clear() @@ -2915,19 +2916,10 @@ function secondary_demod_listbox_changed() { if (secondary_demod_listbox_updating) return; var sdm = $("#openwebrx-secondary-demod-listbox")[0].value; - switch (sdm) { - case "none": - demodulator_analog_replace_last(); - break; - case "bpsk31": - case "rtty": - case "ft8": - case "wspr": - case "jt65": - case "jt9": - case "ft4": - demodulator_digital_replace(sdm); - break; + if (sdm == "none") { + demodulator_analog_replace_last(); + } else { + demodulator_digital_replace(sdm); } update_dial_button(); } From f53b51a208ff72f2b2784487b009557e509ae658 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 16:36:53 +0200 Subject: [PATCH 0301/2616] fix sample rates --- csdr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/csdr.py b/csdr.py index 249605f75..673146c6b 100644 --- a/csdr.py +++ b/csdr.py @@ -150,7 +150,7 @@ def chain(self, which): if which == "nfm": chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"] chain += last_decimation_block - chain += ["csdr deemphasis_nfm_ff {output_rate}"] + chain += ["csdr deemphasis_nfm_ff {audio_rate}"] if self.get_audio_rate() != self.get_output_rate(): chain += [ "sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " @@ -433,7 +433,7 @@ def isWsjtMode(self, demodulator=None): def isPacket(self, demodulator = None): if demodulator is None: - demodulator = self.get_demodulator() + demodulator = self.get_secondary_demodulator() return demodulator == "packet" def set_output_rate(self, output_rate): From 2053e5f521f5250c6772c533e7346f03cff1e1e6 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 16:37:30 +0200 Subject: [PATCH 0302/2616] get raw packet data from KISS socket and start decoding --- csdr.py | 6 +++ owrx/connection.py | 3 ++ owrx/kiss.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++ owrx/source.py | 1 + 4 files changed, 107 insertions(+) create mode 100644 owrx/kiss.py diff --git a/csdr.py b/csdr.py index 673146c6b..921107502 100644 --- a/csdr.py +++ b/csdr.py @@ -25,6 +25,8 @@ import signal import threading from functools import partial + +from owrx.kiss import KissClient from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper import logging @@ -334,6 +336,10 @@ def start_secondary_demodulator(self): else: self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) + if self.isPacket(): + kiss = KissClient(8001) + self.output.send_output("packet_demod", kiss.read) + # open control pipes for csdr and send initialization data if self.secondary_shift_pipe != None: # TODO digimodes self.secondary_shift_pipe_file = open(self.secondary_shift_pipe, "w") # TODO digimodes diff --git a/owrx/connection.py b/owrx/connection.py index 5008e363f..7460afa9e 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -205,6 +205,9 @@ def write_wsjt_message(self, message): def write_dial_frequendies(self, frequencies): self.protected_send({"type": "dial_frequencies", "value": frequencies}) + def write_packet_data(self, data): + self.protected_send({"type": "packet_data", "value": data}) + class MapConnection(Client): def __init__(self, conn): diff --git a/owrx/kiss.py b/owrx/kiss.py new file mode 100644 index 000000000..5b6266deb --- /dev/null +++ b/owrx/kiss.py @@ -0,0 +1,97 @@ +import socket +import time +import logging + +logger = logging.getLogger(__name__) + +FEND = 0xC0 +FESC = 0xDB +TFEND = 0xDC +TFESC = 0XDD + +def group(a, *ns): + for n in ns: + a = [a[i:i+n] for i in range(0, len(a), n)] + return a + +def join(a, *cs): + return [cs[0].join(join(t, *cs[1:])) for t in a] if cs else a + +def hexdump(data): + toHex = lambda c: '{:02X}'.format(c) + toChr = lambda c: chr(c) if 32 <= c < 127 else '.' + make = lambda f, *cs: join(group(list(map(f, data)), 8, 2), *cs) + hs = make(toHex, ' ', ' ') + cs = make(toChr, ' ', '') + for i, (h, c) in enumerate(zip(hs, cs)): + print ('{:010X}: {:48} {:16}'.format(i * 16, h, c)) + + +class KissClient(object): + def __init__(self, port): + time.sleep(1) + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect(("localhost", port)) + + def read(self): + buf = bytes() + escaped = False + while True: + input = self.socket.recv(1) + # EOF + if len(input) == 0: + return bytes() + + if input[0] == FESC: + escaped = True + elif escaped: + if input[0] == TFEND: + buf += [FEND] + elif input[0] == TFESC: + buf += [FESC] + else: + logger.warning("invalid escape char: %s", str(input[0])) + escaped = False + elif input[0] == FEND: + logger.debug("decoded frame: " + str(buf)) + if len(buf) > 0: + return self.parseFrame(buf) + else: + buf += input + + def parseFrame(self, frame): + # data frames start with 0x00 + if frame[0] != 0x00: + return {} + ax25frame = frame[1:] + control_pid = ax25frame.find(bytes([0x03, 0xf0])) + if control_pid % 7 > 0: + logger.warning("aprs packet framing error: control/pid position not aligned with 7-octet callsign data") + + def chunks(l, n): + """Yield successive n-sized chunks from l.""" + for i in range(0, len(l), n): + yield l[i:i + n] + + information = ax25frame[control_pid+2:] + self.parseAprsData(information) + + data = { + "destination": self.extractCallsign(ax25frame[0:7]), + "source": self.extractCallsign(ax25frame[7:14]), + "path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)] + } + logger.debug(data) + return data + + def parseAprsData(self, data): + hexdump(data) + return {} + + def extractCallsign(self, input): + cs = bytes([b >> 1 for b in input[0:6]]).decode().strip() + ssid = (input[6] & 0b00011110) >> 1 + if ssid > 0: + return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid) + else: + return cs diff --git a/owrx/source.py b/owrx/source.py index 8f5dc6534..fe45ccc5c 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -502,6 +502,7 @@ def receive_output(self, t, read_fn): "secondary_demod": self.handler.write_secondary_demod, "meta": self.metaParser.parse, "wsjt_demod": self.wsjtParser.parse, + "packet_demod": self.handler.write_packet_data, } write = writers[t] From cbb65e8d793aa1928f6786b8351e2966d7371cea Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 17:18:02 +0200 Subject: [PATCH 0303/2616] decode basic aprs frames --- owrx/kiss.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/owrx/kiss.py b/owrx/kiss.py index 5b6266deb..9705a7926 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -55,7 +55,11 @@ def read(self): elif input[0] == FEND: logger.debug("decoded frame: " + str(buf)) if len(buf) > 0: - return self.parseFrame(buf) + try: + return self.parseFrame(buf) + except Exception: + logger.exception("failed to decode packet data") + return {} else: buf += input @@ -74,18 +78,45 @@ def chunks(l, n): yield l[i:i + n] information = ax25frame[control_pid+2:] - self.parseAprsData(information) + aprsData = self.parseAprsData(information) data = { "destination": self.extractCallsign(ax25frame[0:7]), "source": self.extractCallsign(ax25frame[7:14]), "path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)] } + data.update(aprsData) logger.debug(data) return data def parseAprsData(self, data): hexdump(data) + data = data.decode() + + def parseCoordinates(raw): + return { + "lat": int(raw[0:2]) + float(raw[2:7]) / 60, + "lon": int(raw[9:12]) + float(raw[12:17]) / 60 + } + + if data[0] == "!": + # fixed + coords = parseCoordinates(data[1:19]) + coords["comment"] = data[20:] + return coords + elif data[0] == "/": + # APRS TNC + coords = parseCoordinates(data[8:26]) + coords["comment"] = data[27:] + return coords + elif data[0] == "@": + # TODO CSE, SPD, BRG, 90Q, comments + if data[26] == "$": + # MOBILE + return parseCoordinates(data[8:26]) + elif data[26] == "\\": + # DF + return parseCoordinates(data[8:26]) return {} def extractCallsign(self, input): From 55c8ce7cf0870321db1df6bc444816a608fa6832 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 17:39:41 +0200 Subject: [PATCH 0304/2616] send decodes to map --- owrx/kiss.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/owrx/kiss.py b/owrx/kiss.py index 9705a7926..010a64fae 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -1,5 +1,6 @@ import socket import time +from owrx.map import Map, LatLngLocation import logging logger = logging.getLogger(__name__) @@ -78,6 +79,7 @@ def chunks(l, n): yield l[i:i + n] information = ax25frame[control_pid+2:] + # TODO how can we tell if this is an APRS frame at all? aprsData = self.parseAprsData(information) data = { @@ -86,12 +88,19 @@ def chunks(l, n): "path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)] } data.update(aprsData) + logger.debug(data) + if "lat" in data and "lon" in data: + Map.getSharedInstance().updateLocation(data["source"], LatLngLocation(data["lat"], data["lon"]), "APRS") return data def parseAprsData(self, data): - hexdump(data) + #hexdump(data) + + # TODO detect MIC-E frame and decode accordingly + data = data.decode() + logger.debug(data) def parseCoordinates(raw): return { From fe84a390972dd241ba929b080bdf7982c2dbf85b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 18:12:50 +0200 Subject: [PATCH 0305/2616] add aprs frequency --- bands.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bands.json b/bands.json index 30a26bbdf..bddfbef9b 100644 --- a/bands.json +++ b/bands.json @@ -153,7 +153,8 @@ "wspr": 144489000, "ft8": 144174000, "ft4": 144170000, - "jt65": 144120000 + "jt65": 144120000, + "packet": 144800000 } }, { From e5dffc3d9f79164eddaaf43e6310969bb6a8fa6b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 18:13:12 +0200 Subject: [PATCH 0306/2616] better decoding --- owrx/kiss.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/owrx/kiss.py b/owrx/kiss.py index 010a64fae..d153d5bce 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -103,6 +103,7 @@ def parseAprsData(self, data): logger.debug(data) def parseCoordinates(raw): + # TODO parse N/S and E/W return { "lat": int(raw[0:2]) + float(raw[2:7]) / 60, "lon": int(raw[9:12]) + float(raw[12:17]) / 60 @@ -119,13 +120,15 @@ def parseCoordinates(raw): coords["comment"] = data[27:] return coords elif data[0] == "@": + # MOBILE # TODO CSE, SPD, BRG, 90Q, comments - if data[26] == "$": - # MOBILE - return parseCoordinates(data[8:26]) - elif data[26] == "\\": + if data[26] == "\\": # DF return parseCoordinates(data[8:26]) + coords = parseCoordinates(data[8:26]) + coords["symbol"] = data[26] + coords["comment"] = data[27:] + return coords return {} def extractCallsign(self, input): From 12c92928fa1eb801274100983abbbd9142bedfad Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 18:42:41 +0200 Subject: [PATCH 0307/2616] pass through comments for display on the map --- htdocs/map.js | 8 +++++++- owrx/kiss.py | 3 ++- owrx/map.py | 7 +++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/htdocs/map.js b/htdocs/map.js index 7d98f4907..d235e07b9 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -115,6 +115,7 @@ marker.lastseen = update.lastseen; marker.mode = update.mode; marker.band = update.band; + marker.comment = update.location.comment; // TODO the trim should happen on the server side if (expectedCallsign && expectedCallsign == update.callsign.trim()) { @@ -283,9 +284,14 @@ if (!infowindow) infowindow = new google.maps.InfoWindow(); var marker = markers[callsign]; var timestring = moment(marker.lastseen).fromNow(); + var commentString = ""; + if (marker.comment) { + commentString = '
    ' + marker.comment + '
    '; + } infowindow.setContent( '

    ' + callsign + '

    ' + - '
    ' + timestring + ' using ' + marker.mode + '
    ' + '
    ' + timestring + ' using ' + marker.mode + '
    ' + + commentString ); infowindow.open(map, marker); } diff --git a/owrx/kiss.py b/owrx/kiss.py index d153d5bce..bc3300be7 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -91,7 +91,8 @@ def chunks(l, n): logger.debug(data) if "lat" in data and "lon" in data: - Map.getSharedInstance().updateLocation(data["source"], LatLngLocation(data["lat"], data["lon"]), "APRS") + loc = LatLngLocation(data["lat"], data["lon"], data["comment"] if "comment" in data else None) + Map.getSharedInstance().updateLocation(data["source"], loc, "APRS") return data def parseAprsData(self, data): diff --git a/owrx/map.py b/owrx/map.py index 9908d7afd..a07fd0d1b 100644 --- a/owrx/map.py +++ b/owrx/map.py @@ -92,12 +92,15 @@ def removeOldPositions(self): class LatLngLocation(Location): - def __init__(self, lat: float, lon: float): + def __init__(self, lat: float, lon: float, comment = None): self.lat = lat self.lon = lon + self.comment = comment def __dict__(self): - return {"type": "latlon", "lat": self.lat, "lon": self.lon} + res = {"type": "latlon", "lat": self.lat, "lon": self.lon} + if self.comment is not None: res["comment"] = self.comment + return res class LocatorLocation(Location): From b80e85638a24541696e46c1f2649cb008564a458 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 22:08:32 +0200 Subject: [PATCH 0308/2616] implement the horrifying mic-e protocol --- owrx/kiss.py | 121 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 86 insertions(+), 35 deletions(-) diff --git a/owrx/kiss.py b/owrx/kiss.py index bc3300be7..6a858429f 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -79,14 +79,14 @@ def chunks(l, n): yield l[i:i + n] information = ax25frame[control_pid+2:] - # TODO how can we tell if this is an APRS frame at all? - aprsData = self.parseAprsData(information) data = { "destination": self.extractCallsign(ax25frame[0:7]), "source": self.extractCallsign(ax25frame[7:14]), "path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)] } + # TODO how can we tell if this is an APRS frame at all? + aprsData = self.parseAprsData(data["destination"], information) data.update(aprsData) logger.debug(data) @@ -95,43 +95,94 @@ def chunks(l, n): Map.getSharedInstance().updateLocation(data["source"], loc, "APRS") return data - def parseAprsData(self, data): - #hexdump(data) + def hasCompressedCoordinatesx(self, raw): + return raw[0] == "/" or raw[0] == "\\" - # TODO detect MIC-E frame and decode accordingly - - data = data.decode() - logger.debug(data) + def parseUncompressedCoordinates(self, raw): + # TODO parse N/S and E/W + return { + "lat": int(raw[0:2]) + float(raw[2:7]) / 60, + "lon": int(raw[9:12]) + float(raw[12:17]) / 60, + "symbol": raw[18] + } - def parseCoordinates(raw): - # TODO parse N/S and E/W - return { - "lat": int(raw[0:2]) + float(raw[2:7]) / 60, - "lon": int(raw[9:12]) + float(raw[12:17]) / 60 - } - - if data[0] == "!": - # fixed - coords = parseCoordinates(data[1:19]) - coords["comment"] = data[20:] - return coords - elif data[0] == "/": - # APRS TNC - coords = parseCoordinates(data[8:26]) - coords["comment"] = data[27:] - return coords - elif data[0] == "@": - # MOBILE - # TODO CSE, SPD, BRG, 90Q, comments - if data[26] == "\\": - # DF - return parseCoordinates(data[8:26]) - coords = parseCoordinates(data[8:26]) - coords["symbol"] = data[26] - coords["comment"] = data[27:] - return coords + def parseCompressedCoordinates(self, raw): + # TODO parse compressed coordinate formats return {} + def parseMicEFrame(self, destination, information): + # TODO decode MIC-E Frame + + def extractNumber(input): + n = ord(input) + if n >= ord("P"): + return n - ord("P") + if n >= ord("A"): + return n - ord("A") + return n - ord("0") + + def listToNumber(input): + base = listToNumber(input[:-1]) * 10 if len(input) > 1 else 0 + return base + input[-1] + + logger.debug(destination) + rawLatitude = [extractNumber(c) for c in destination[0:6]] + logger.debug(rawLatitude) + lat = listToNumber(rawLatitude[0:2]) + listToNumber(rawLatitude[2:6]) / 6000 + if ord(destination[3]) <= ord("9"): + lat *= -1 + + logger.debug(lat) + + logger.debug(information) + lon = information[1] - 28 + if ord(destination[4]) >= ord("P"): + lon += 100 + if 180 <= lon <= 189: + lon -= 80 + if 190 <= lon <= 199: + lon -= 190 + + minutes = information[2] - 28 + if minutes >= 60: + minutes -= 60 + + lon += minutes / 60 + (information[3] - 28) / 6000 + + if ord(destination[5]) >= ord("P"): + lon *= -1 + + return { + "lat": lat, + "lon": lon, + "comment": information[9:].decode() + } + + def parseAprsData(self, destination, information): + if information[0] == 0x1c or information[0] == 0x60: + return self.parseMicEFrame(destination, information) + + information = information.decode() + logger.debug(information) + + if information[0] == "!" or information[0] == "=": + # position without timestamp + information = information[1:] + elif information[0] == "/" or information[0] == "@": + # position with timestamp + # TODO parse timestamp + information = information[8:] + else: + return {} + + if self.hasCompressedCoordinatesx(information): + coords = self.parseCompressedCoordinates(information[0:9]) + coords["comment"] = information[9:] + else: + coords = self.parseUncompressedCoordinates(information[0:19]) + coords["comment"] = information[19:] + return coords + def extractCallsign(self, input): cs = bytes([b >> 1 for b in input[0:6]]).decode().strip() ssid = (input[6] & 0b00011110) >> 1 From bf5e2bcc8432e45618466fb23bc29c4517edc1ac Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 11 Aug 2019 22:58:04 +0200 Subject: [PATCH 0309/2616] compressed locations; other TODOS --- owrx/kiss.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/owrx/kiss.py b/owrx/kiss.py index 6a858429f..43f8e6427 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -99,20 +99,29 @@ def hasCompressedCoordinatesx(self, raw): return raw[0] == "/" or raw[0] == "\\" def parseUncompressedCoordinates(self, raw): - # TODO parse N/S and E/W + lat = int(raw[0:2]) + float(raw[2:7]) / 60 + if raw[7] == "S": + lat *= -1 + lon = int(raw[9:12]) + float(raw[12:17]) / 60 + if raw[17] == "W": + lon *= -1 return { - "lat": int(raw[0:2]) + float(raw[2:7]) / 60, - "lon": int(raw[9:12]) + float(raw[12:17]) / 60, + "lat": lat, + "lon": lon, "symbol": raw[18] } def parseCompressedCoordinates(self, raw): - # TODO parse compressed coordinate formats - return {} + def decodeBase91(input): + base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 + return base + (ord(input[-1]) - 33) + return { + "lat": 90 - decodeBase91(raw[1:5]) / 380926, + "lon": -180 + decodeBase91(raw[5:9]) / 190463, + "symbol": raw[9] + } def parseMicEFrame(self, destination, information): - # TODO decode MIC-E Frame - def extractNumber(input): n = ord(input) if n >= ord("P"): @@ -176,8 +185,8 @@ def parseAprsData(self, destination, information): return {} if self.hasCompressedCoordinatesx(information): - coords = self.parseCompressedCoordinates(information[0:9]) - coords["comment"] = information[9:] + coords = self.parseCompressedCoordinates(information[0:10]) + coords["comment"] = information[10:] else: coords = self.parseUncompressedCoordinates(information[0:19]) coords["comment"] = information[19:] From 3dbc6ffb2b6d2c65d6d95ed21c864d7844ba4b1a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 12 Aug 2019 00:02:39 +0200 Subject: [PATCH 0310/2616] make aprs available as service --- config_webrx.py | 2 +- owrx/service.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index f75d3e3b1..a5cf0ad3a 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -253,4 +253,4 @@ temporary_directory = "/tmp" services_enabled = False -services_decoders = ["ft8", "ft4", "wspr"] +services_decoders = ["ft8", "ft4", "wspr", "packet"] diff --git a/owrx/service.py b/owrx/service.py index b64e504bc..0ae739044 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -80,8 +80,12 @@ def setupService(self, mode, frequency): d = dsp(ServiceOutput(frequency)) d.nc_port = self.source.getPort() d.set_offset_freq(frequency - self.source.getProps()["center_freq"]) - d.set_demodulator("usb") - d.set_bpf(0, 3000) + if mode == "packet": + d.set_demodulator("nfm") + d.set_bpf(-4000, 4000) + else: + d.set_demodulator("usb") + d.set_bpf(0, 3000) d.set_secondary_demodulator(mode) d.set_audio_compression("none") d.set_samp_rate(self.source.getProps()["samp_rate"]) From 4b3a68f4cdd906dad724218877e9aa08eb17c561 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 12 Aug 2019 11:05:32 +0200 Subject: [PATCH 0311/2616] fix the dial button (not enough space on some browsers) --- htdocs/css/openwebrx.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index cd1498ba0..c7460da9f 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -616,7 +616,7 @@ img.openwebrx-mirror-img #openwebrx-secondary-demod-listbox { - width: 174px; + width: 173px; height: 27px; padding-left:3px; } From 02073745929ba3a62ece36c1502ec160f152b7c7 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 15 Aug 2019 15:45:15 +0200 Subject: [PATCH 0312/2616] restructure the code to have the parser sit where all the parsers sit --- csdr.py | 2 +- htdocs/map.js | 2 +- owrx/aprs.py | 172 ++++++++++++++++++++++++++++++++++++++ owrx/connection.py | 4 +- owrx/kiss.py | 203 ++++++--------------------------------------- owrx/service.py | 34 +++++++- owrx/source.py | 8 +- 7 files changed, 239 insertions(+), 186 deletions(-) create mode 100644 owrx/aprs.py diff --git a/csdr.py b/csdr.py index 921107502..f3f42d350 100644 --- a/csdr.py +++ b/csdr.py @@ -235,7 +235,7 @@ def secondary_chain(self, which): chain = secondary_chain_base + "csdr fmdemod_quadri_cf | " if self.last_decimation != 1.0: chain += "csdr fractional_decimator_ff {last_decimation} | " - chain += "csdr convert_f_s16 | direwolf -r {audio_rate} -t 0 -" + chain += "csdr convert_f_s16 | direwolf -r {audio_rate} -t 0 - 1>&2" return chain def set_secondary_demodulator(self, what): diff --git a/htdocs/map.js b/htdocs/map.js index d235e07b9..190b0baf2 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -290,7 +290,7 @@ } infowindow.setContent( '

    ' + callsign + '

    ' + - '
    ' + timestring + ' using ' + marker.mode + '
    ' + + '
    ' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '
    ' + commentString ); infowindow.open(map, marker); diff --git a/owrx/aprs.py b/owrx/aprs.py new file mode 100644 index 000000000..e0ead98d3 --- /dev/null +++ b/owrx/aprs.py @@ -0,0 +1,172 @@ +from owrx.kiss import KissDeframer +from owrx.map import Map, LatLngLocation +from owrx.bands import Bandplan +import logging + +logger = logging.getLogger(__name__) + + +class Ax25Parser(object): + + def parse(self, ax25frame): + control_pid = ax25frame.find(bytes([0x03, 0xf0])) + if control_pid % 7 > 0: + logger.warning("aprs packet framing error: control/pid position not aligned with 7-octet callsign data") + + def chunks(l, n): + """Yield successive n-sized chunks from l.""" + for i in range(0, len(l), n): + yield l[i:i + n] + + return { + "destination": self.extractCallsign(ax25frame[0:7]), + "source": self.extractCallsign(ax25frame[7:14]), + "path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)], + "data": ax25frame[control_pid+2:] + } + + def extractCallsign(self, input): + cs = bytes([b >> 1 for b in input[0:6]]).decode().strip() + ssid = (input[6] & 0b00011110) >> 1 + if ssid > 0: + return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid) + else: + return cs + + +class AprsParser(object): + + def __init__(self, handler): + self.ax25parser = Ax25Parser() + self.deframer = KissDeframer() + self.dial_freq = None + self.band = None + self.handler = handler + + def setDialFrequency(self, freq): + self.dial_freq = freq + self.band = Bandplan.getSharedInstance().findBand(freq) + + def parse(self, raw): + for frame in self.deframer.parse(raw): + data = self.ax25parser.parse(frame) + + # TODO how can we tell if this is an APRS frame at all? + aprsData = self.parseAprsData(data) + + logger.debug(aprsData) + if "lat" in aprsData and "lon" in aprsData: + loc = LatLngLocation(aprsData["lat"], aprsData["lon"], aprsData["comment"] if "comment" in data else None) + Map.getSharedInstance().updateLocation(data["source"], loc, "APRS", self.band) + + self.handler.write_aprs_data(aprsData) + + def hasCompressedCoordinatesx(self, raw): + return raw[0] == "/" or raw[0] == "\\" + + def parseUncompressedCoordinates(self, raw): + lat = int(raw[0:2]) + float(raw[2:7]) / 60 + if raw[7] == "S": + lat *= -1 + lon = int(raw[9:12]) + float(raw[12:17]) / 60 + if raw[17] == "W": + lon *= -1 + return { + "lat": lat, + "lon": lon, + "symbol": raw[18] + } + + def parseCompressedCoordinates(self, raw): + def decodeBase91(input): + base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 + return base + (ord(input[-1]) - 33) + return { + "lat": 90 - decodeBase91(raw[1:5]) / 380926, + "lon": -180 + decodeBase91(raw[5:9]) / 190463, + "symbol": raw[9] + } + + def parseMicEFrame(self, data): + information = data["data"] + destination = data["destination"] + + def extractNumber(input): + n = ord(input) + if n >= ord("P"): + return n - ord("P") + if n >= ord("A"): + return n - ord("A") + return n - ord("0") + + def listToNumber(input): + base = listToNumber(input[:-1]) * 10 if len(input) > 1 else 0 + return base + input[-1] + + logger.debug(destination) + rawLatitude = [extractNumber(c) for c in destination[0:6]] + logger.debug(rawLatitude) + lat = listToNumber(rawLatitude[0:2]) + listToNumber(rawLatitude[2:6]) / 6000 + if ord(destination[3]) <= ord("9"): + lat *= -1 + + logger.debug(lat) + + logger.debug(information) + lon = information[1] - 28 + if ord(destination[4]) >= ord("P"): + lon += 100 + if 180 <= lon <= 189: + lon -= 80 + if 190 <= lon <= 199: + lon -= 190 + + minutes = information[2] - 28 + if minutes >= 60: + minutes -= 60 + + lon += minutes / 60 + (information[3] - 28) / 6000 + + if ord(destination[5]) >= ord("P"): + lon *= -1 + + return { + "lat": lat, + "lon": lon, + "comment": information[9:].decode() + } + + def parseAprsData(self, data): + information = data["data"] + + # forward some of the ax25 data + aprsData = { + "source": data["source"], + "destination": data["destination"], + "path": data["path"] + } + + if information[0] == 0x1c or information[0] == 0x60: + aprsData.update(self.parseMicEFrame(data)) + return aprsData + + information = information.decode() + logger.debug(information) + + if information[0] == "!" or information[0] == "=": + # position without timestamp + information = information[1:] + elif information[0] == "/" or information[0] == "@": + # position with timestamp + # TODO parse timestamp + information = information[8:] + else: + return {} + + if self.hasCompressedCoordinatesx(information): + aprsData.update(self.parseCompressedCoordinates(information[0:10])) + aprsData["comment"] = information[10:] + else: + aprsData.update(self.parseUncompressedCoordinates(information[0:19])) + aprsData["comment"] = information[19:] + return aprsData diff --git a/owrx/connection.py b/owrx/connection.py index 7460afa9e..f873dc07c 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -205,8 +205,8 @@ def write_wsjt_message(self, message): def write_dial_frequendies(self, frequencies): self.protected_send({"type": "dial_frequencies", "value": frequencies}) - def write_packet_data(self, data): - self.protected_send({"type": "packet_data", "value": data}) + def write_aprs_data(self, data): + self.protected_send({"type": "aprs_data", "value": data}) class MapConnection(Client): diff --git a/owrx/kiss.py b/owrx/kiss.py index 43f8e6427..8338d9189 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -1,6 +1,5 @@ import socket import time -from owrx.map import Map, LatLngLocation import logging logger = logging.getLogger(__name__) @@ -10,23 +9,6 @@ TFEND = 0xDC TFESC = 0XDD -def group(a, *ns): - for n in ns: - a = [a[i:i+n] for i in range(0, len(a), n)] - return a - -def join(a, *cs): - return [cs[0].join(join(t, *cs[1:])) for t in a] if cs else a - -def hexdump(data): - toHex = lambda c: '{:02X}'.format(c) - toChr = lambda c: chr(c) if 32 <= c < 127 else '.' - make = lambda f, *cs: join(group(list(map(f, data)), 8, 2), *cs) - hs = make(toHex, ' ', ' ') - cs = make(toChr, ' ', '') - for i, (h, c) in enumerate(zip(hs, cs)): - print ('{:010X}: {:48} {:16}'.format(i * 16, h, c)) - class KissClient(object): def __init__(self, port): @@ -35,167 +17,32 @@ def __init__(self, port): self.socket.connect(("localhost", port)) def read(self): - buf = bytes() - escaped = False - while True: - input = self.socket.recv(1) - # EOF - if len(input) == 0: - return bytes() - - if input[0] == FESC: - escaped = True - elif escaped: - if input[0] == TFEND: - buf += [FEND] - elif input[0] == TFESC: - buf += [FESC] + return self.socket.recv(1) + + +class KissDeframer(object): + def __init__(self): + self.escaped = False + self.buf = bytearray() + + def parse(self, input): + frames = [] + for b in input: + if b == FESC: + self.escaped = True + elif self.escaped: + if b == TFEND: + self.buf.append(FEND) + elif b == TFESC: + self.buf.append(FESC) else: logger.warning("invalid escape char: %s", str(input[0])) - escaped = False + self.escaped = False elif input[0] == FEND: - logger.debug("decoded frame: " + str(buf)) - if len(buf) > 0: - try: - return self.parseFrame(buf) - except Exception: - logger.exception("failed to decode packet data") - return {} + # data frames start with 0x00 + if len(self.buf) > 1 and self.buf[0] == 0x00: + frames += [self.buf[1:]] + self.buf = bytearray() else: - buf += input - - def parseFrame(self, frame): - # data frames start with 0x00 - if frame[0] != 0x00: - return {} - ax25frame = frame[1:] - control_pid = ax25frame.find(bytes([0x03, 0xf0])) - if control_pid % 7 > 0: - logger.warning("aprs packet framing error: control/pid position not aligned with 7-octet callsign data") - - def chunks(l, n): - """Yield successive n-sized chunks from l.""" - for i in range(0, len(l), n): - yield l[i:i + n] - - information = ax25frame[control_pid+2:] - - data = { - "destination": self.extractCallsign(ax25frame[0:7]), - "source": self.extractCallsign(ax25frame[7:14]), - "path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)] - } - # TODO how can we tell if this is an APRS frame at all? - aprsData = self.parseAprsData(data["destination"], information) - data.update(aprsData) - - logger.debug(data) - if "lat" in data and "lon" in data: - loc = LatLngLocation(data["lat"], data["lon"], data["comment"] if "comment" in data else None) - Map.getSharedInstance().updateLocation(data["source"], loc, "APRS") - return data - - def hasCompressedCoordinatesx(self, raw): - return raw[0] == "/" or raw[0] == "\\" - - def parseUncompressedCoordinates(self, raw): - lat = int(raw[0:2]) + float(raw[2:7]) / 60 - if raw[7] == "S": - lat *= -1 - lon = int(raw[9:12]) + float(raw[12:17]) / 60 - if raw[17] == "W": - lon *= -1 - return { - "lat": lat, - "lon": lon, - "symbol": raw[18] - } - - def parseCompressedCoordinates(self, raw): - def decodeBase91(input): - base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 - return base + (ord(input[-1]) - 33) - return { - "lat": 90 - decodeBase91(raw[1:5]) / 380926, - "lon": -180 + decodeBase91(raw[5:9]) / 190463, - "symbol": raw[9] - } - - def parseMicEFrame(self, destination, information): - def extractNumber(input): - n = ord(input) - if n >= ord("P"): - return n - ord("P") - if n >= ord("A"): - return n - ord("A") - return n - ord("0") - - def listToNumber(input): - base = listToNumber(input[:-1]) * 10 if len(input) > 1 else 0 - return base + input[-1] - - logger.debug(destination) - rawLatitude = [extractNumber(c) for c in destination[0:6]] - logger.debug(rawLatitude) - lat = listToNumber(rawLatitude[0:2]) + listToNumber(rawLatitude[2:6]) / 6000 - if ord(destination[3]) <= ord("9"): - lat *= -1 - - logger.debug(lat) - - logger.debug(information) - lon = information[1] - 28 - if ord(destination[4]) >= ord("P"): - lon += 100 - if 180 <= lon <= 189: - lon -= 80 - if 190 <= lon <= 199: - lon -= 190 - - minutes = information[2] - 28 - if minutes >= 60: - minutes -= 60 - - lon += minutes / 60 + (information[3] - 28) / 6000 - - if ord(destination[5]) >= ord("P"): - lon *= -1 - - return { - "lat": lat, - "lon": lon, - "comment": information[9:].decode() - } - - def parseAprsData(self, destination, information): - if information[0] == 0x1c or information[0] == 0x60: - return self.parseMicEFrame(destination, information) - - information = information.decode() - logger.debug(information) - - if information[0] == "!" or information[0] == "=": - # position without timestamp - information = information[1:] - elif information[0] == "/" or information[0] == "@": - # position with timestamp - # TODO parse timestamp - information = information[8:] - else: - return {} - - if self.hasCompressedCoordinatesx(information): - coords = self.parseCompressedCoordinates(information[0:10]) - coords["comment"] = information[10:] - else: - coords = self.parseUncompressedCoordinates(information[0:19]) - coords["comment"] = information[19:] - return coords - - def extractCallsign(self, input): - cs = bytes([b >> 1 for b in input[0:6]]).decode().strip() - ssid = (input[6] & 0b00011110) >> 1 - if ssid > 0: - return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid) - else: - return cs + self.buf.append(b) + return frames diff --git a/owrx/service.py b/owrx/service.py index 0ae739044..c3d598e06 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -3,6 +3,7 @@ from owrx.bands import Bandplan from csdr import dsp, output from owrx.wsjt import WsjtParser +from owrx.aprs import AprsParser from owrx.config import PropertyManager import logging @@ -14,16 +15,35 @@ class ServiceOutput(output): def __init__(self, frequency): self.frequency = frequency + def getParser(self): + # abstract method; implement in subclasses + pass + def receive_output(self, t, read_fn): - parser = WsjtParser(WsjtHandler()) + parser = self.getParser() parser.setDialFrequency(self.frequency) target = self.pump(read_fn, parser.parse) threading.Thread(target=target).start() + +class WsjtServiceOutput(ServiceOutput): + + def getParser(self): + return WsjtParser(WsjtHandler()) + def supports_type(self, t): return t == "wsjt_demod" +class AprsServiceOutput(ServiceOutput): + + def getParser(self): + return AprsParser(AprsHandler()) + + def supports_type(self, t): + return t == "packet_demod" + + class ServiceHandler(object): def __init__(self, source): self.services = [] @@ -77,7 +97,12 @@ def updateServices(self): def setupService(self, mode, frequency): logger.debug("setting up service {0} on frequency {1}".format(mode, frequency)) - d = dsp(ServiceOutput(frequency)) + # TODO selecting outputs will need some more intelligence here + if mode == "packet": + output = AprsServiceOutput(frequency) + else: + output = WsjtServiceOutput(frequency) + d = dsp(output) d.nc_port = self.source.getPort() d.set_offset_freq(frequency - self.source.getProps()["center_freq"]) if mode == "packet": @@ -98,6 +123,11 @@ def write_wsjt_message(self, msg): pass +class AprsHandler(object): + def write_aprs_data(self, data): + pass + + class ServiceManager(object): sharedInstance = None diff --git a/owrx/source.py b/owrx/source.py index fe45ccc5c..e867af6c1 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -3,6 +3,7 @@ from owrx.feature import FeatureDetector, UnknownFeatureException from owrx.meta import MetaParser from owrx.wsjt import WsjtParser +from owrx.aprs import AprsParser import threading import csdr import time @@ -406,6 +407,7 @@ def __init__(self, handler, sdrSource): self.sdrSource = sdrSource self.metaParser = MetaParser(self.handler) self.wsjtParser = WsjtParser(self.handler) + self.aprsParser = AprsParser(self.handler) self.localProps = ( self.sdrSource.getProps() @@ -440,7 +442,9 @@ def set_high_cut(cut): self.dsp.set_bpf(*bpf) def set_dial_freq(key, value): - self.wsjtParser.setDialFrequency(self.localProps["center_freq"] + self.localProps["offset_freq"]) + freq = self.localProps["center_freq"] + self.localProps["offset_freq"] + self.wsjtParser.setDialFrequency(freq) + self.aprsParser.setDialFrequency(freq) self.subscriptions = [ self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression), @@ -502,7 +506,7 @@ def receive_output(self, t, read_fn): "secondary_demod": self.handler.write_secondary_demod, "meta": self.metaParser.parse, "wsjt_demod": self.wsjtParser.parse, - "packet_demod": self.handler.write_packet_data, + "packet_demod": self.aprsParser.parse, } write = writers[t] From 439da266a9136b44e029b90dfbf2b59f6268df3e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 15 Aug 2019 15:53:55 +0200 Subject: [PATCH 0313/2616] prevent empty frames --- owrx/aprs.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index e0ead98d3..e4f67d301 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -155,18 +155,19 @@ def parseAprsData(self, data): if information[0] == "!" or information[0] == "=": # position without timestamp - information = information[1:] + aprsData.update(self.parseRegularAprsData(information[1:])) elif information[0] == "/" or information[0] == "@": # position with timestamp # TODO parse timestamp - information = information[8:] - else: - return {} + aprsData.update(self.parseRegularAprsData(information[8:])) + + return aprsData + def parseRegularAprsData(self, information): if self.hasCompressedCoordinatesx(information): - aprsData.update(self.parseCompressedCoordinates(information[0:10])) + aprsData = self.parseCompressedCoordinates(information[0:10]) aprsData["comment"] = information[10:] else: - aprsData.update(self.parseUncompressedCoordinates(information[0:19])) + aprsData = self.parseUncompressedCoordinates(information[0:19]) aprsData["comment"] = information[19:] return aprsData From 6b93973d9bfe5be530be3f0ede8ba9b5fa223287 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 15 Aug 2019 18:08:20 +0200 Subject: [PATCH 0314/2616] decode mic-e device and altitude data --- owrx/aprs.py | 201 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 138 insertions(+), 63 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index e4f67d301..6b4061cb2 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -6,6 +6,11 @@ logger = logging.getLogger(__name__) +def decodeBase91(input): + base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 + return base + (ord(input[-1]) - 33) + + class Ax25Parser(object): def parse(self, ax25frame): @@ -26,7 +31,7 @@ def chunks(l, n): } def extractCallsign(self, input): - cs = bytes([b >> 1 for b in input[0:6]]).decode().strip() + cs = bytes([b >> 1 for b in input[0:6]]).decode('us-ascii').strip() ssid = (input[6] & 0b00011110) >> 1 if ssid > 0: return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid) @@ -49,17 +54,20 @@ def setDialFrequency(self, freq): def parse(self, raw): for frame in self.deframer.parse(raw): - data = self.ax25parser.parse(frame) + try: + data = self.ax25parser.parse(frame) - # TODO how can we tell if this is an APRS frame at all? - aprsData = self.parseAprsData(data) + # TODO how can we tell if this is an APRS frame at all? + aprsData = self.parseAprsData(data) - logger.debug(aprsData) - if "lat" in aprsData and "lon" in aprsData: - loc = LatLngLocation(aprsData["lat"], aprsData["lon"], aprsData["comment"] if "comment" in data else None) - Map.getSharedInstance().updateLocation(data["source"], loc, "APRS", self.band) + logger.debug(aprsData) + if "lat" in aprsData and "lon" in aprsData: + loc = LatLngLocation(aprsData["lat"], aprsData["lon"], aprsData["comment"] if "comment" in aprsData else None) + Map.getSharedInstance().updateLocation(data["source"], loc, "APRS", self.band) - self.handler.write_aprs_data(aprsData) + self.handler.write_aprs_data(aprsData) + except Exception: + logger.exception("exception while parsing aprs data") def hasCompressedCoordinatesx(self, raw): return raw[0] == "/" or raw[0] == "\\" @@ -78,64 +86,12 @@ def parseUncompressedCoordinates(self, raw): } def parseCompressedCoordinates(self, raw): - def decodeBase91(input): - base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 - return base + (ord(input[-1]) - 33) return { "lat": 90 - decodeBase91(raw[1:5]) / 380926, "lon": -180 + decodeBase91(raw[5:9]) / 190463, "symbol": raw[9] } - def parseMicEFrame(self, data): - information = data["data"] - destination = data["destination"] - - def extractNumber(input): - n = ord(input) - if n >= ord("P"): - return n - ord("P") - if n >= ord("A"): - return n - ord("A") - return n - ord("0") - - def listToNumber(input): - base = listToNumber(input[:-1]) * 10 if len(input) > 1 else 0 - return base + input[-1] - - logger.debug(destination) - rawLatitude = [extractNumber(c) for c in destination[0:6]] - logger.debug(rawLatitude) - lat = listToNumber(rawLatitude[0:2]) + listToNumber(rawLatitude[2:6]) / 6000 - if ord(destination[3]) <= ord("9"): - lat *= -1 - - logger.debug(lat) - - logger.debug(information) - lon = information[1] - 28 - if ord(destination[4]) >= ord("P"): - lon += 100 - if 180 <= lon <= 189: - lon -= 80 - if 190 <= lon <= 199: - lon -= 190 - - minutes = information[2] - 28 - if minutes >= 60: - minutes -= 60 - - lon += minutes / 60 + (information[3] - 28) / 6000 - - if ord(destination[5]) >= ord("P"): - lon *= -1 - - return { - "lat": lat, - "lon": lon, - "comment": information[9:].decode() - } - def parseAprsData(self, data): information = data["data"] @@ -147,10 +103,11 @@ def parseAprsData(self, data): } if information[0] == 0x1c or information[0] == 0x60: - aprsData.update(self.parseMicEFrame(data)) + parser = MicEParser() + aprsData.update(parser.parse(data)) return aprsData - information = information.decode() + information = information.decode('us-ascii') logger.debug(information) if information[0] == "!" or information[0] == "=": @@ -171,3 +128,121 @@ def parseRegularAprsData(self, information): aprsData = self.parseUncompressedCoordinates(information[0:19]) aprsData["comment"] = information[19:] return aprsData + + +class MicEParser(object): + def extractNumber(self, input): + n = ord(input) + if n >= ord("P"): + return n - ord("P") + if n >= ord("A"): + return n - ord("A") + return n - ord("0") + + def listToNumber(self, input): + base = self.listToNumber(input[:-1]) * 10 if len(input) > 1 else 0 + return base + input[-1] + + def extractAltitude(self, comment): + if len(comment) < 4 or comment[3] != "}": + return (comment, None) + return comment[4:], decodeBase91(comment[:3]) - 10000 + + def extractDevice(self, comment): + if comment[0] == ">": + if comment[-1] == "=": + return comment[1:], {"manufacturer": "Kenwood", "device": "TH-D72"} + if comment[-1] == "^": + return comment[1:], {"manufacturer": "Kenwood", "device": "TH-D74"} + return comment[1:], {"manufacturer": "Kenwood", "device": "TH-D7A"} + if comment[0] == "]": + if comment[-1] == "=": + return comment[1:], {"manufacturer": "Kenwood", "device": "TM-D710"} + return comment[1:], {"manufacturer": "Kenwood", "device": "TM-D700"} + if comment[0] == "`" or comment[0] == "'": + if comment[-2] == "_": + devices = { + "b": "VX-8", + "\"": "FTM-350", + "#": "VX-8G", + "$": "FT1D", + "%": "FTM-400DR", + ")": "FTM-100D", + "(": "FT2D", + "0": "FT3D", + } + return comment[1:-2], {"manufacturer": "Yaesu", "device": devices.get(comment[-1], "Unknown")} + if comment[-2:] == " X": + return comment[1:-2], {"manufacturer": "SainSonic", "device": "AP510"} + if comment[-2] == "(": + devices = { + "5": "D578UV", + "8": "D878UV" + } + return comment[1:-2], {"manufacturer": "Anytone", "device": devices.get(comment[-1], "Unknown")} + if comment[-2] == "|": + devices = { + "3": "TinyTrack3", + "4": "TinyTrack4" + } + return comment[1:-2], {"manufacturer": "Byonics", "device": devices.get(comment[-1], "Unknown")} + if comment[-2:] == "^v": + return comment[1:-2], {"manufacturer": "HinzTec", "device": "anyfrog"} + if comment[-2] == ":": + devices = { + "4": "P4dragon DR-7400 modem", + "8": "P4dragon DR-7800 modem" + } + return comment[1:-2], {"manufacturer": "SCS GmbH & Co.", "device": devices.get(comment[-1], "Unknown")} + if comment[-2:] == "~v": + return comment[1:-2], {"manufacturer": "Other", "device": "Other"} + return comment[1:-2], None + return comment, None + + def parse(self, data): + information = data["data"] + destination = data["destination"] + + logger.debug(destination) + rawLatitude = [self.extractNumber(c) for c in destination[0:6]] + logger.debug(rawLatitude) + lat = self.listToNumber(rawLatitude[0:2]) + self.listToNumber(rawLatitude[2:6]) / 6000 + if ord(destination[3]) <= ord("9"): + lat *= -1 + + logger.debug(lat) + + logger.debug(information) + lon = information[1] - 28 + if ord(destination[4]) >= ord("P"): + lon += 100 + if 180 <= lon <= 189: + lon -= 80 + if 190 <= lon <= 199: + lon -= 190 + + minutes = information[2] - 28 + if minutes >= 60: + minutes -= 60 + + lon += minutes / 60 + (information[3] - 28) / 6000 + + if ord(destination[5]) >= ord("P"): + lon *= -1 + + comment = information[9:].decode('us-ascii').strip() + (comment, altitude) = self.extractAltitude(comment) + + (comment, device) = self.extractDevice(comment) + + # altitude might be inside the device string, so repeat and choose one + (comment, insideAltitude) = self.extractAltitude(comment) + altitude = next((a for a in [altitude, insideAltitude] if a is not None), None) + + return { + "lat": lat, + "lon": lon, + "comment": comment, + "altitude": altitude, + "device": device, + } From 765f075576e33ae004fade13f72f4982f37cd807 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 15 Aug 2019 18:21:35 +0200 Subject: [PATCH 0315/2616] add some type information; fix string offsets --- owrx/aprs.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 6b4061cb2..77f3ef2c3 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -103,8 +103,7 @@ def parseAprsData(self, data): } if information[0] == 0x1c or information[0] == 0x60: - parser = MicEParser() - aprsData.update(parser.parse(data)) + aprsData.update(MicEParser().parse(data)) return aprsData information = information.decode('us-ascii') @@ -123,9 +122,11 @@ def parseAprsData(self, data): def parseRegularAprsData(self, information): if self.hasCompressedCoordinatesx(information): aprsData = self.parseCompressedCoordinates(information[0:10]) + aprsData["type"] = "compressed" aprsData["comment"] = information[10:] else: aprsData = self.parseUncompressedCoordinates(information[0:19]) + aprsData["type"] = "regular" aprsData["comment"] = information[19:] return aprsData @@ -151,13 +152,13 @@ def extractAltitude(self, comment): def extractDevice(self, comment): if comment[0] == ">": if comment[-1] == "=": - return comment[1:], {"manufacturer": "Kenwood", "device": "TH-D72"} + return comment[1:-1], {"manufacturer": "Kenwood", "device": "TH-D72"} if comment[-1] == "^": - return comment[1:], {"manufacturer": "Kenwood", "device": "TH-D74"} + return comment[1:-1], {"manufacturer": "Kenwood", "device": "TH-D74"} return comment[1:], {"manufacturer": "Kenwood", "device": "TH-D7A"} if comment[0] == "]": if comment[-1] == "=": - return comment[1:], {"manufacturer": "Kenwood", "device": "TM-D710"} + return comment[1:-1], {"manufacturer": "Kenwood", "device": "TM-D710"} return comment[1:], {"manufacturer": "Kenwood", "device": "TM-D700"} if comment[0] == "`" or comment[0] == "'": if comment[-2] == "_": @@ -245,4 +246,5 @@ def parse(self, data): "comment": comment, "altitude": altitude, "device": device, + "type": "Mic-E", } From 88bbb767526588eba698529e4155734fa1234dd0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 15 Aug 2019 19:50:47 +0200 Subject: [PATCH 0316/2616] make sure there is actually enough data to parse --- owrx/aprs.py | 98 +++++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 77f3ef2c3..2d9e48c3c 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -150,54 +150,56 @@ def extractAltitude(self, comment): return comment[4:], decodeBase91(comment[:3]) - 10000 def extractDevice(self, comment): - if comment[0] == ">": - if comment[-1] == "=": - return comment[1:-1], {"manufacturer": "Kenwood", "device": "TH-D72"} - if comment[-1] == "^": - return comment[1:-1], {"manufacturer": "Kenwood", "device": "TH-D74"} - return comment[1:], {"manufacturer": "Kenwood", "device": "TH-D7A"} - if comment[0] == "]": - if comment[-1] == "=": - return comment[1:-1], {"manufacturer": "Kenwood", "device": "TM-D710"} - return comment[1:], {"manufacturer": "Kenwood", "device": "TM-D700"} - if comment[0] == "`" or comment[0] == "'": - if comment[-2] == "_": - devices = { - "b": "VX-8", - "\"": "FTM-350", - "#": "VX-8G", - "$": "FT1D", - "%": "FTM-400DR", - ")": "FTM-100D", - "(": "FT2D", - "0": "FT3D", - } - return comment[1:-2], {"manufacturer": "Yaesu", "device": devices.get(comment[-1], "Unknown")} - if comment[-2:] == " X": - return comment[1:-2], {"manufacturer": "SainSonic", "device": "AP510"} - if comment[-2] == "(": - devices = { - "5": "D578UV", - "8": "D878UV" - } - return comment[1:-2], {"manufacturer": "Anytone", "device": devices.get(comment[-1], "Unknown")} - if comment[-2] == "|": - devices = { - "3": "TinyTrack3", - "4": "TinyTrack4" - } - return comment[1:-2], {"manufacturer": "Byonics", "device": devices.get(comment[-1], "Unknown")} - if comment[-2:] == "^v": - return comment[1:-2], {"manufacturer": "HinzTec", "device": "anyfrog"} - if comment[-2] == ":": - devices = { - "4": "P4dragon DR-7400 modem", - "8": "P4dragon DR-7800 modem" - } - return comment[1:-2], {"manufacturer": "SCS GmbH & Co.", "device": devices.get(comment[-1], "Unknown")} - if comment[-2:] == "~v": - return comment[1:-2], {"manufacturer": "Other", "device": "Other"} - return comment[1:-2], None + if len(comment) > 0: + if comment[0] == ">": + if len(comment) > 1: + if comment[-1] == "=": + return comment[1:-1], {"manufacturer": "Kenwood", "device": "TH-D72"} + if comment[-1] == "^": + return comment[1:-1], {"manufacturer": "Kenwood", "device": "TH-D74"} + return comment[1:], {"manufacturer": "Kenwood", "device": "TH-D7A"} + if comment[0] == "]": + if len(comment) > 1 and comment[-1] == "=": + return comment[1:-1], {"manufacturer": "Kenwood", "device": "TM-D710"} + return comment[1:], {"manufacturer": "Kenwood", "device": "TM-D700"} + if len(comment) > 2 and (comment[0] == "`" or comment[0] == "'"): + if comment[-2] == "_": + devices = { + "b": "VX-8", + "\"": "FTM-350", + "#": "VX-8G", + "$": "FT1D", + "%": "FTM-400DR", + ")": "FTM-100D", + "(": "FT2D", + "0": "FT3D", + } + return comment[1:-2], {"manufacturer": "Yaesu", "device": devices.get(comment[-1], "Unknown")} + if comment[-2:] == " X": + return comment[1:-2], {"manufacturer": "SainSonic", "device": "AP510"} + if comment[-2] == "(": + devices = { + "5": "D578UV", + "8": "D878UV" + } + return comment[1:-2], {"manufacturer": "Anytone", "device": devices.get(comment[-1], "Unknown")} + if comment[-2] == "|": + devices = { + "3": "TinyTrack3", + "4": "TinyTrack4" + } + return comment[1:-2], {"manufacturer": "Byonics", "device": devices.get(comment[-1], "Unknown")} + if comment[-2:] == "^v": + return comment[1:-2], {"manufacturer": "HinzTec", "device": "anyfrog"} + if comment[-2] == ":": + devices = { + "4": "P4dragon DR-7400 modem", + "8": "P4dragon DR-7800 modem" + } + return comment[1:-2], {"manufacturer": "SCS GmbH & Co.", "device": devices.get(comment[-1], "Unknown")} + if comment[-2:] == "~v": + return comment[1:-2], {"manufacturer": "Other", "device": "Other"} + return comment[1:-2], None return comment, None def parse(self, data): From 21591ad6b871f85dcd120fd7d079d3d4cb62686b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 15 Aug 2019 19:56:59 +0200 Subject: [PATCH 0317/2616] format --- csdr.py | 2 +- owrx/aprs.py | 79 ++++++++++++++++++------------------------------- owrx/feature.py | 2 +- owrx/kiss.py | 2 +- owrx/map.py | 5 ++-- owrx/service.py | 2 -- 6 files changed, 35 insertions(+), 57 deletions(-) diff --git a/csdr.py b/csdr.py index f3f42d350..a896e9b84 100644 --- a/csdr.py +++ b/csdr.py @@ -437,7 +437,7 @@ def isWsjtMode(self, demodulator=None): demodulator = self.get_secondary_demodulator() return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"] - def isPacket(self, demodulator = None): + def isPacket(self, demodulator=None): if demodulator is None: demodulator = self.get_secondary_demodulator() return demodulator == "packet" diff --git a/owrx/aprs.py b/owrx/aprs.py index 2d9e48c3c..13adb4a34 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -12,26 +12,25 @@ def decodeBase91(input): class Ax25Parser(object): - def parse(self, ax25frame): - control_pid = ax25frame.find(bytes([0x03, 0xf0])) + control_pid = ax25frame.find(bytes([0x03, 0xF0])) if control_pid % 7 > 0: logger.warning("aprs packet framing error: control/pid position not aligned with 7-octet callsign data") def chunks(l, n): """Yield successive n-sized chunks from l.""" for i in range(0, len(l), n): - yield l[i:i + n] + yield l[i : i + n] return { "destination": self.extractCallsign(ax25frame[0:7]), "source": self.extractCallsign(ax25frame[7:14]), "path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)], - "data": ax25frame[control_pid+2:] + "data": ax25frame[control_pid + 2 :], } def extractCallsign(self, input): - cs = bytes([b >> 1 for b in input[0:6]]).decode('us-ascii').strip() + cs = bytes([b >> 1 for b in input[0:6]]).decode("us-ascii").strip() ssid = (input[6] & 0b00011110) >> 1 if ssid > 0: return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid) @@ -40,7 +39,6 @@ def extractCallsign(self, input): class AprsParser(object): - def __init__(self, handler): self.ax25parser = Ax25Parser() self.deframer = KissDeframer() @@ -62,7 +60,9 @@ def parse(self, raw): logger.debug(aprsData) if "lat" in aprsData and "lon" in aprsData: - loc = LatLngLocation(aprsData["lat"], aprsData["lon"], aprsData["comment"] if "comment" in aprsData else None) + loc = LatLngLocation( + aprsData["lat"], aprsData["lon"], aprsData["comment"] if "comment" in aprsData else None + ) Map.getSharedInstance().updateLocation(data["source"], loc, "APRS", self.band) self.handler.write_aprs_data(aprsData) @@ -79,34 +79,26 @@ def parseUncompressedCoordinates(self, raw): lon = int(raw[9:12]) + float(raw[12:17]) / 60 if raw[17] == "W": lon *= -1 - return { - "lat": lat, - "lon": lon, - "symbol": raw[18] - } + return {"lat": lat, "lon": lon, "symbol": raw[18]} def parseCompressedCoordinates(self, raw): return { "lat": 90 - decodeBase91(raw[1:5]) / 380926, "lon": -180 + decodeBase91(raw[5:9]) / 190463, - "symbol": raw[9] + "symbol": raw[9], } def parseAprsData(self, data): information = data["data"] # forward some of the ax25 data - aprsData = { - "source": data["source"], - "destination": data["destination"], - "path": data["path"] - } + aprsData = {"source": data["source"], "destination": data["destination"], "path": data["path"]} - if information[0] == 0x1c or information[0] == 0x60: + if information[0] == 0x1C or information[0] == 0x60: aprsData.update(MicEParser().parse(data)) return aprsData - information = information.decode('us-ascii') + information = information.decode("us-ascii") logger.debug(information) if information[0] == "!" or information[0] == "=": @@ -165,38 +157,32 @@ def extractDevice(self, comment): if len(comment) > 2 and (comment[0] == "`" or comment[0] == "'"): if comment[-2] == "_": devices = { - "b": "VX-8", - "\"": "FTM-350", - "#": "VX-8G", - "$": "FT1D", - "%": "FTM-400DR", - ")": "FTM-100D", - "(": "FT2D", - "0": "FT3D", + "b": "VX-8", + '"': "FTM-350", + "#": "VX-8G", + "$": "FT1D", + "%": "FTM-400DR", + ")": "FTM-100D", + "(": "FT2D", + "0": "FT3D", } return comment[1:-2], {"manufacturer": "Yaesu", "device": devices.get(comment[-1], "Unknown")} if comment[-2:] == " X": return comment[1:-2], {"manufacturer": "SainSonic", "device": "AP510"} if comment[-2] == "(": - devices = { - "5": "D578UV", - "8": "D878UV" - } + devices = {"5": "D578UV", "8": "D878UV"} return comment[1:-2], {"manufacturer": "Anytone", "device": devices.get(comment[-1], "Unknown")} if comment[-2] == "|": - devices = { - "3": "TinyTrack3", - "4": "TinyTrack4" - } + devices = {"3": "TinyTrack3", "4": "TinyTrack4"} return comment[1:-2], {"manufacturer": "Byonics", "device": devices.get(comment[-1], "Unknown")} if comment[-2:] == "^v": return comment[1:-2], {"manufacturer": "HinzTec", "device": "anyfrog"} if comment[-2] == ":": - devices = { - "4": "P4dragon DR-7400 modem", - "8": "P4dragon DR-7800 modem" - } - return comment[1:-2], {"manufacturer": "SCS GmbH & Co.", "device": devices.get(comment[-1], "Unknown")} + devices = {"4": "P4dragon DR-7400 modem", "8": "P4dragon DR-7800 modem"} + return ( + comment[1:-2], + {"manufacturer": "SCS GmbH & Co.", "device": devices.get(comment[-1], "Unknown")}, + ) if comment[-2:] == "~v": return comment[1:-2], {"manufacturer": "Other", "device": "Other"} return comment[1:-2], None @@ -233,7 +219,7 @@ def parse(self, data): if ord(destination[5]) >= ord("P"): lon *= -1 - comment = information[9:].decode('us-ascii').strip() + comment = information[9:].decode("us-ascii").strip() (comment, altitude) = self.extractAltitude(comment) (comment, device) = self.extractDevice(comment) @@ -242,11 +228,4 @@ def parse(self, data): (comment, insideAltitude) = self.extractAltitude(comment) altitude = next((a for a in [altitude, insideAltitude] if a is not None), None) - return { - "lat": lat, - "lon": lon, - "comment": comment, - "altitude": altitude, - "device": device, - "type": "Mic-E", - } + return {"lat": lat, "lon": lon, "comment": comment, "altitude": altitude, "device": device, "type": "Mic-E"} diff --git a/owrx/feature.py b/owrx/feature.py index 74b1a3712..7db8886aa 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -25,7 +25,7 @@ class FeatureDetector(object): "digital_voice_digiham": ["digiham", "sox"], "digital_voice_dsd": ["dsd", "sox", "digiham"], "wsjt-x": ["wsjtx", "sox"], - "packet": [ "direwolf" ], + "packet": ["direwolf"], } def feature_availability(self): diff --git a/owrx/kiss.py b/owrx/kiss.py index 8338d9189..7a401f2e7 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -7,7 +7,7 @@ FEND = 0xC0 FESC = 0xDB TFEND = 0xDC -TFESC = 0XDD +TFESC = 0xDD class KissClient(object): diff --git a/owrx/map.py b/owrx/map.py index a07fd0d1b..65cc2ca60 100644 --- a/owrx/map.py +++ b/owrx/map.py @@ -92,14 +92,15 @@ def removeOldPositions(self): class LatLngLocation(Location): - def __init__(self, lat: float, lon: float, comment = None): + def __init__(self, lat: float, lon: float, comment=None): self.lat = lat self.lon = lon self.comment = comment def __dict__(self): res = {"type": "latlon", "lat": self.lat, "lon": self.lon} - if self.comment is not None: res["comment"] = self.comment + if self.comment is not None: + res["comment"] = self.comment return res diff --git a/owrx/service.py b/owrx/service.py index c3d598e06..c2d41faca 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -27,7 +27,6 @@ def receive_output(self, t, read_fn): class WsjtServiceOutput(ServiceOutput): - def getParser(self): return WsjtParser(WsjtHandler()) @@ -36,7 +35,6 @@ def supports_type(self, t): class AprsServiceOutput(ServiceOutput): - def getParser(self): return AprsParser(AprsHandler()) From 66382eb50fded8738320588db31574b99dd3810d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 15 Aug 2019 20:28:24 +0200 Subject: [PATCH 0318/2616] add symbol information --- owrx/aprs.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 13adb4a34..26ffc7ae9 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -79,12 +79,13 @@ def parseUncompressedCoordinates(self, raw): lon = int(raw[9:12]) + float(raw[12:17]) / 60 if raw[17] == "W": lon *= -1 - return {"lat": lat, "lon": lon, "symbol": raw[18]} + return {"lat": lat, "lon": lon, "symboltable": raw[8], "symbol": raw[18]} def parseCompressedCoordinates(self, raw): return { "lat": 90 - decodeBase91(raw[1:5]) / 380926, "lon": -180 + decodeBase91(raw[5:9]) / 190463, + "symboltable": raw[0], "symbol": raw[9], } @@ -228,4 +229,13 @@ def parse(self, data): (comment, insideAltitude) = self.extractAltitude(comment) altitude = next((a for a in [altitude, insideAltitude] if a is not None), None) - return {"lat": lat, "lon": lon, "comment": comment, "altitude": altitude, "device": device, "type": "Mic-E"} + return { + "lat": lat, + "lon": lon, + "comment": comment, + "altitude": altitude, + "device": device, + "type": "Mic-E", + "symboltable": chr(information[8]), + "symbol": chr(information[7]), + } From 3022406f63c3324815717329c37c06c7332b81a9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 15 Aug 2019 21:00:01 +0200 Subject: [PATCH 0319/2616] get the extra information out of compressed messages --- owrx/aprs.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 26ffc7ae9..be13f10c7 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -69,7 +69,7 @@ def parse(self, raw): except Exception: logger.exception("exception while parsing aprs data") - def hasCompressedCoordinatesx(self, raw): + def hasCompressedCoordinates(self, raw): return raw[0] == "/" or raw[0] == "\\" def parseUncompressedCoordinates(self, raw): @@ -113,10 +113,33 @@ def parseAprsData(self, data): return aprsData def parseRegularAprsData(self, information): - if self.hasCompressedCoordinatesx(information): + if self.hasCompressedCoordinates(information): aprsData = self.parseCompressedCoordinates(information[0:10]) aprsData["type"] = "compressed" - aprsData["comment"] = information[10:] + if information[10] != " ": + if information[10] == "{": + # pre-calculated radio range + aprsData["range"] = 2 * 1.08 ** (information[11] - 33) + else: + aprsData["course"] = (information[10] - 33) * 4 + aprsData["speed"] = 1.08 ** (information[11] - 33) - 1 + # compression type + t = information[12] + aprsData["fix"] = (t & 0b00100000) > 0 + sources = ["other", "GLL", "GGA", "RMC"] + aprsData["nmeasource"] = sources[(t & 0b00011000) >> 3] + origins = [ + "Compressed", + "TNC BText", + "Software", + "[tbd]", + "KPC3", + "Pico", + "Other tracker", + "Digipeater conversion", + ] + aprsData["compressionorigin"] = origins[t & 0b00000111] + aprsData["comment"] = information[13:] else: aprsData = self.parseUncompressedCoordinates(information[0:19]) aprsData["type"] = "regular" From cc6561bddac8ae1e7c02c4913ff35384b5ec70e2 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 15 Aug 2019 21:46:08 +0200 Subject: [PATCH 0320/2616] get course and speed and extended info from mic-e frames --- owrx/aprs.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index be13f10c7..9c059c45f 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -95,7 +95,7 @@ def parseAprsData(self, data): # forward some of the ax25 data aprsData = {"source": data["source"], "destination": data["destination"], "path": data["path"]} - if information[0] == 0x1C or information[0] == 0x60: + if information[0] == 0x1C or information[0] == ord("`") or information[0] == ord("'"): aprsData.update(MicEParser().parse(data)) return aprsData @@ -224,8 +224,8 @@ def parse(self, data): lat *= -1 logger.debug(lat) - logger.debug(information) + lon = information[1] - 28 if ord(destination[4]) >= ord("P"): lon += 100 @@ -243,6 +243,16 @@ def parse(self, data): if ord(destination[5]) >= ord("P"): lon *= -1 + speed = (information[4] - 28) * 10 + dc28 = information[5] - 28 + speed += int(dc28 / 10) + course = (dc28 % 10) * 100 + course += information[6] - 28 + if speed >= 800: + speed -= 800 + if course >= 400: + course -= 400 + comment = information[9:].decode("us-ascii").strip() (comment, altitude) = self.extractAltitude(comment) @@ -253,10 +263,13 @@ def parse(self, data): altitude = next((a for a in [altitude, insideAltitude] if a is not None), None) return { + "fix": information[0] == ord("`") or information[0] == 0x1c, "lat": lat, "lon": lon, "comment": comment, "altitude": altitude, + "speed": speed, + "course": course, "device": device, "type": "Mic-E", "symboltable": chr(information[8]), From 46ac0ecc772819d8d84e390bdd3ec92821888484 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 15 Aug 2019 22:10:58 +0200 Subject: [PATCH 0321/2616] convert speed to metric --- owrx/aprs.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 9c059c45f..1bdbdf400 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -10,6 +10,9 @@ def decodeBase91(input): base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 return base + (ord(input[-1]) - 33) +# speed is in knots... convert to metric (km/h) +speedConversionFactor = 1.852 + class Ax25Parser(object): def parse(self, ax25frame): @@ -122,7 +125,8 @@ def parseRegularAprsData(self, information): aprsData["range"] = 2 * 1.08 ** (information[11] - 33) else: aprsData["course"] = (information[10] - 33) * 4 - aprsData["speed"] = 1.08 ** (information[11] - 33) - 1 + # speed is in knots... convert to metric (km/h) + aprsData["speed"] = (1.08 ** (information[11] - 33) - 1) * speedConversionFactor # compression type t = information[12] aprsData["fix"] = (t & 0b00100000) > 0 @@ -252,6 +256,8 @@ def parse(self, data): speed -= 800 if course >= 400: course -= 400 + # speed is in knots... convert to metric (km/h) + speed *= speedConversionFactor comment = information[9:].decode("us-ascii").strip() (comment, altitude) = self.extractAltitude(comment) From 5a7ef65c56f745f1a95c2cb20f2576a7fd102d2b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 15 Aug 2019 23:33:02 +0200 Subject: [PATCH 0322/2616] reduce debugging output --- owrx/aprs.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 1bdbdf400..d07cfc7b7 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -61,7 +61,7 @@ def parse(self, raw): # TODO how can we tell if this is an APRS frame at all? aprsData = self.parseAprsData(data) - logger.debug(aprsData) + logger.debug("decoded APRS data: %s", aprsData) if "lat" in aprsData and "lon" in aprsData: loc = LatLngLocation( aprsData["lat"], aprsData["lon"], aprsData["comment"] if "comment" in aprsData else None @@ -103,7 +103,6 @@ def parseAprsData(self, data): return aprsData information = information.decode("us-ascii") - logger.debug(information) if information[0] == "!" or information[0] == "=": # position without timestamp @@ -220,16 +219,11 @@ def parse(self, data): information = data["data"] destination = data["destination"] - logger.debug(destination) rawLatitude = [self.extractNumber(c) for c in destination[0:6]] - logger.debug(rawLatitude) lat = self.listToNumber(rawLatitude[0:2]) + self.listToNumber(rawLatitude[2:6]) / 6000 if ord(destination[3]) <= ord("9"): lat *= -1 - logger.debug(lat) - logger.debug(information) - lon = information[1] - 28 if ord(destination[4]) >= ord("P"): lon += 100 From cc66ffd6f303eaa1781e42560281d45db3ae16dd Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 16 Aug 2019 01:27:03 +0200 Subject: [PATCH 0323/2616] use generated port numbers for direwolf, allowing multiple instances --- csdr.py | 20 ++++++++++++++++++-- owrx/kiss.py | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/csdr.py b/csdr.py index a896e9b84..d55bf0fcf 100644 --- a/csdr.py +++ b/csdr.py @@ -235,7 +235,7 @@ def secondary_chain(self, which): chain = secondary_chain_base + "csdr fmdemod_quadri_cf | " if self.last_decimation != 1.0: chain += "csdr fractional_decimator_ff {last_decimation} | " - chain += "csdr convert_f_s16 | direwolf -r {audio_rate} -t 0 - 1>&2" + chain += "csdr convert_f_s16 | direwolf -c {direwolf_config} -r {audio_rate} -t 0 - 1>&2" return chain def set_secondary_demodulator(self, what): @@ -278,6 +278,7 @@ def start_secondary_demodulator(self): logger.debug("starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate()) secondary_command_demod = self.secondary_chain(self.secondary_demodulator) self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod) + self.try_create_configs(secondary_command_demod) secondary_command_demod = secondary_command_demod.format( input_pipe=self.iqtee2_pipe, @@ -289,6 +290,7 @@ def start_secondary_demodulator(self): if_samp_rate=self.if_samp_rate(), last_decimation=self.last_decimation, audio_rate=self.get_audio_rate(), + direwolf_config=self.direwolf_config, ) logger.debug("secondary command (demod) = %s", secondary_command_demod) @@ -337,7 +339,7 @@ def start_secondary_demodulator(self): self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) if self.isPacket(): - kiss = KissClient(8001) + kiss = KissClient(self.direwolf_port) self.output.send_output("packet_demod", kiss.read) # open control pipes for csdr and send initialization data @@ -548,6 +550,20 @@ def try_delete_pipes(self, pipe_names): except Exception: logger.exception("try_delete_pipes()") + def try_create_configs(self, command): + if "{direwolf_config}" in command: + self.direwolf_config = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(tmp_dir=self.temporary_directory, myid=id(self)) + self.direwolf_port = KissClient.getFreePort() + file = open(self.direwolf_config, "w") + file.write(""" +MODEM 1200 +KISSPORT {port} +AGWPORT off + """.format(port=self.direwolf_port)) + file.close() + else: + self.direwolf_config = None + def start(self): self.modification_lock.acquire() if self.running: diff --git a/owrx/kiss.py b/owrx/kiss.py index 7a401f2e7..c03671584 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -1,6 +1,7 @@ import socket import time import logging +import random logger = logging.getLogger(__name__) @@ -11,6 +12,21 @@ class KissClient(object): + @staticmethod + def getFreePort(): + # direwolf has some strange hardcoded port ranges + while True: + try: + port = random.randrange(1024, 49151) + # test if port is available for use + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(('localhost', port)) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.close() + return port + except OSError: + pass + def __init__(self, port): time.sleep(1) self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) From b40af9bbdc71b73de8352ebfc7b5c596f7361cc4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 16 Aug 2019 07:29:31 +0200 Subject: [PATCH 0324/2616] back to utf-8 --- owrx/aprs.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index d07cfc7b7..c6bdb80bd 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -13,6 +13,9 @@ def decodeBase91(input): # speed is in knots... convert to metric (km/h) speedConversionFactor = 1.852 +# not sure what the correct encoding is. it seems TAPR has set utf-8 as a standard, but not everybody is following it. +encoding = "utf-8" + class Ax25Parser(object): def parse(self, ax25frame): @@ -33,7 +36,7 @@ def chunks(l, n): } def extractCallsign(self, input): - cs = bytes([b >> 1 for b in input[0:6]]).decode("us-ascii").strip() + cs = bytes([b >> 1 for b in input[0:6]]).decode(encoding).strip() ssid = (input[6] & 0b00011110) >> 1 if ssid > 0: return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid) @@ -102,7 +105,7 @@ def parseAprsData(self, data): aprsData.update(MicEParser().parse(data)) return aprsData - information = information.decode("us-ascii") + information = information.decode(encoding) if information[0] == "!" or information[0] == "=": # position without timestamp @@ -253,7 +256,7 @@ def parse(self, data): # speed is in knots... convert to metric (km/h) speed *= speedConversionFactor - comment = information[9:].decode("us-ascii").strip() + comment = information[9:].decode(encoding).strip() (comment, altitude) = self.extractAltitude(comment) (comment, device) = self.extractDevice(comment) From 67f3dc7430cbfd9ff438a862800510d790850fa9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 16 Aug 2019 16:43:16 +0200 Subject: [PATCH 0325/2616] fix conversion errors --- owrx/aprs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index c6bdb80bd..848c2e5e8 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -124,13 +124,13 @@ def parseRegularAprsData(self, information): if information[10] != " ": if information[10] == "{": # pre-calculated radio range - aprsData["range"] = 2 * 1.08 ** (information[11] - 33) + aprsData["range"] = 2 * 1.08 ** (ord(information[11]) - 33) else: - aprsData["course"] = (information[10] - 33) * 4 + aprsData["course"] = (ord(information[10]) - 33) * 4 # speed is in knots... convert to metric (km/h) - aprsData["speed"] = (1.08 ** (information[11] - 33) - 1) * speedConversionFactor + aprsData["speed"] = (1.08 ** (ord(information[11]) - 33) - 1) * speedConversionFactor # compression type - t = information[12] + t = ord(information[12]) aprsData["fix"] = (t & 0b00100000) > 0 sources = ["other", "GLL", "GGA", "RMC"] aprsData["nmeasource"] = sources[(t & 0b00011000) >> 3] From 5b72728aa28f9c2e26440f0abdc2084a4791fdf1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 17 Aug 2019 13:39:02 +0200 Subject: [PATCH 0326/2616] timestamps, status updates, replace faulty characters --- owrx/aprs.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 848c2e5e8..ba4f50892 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -1,6 +1,7 @@ from owrx.kiss import KissDeframer from owrx.map import Map, LatLngLocation from owrx.bands import Bandplan +from datetime import datetime, timezone import logging logger = logging.getLogger(__name__) @@ -36,7 +37,7 @@ def chunks(l, n): } def extractCallsign(self, input): - cs = bytes([b >> 1 for b in input[0:6]]).decode(encoding).strip() + cs = bytes([b >> 1 for b in input[0:6]]).decode(encoding, "replace").strip() ssid = (input[6] & 0b00011110) >> 1 if ssid > 0: return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid) @@ -95,6 +96,32 @@ def parseCompressedCoordinates(self, raw): "symbol": raw[9], } + def parseTimestamp(self, raw): + now = datetime.now() + if raw[6] == "h": + ts = datetime.strptime(raw[0:6], "%H%M%S") + ts = ts.replace(year=now.year, month=now.month, day=now.month, tzinfo=timezone.utc) + else: + ts = datetime.strptime(raw[0:6], "%d%H%M") + ts = ts.replace(year=now.year, month=now.month) + if raw[6] == "z": + ts = ts.replace(tzinfo=timezone.utc) + elif raw[6] == "/": + ts = ts.replace(tzinfo=now.tzinfo) + else: + logger.warning("invalid timezone info byte: %s", raw[6]) + logger.debug(ts) + return int(ts.timestamp() * 1000) + + def parseStatusUpate(self, raw): + res = {"type": "status"} + if raw[6] == "z": + res["timestamp"] = self.parseTimestamp(raw[0:7]) + res["comment"] = raw[7:] + else: + res["comment"] = raw + return res + def parseAprsData(self, data): information = data["data"] @@ -105,15 +132,18 @@ def parseAprsData(self, data): aprsData.update(MicEParser().parse(data)) return aprsData - information = information.decode(encoding) + information = information.decode(encoding, "replace") if information[0] == "!" or information[0] == "=": # position without timestamp aprsData.update(self.parseRegularAprsData(information[1:])) elif information[0] == "/" or information[0] == "@": # position with timestamp - # TODO parse timestamp + aprsData["timestamp"] = self.parseTimestamp(information[1:8]) aprsData.update(self.parseRegularAprsData(information[8:])) + elif information[0] == ">": + # status update + aprsData.update(self.parseStatusUpate(information[1:])) return aprsData @@ -256,7 +286,7 @@ def parse(self, data): # speed is in knots... convert to metric (km/h) speed *= speedConversionFactor - comment = information[9:].decode(encoding).strip() + comment = information[9:].decode(encoding, "replace").strip() (comment, altitude) = self.extractAltitude(comment) (comment, device) = self.extractDevice(comment) From cf45caa76250252dacd89ac2852ef553ba45a658 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 17 Aug 2019 19:59:58 +0200 Subject: [PATCH 0327/2616] fix piping stuff for packet --- csdr.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/csdr.py b/csdr.py index d55bf0fcf..0b5f53d74 100644 --- a/csdr.py +++ b/csdr.py @@ -316,8 +316,12 @@ def start_secondary_demodulator(self): partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())), ) + # direwolf does not provide any meaningful data on stdout + # more specifically, it doesn't provide any data. if however, for any strange reason, it would start to do so, + # it would block if not read. by piping it to devnull, we avoid a potential pitfall here. + secondary_output = subprocess.DEVNULL if self.isPacket() else subprocess.PIPE self.secondary_process_demod = subprocess.Popen( - secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env + secondary_command_demod, stdout=secondary_output, shell=True, preexec_fn=os.setpgrp, env=my_env ) self.secondary_processes_running = True @@ -335,12 +339,12 @@ def start_secondary_demodulator(self): chopper = Ft4Chopper(self.secondary_process_demod.stdout) chopper.start() self.output.send_output("wsjt_demod", chopper.read) - else: - self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) - - if self.isPacket(): + elif self.isPacket(): + # we best get the ax25 packets from the kiss socket kiss = KissClient(self.direwolf_port) self.output.send_output("packet_demod", kiss.read) + else: + self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) # open control pipes for csdr and send initialization data if self.secondary_shift_pipe != None: # TODO digimodes From 34a8311647406896bfc6445778c5c0359c5ea9cb Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 17 Aug 2019 20:00:57 +0200 Subject: [PATCH 0328/2616] remove annoying debugging line --- owrx/http.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/owrx/http.py b/owrx/http.py index ce96acc16..189dd9585 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -64,9 +64,6 @@ def route(self, handler): if res is not None: (controller, matches) = res query = parse_qs(url.query) - logger.debug( - "path: {0}, controller: {1}, query: {2}, matches: {3}".format(handler.path, controller, query, matches) - ) request = Request(query, matches) controller(handler, request).handle_request() else: From bdbe45e322e84068ff98aa583e5a000a98b261e4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 17 Aug 2019 20:01:12 +0200 Subject: [PATCH 0329/2616] recognize third party data (don't think we can parse them) --- owrx/aprs.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/owrx/aprs.py b/owrx/aprs.py index ba4f50892..b30aa58ce 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -144,6 +144,9 @@ def parseAprsData(self, data): elif information[0] == ">": # status update aprsData.update(self.parseStatusUpate(information[1:])) + elif information[0] == "}": + # third party + aprsData["type"] = "thirdparty" return aprsData From 1eb28d6aee185f101503e42247ada3a5d5829e54 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 17 Aug 2019 20:20:28 +0200 Subject: [PATCH 0330/2616] optimize --- owrx/aprs.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index b30aa58ce..97b8a5b93 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -134,17 +134,20 @@ def parseAprsData(self, data): information = information.decode(encoding, "replace") - if information[0] == "!" or information[0] == "=": + # APRS data type identifier + dti = information[0] + + if dti == "!" or dti == "=": # position without timestamp aprsData.update(self.parseRegularAprsData(information[1:])) - elif information[0] == "/" or information[0] == "@": + elif dti == "/" or dti == "@": # position with timestamp aprsData["timestamp"] = self.parseTimestamp(information[1:8]) aprsData.update(self.parseRegularAprsData(information[8:])) - elif information[0] == ">": + elif dti == ">": # status update aprsData.update(self.parseStatusUpate(information[1:])) - elif information[0] == "}": + elif dti == "}": # third party aprsData["type"] = "thirdparty" From 82eaff5da64da93e31dc3480f04737b950f84c03 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 17 Aug 2019 20:35:32 +0200 Subject: [PATCH 0331/2616] get altitude from comment --- owrx/aprs.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 97b8a5b93..91232c414 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -2,21 +2,27 @@ from owrx.map import Map, LatLngLocation from owrx.bands import Bandplan from datetime import datetime, timezone +import re import logging logger = logging.getLogger(__name__) -def decodeBase91(input): - base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 - return base + (ord(input[-1]) - 33) - # speed is in knots... convert to metric (km/h) -speedConversionFactor = 1.852 +knotsToKilometers = 1.852 +feetToMeters = 0.3048 # not sure what the correct encoding is. it seems TAPR has set utf-8 as a standard, but not everybody is following it. encoding = "utf-8" +# regex for altitute in comment field +altitudeRegex = re.compile("(^.*)\\/A=([0-9]{6})(.*$)") + + +def decodeBase91(input): + base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 + return base + (ord(input[-1]) - 33) + class Ax25Parser(object): def parse(self, ax25frame): @@ -164,7 +170,7 @@ def parseRegularAprsData(self, information): else: aprsData["course"] = (ord(information[10]) - 33) * 4 # speed is in knots... convert to metric (km/h) - aprsData["speed"] = (1.08 ** (ord(information[11]) - 33) - 1) * speedConversionFactor + aprsData["speed"] = (1.08 ** (ord(information[11]) - 33) - 1) * knotsToKilometers # compression type t = ord(information[12]) aprsData["fix"] = (t & 0b00100000) > 0 @@ -186,6 +192,12 @@ def parseRegularAprsData(self, information): aprsData = self.parseUncompressedCoordinates(information[0:19]) aprsData["type"] = "regular" aprsData["comment"] = information[19:] + + matches = altitudeRegex.match(aprsData["comment"]) + if matches: + aprsData["altitude"] = int(matches[2]) * feetToMeters + aprsData["comment"] = matches[1] + matches[3] + return aprsData @@ -290,7 +302,7 @@ def parse(self, data): if course >= 400: course -= 400 # speed is in knots... convert to metric (km/h) - speed *= speedConversionFactor + speed *= knotsToKilometers comment = information[9:].decode(encoding, "replace").strip() (comment, altitude) = self.extractAltitude(comment) From 7e757c005c2ab443efb6a2d91b4cc4eb96b72e2d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 17 Aug 2019 22:04:45 +0200 Subject: [PATCH 0332/2616] implement aprs data extensions --- owrx/aprs.py | 51 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 91232c414..ac48d556c 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -11,6 +11,7 @@ # speed is in knots... convert to metric (km/h) knotsToKilometers = 1.852 feetToMeters = 0.3048 +milesToKilometers = 1.609344 # not sure what the correct encoding is. it seems TAPR has set utf-8 as a standard, but not everybody is following it. encoding = "utf-8" @@ -166,7 +167,7 @@ def parseRegularAprsData(self, information): if information[10] != " ": if information[10] == "{": # pre-calculated radio range - aprsData["range"] = 2 * 1.08 ** (ord(information[11]) - 33) + aprsData["range"] = 2 * 1.08 ** (ord(information[11]) - 33) * milesToKilometers else: aprsData["course"] = (ord(information[10]) - 33) * 4 # speed is in knots... convert to metric (km/h) @@ -187,16 +188,54 @@ def parseRegularAprsData(self, information): "Digipeater conversion", ] aprsData["compressionorigin"] = origins[t & 0b00000111] - aprsData["comment"] = information[13:] + comment = information[13:] else: aprsData = self.parseUncompressedCoordinates(information[0:19]) aprsData["type"] = "regular" - aprsData["comment"] = information[19:] - - matches = altitudeRegex.match(aprsData["comment"]) + comment = information[19:] + + def decodeHeightGainDirectivity(comment): + res = { + "height": 2 ** int(comment[4]) * 10 * feetToMeters, + "gain": int(comment[5]), + } + directivity = int(comment[6]) + if directivity == 0: + res["directivity"] = "omni" + elif 0 < directivity < 9: + res["directivity"] = directivity * 45 + return res + + # aprs data extensions + if len(comment) > 6: + if comment[3] == "/": + # course and speed + # for a weather report, this would be wind direction and speed + aprsData["course"] = int(comment[0:3]) + aprsData["speed"] = int(comment[4:7]) * knotsToKilometers + comment = comment[7:] + elif comment[0:3] == "PHG": + # station power and effective antenna height/gain/directivity + powerCodes = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + aprsData["power"] = powerCodes[int(comment[3])] + aprsData.update(decodeHeightGainDirectivity(comment)) + comment = comment[7:] + elif comment[0:3] == "RNG": + # pre-calculated radio range + aprsData["range"] = int(comment[3:7]) * milesToKilometers + comment = comment[7:] + elif comment[0:3] == "DFS": + # direction finding signal strength and antenna height/gain + aprsData["strength"] = int(comment[3]) + aprsData.update(decodeHeightGainDirectivity(comment)) + comment = comment[7:] + + matches = altitudeRegex.match(comment) if matches: aprsData["altitude"] = int(matches[2]) * feetToMeters - aprsData["comment"] = matches[1] + matches[3] + comment = matches[1] + matches[3] + + aprsData["comment"] = comment return aprsData From 54bcba195d71b2bc79da917ef22a51603978d978 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 17 Aug 2019 22:38:09 +0200 Subject: [PATCH 0333/2616] delete configs after use --- csdr.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/csdr.py b/csdr.py index 0b5f53d74..40348be2a 100644 --- a/csdr.py +++ b/csdr.py @@ -108,6 +108,8 @@ def __init__(self, output): self.modification_lock = threading.Lock() self.output = output self.temporary_directory = "/tmp" + self.direwolf_config = None + self.direwolf_port = None def set_temporary_directory(self, what): self.temporary_directory = what @@ -361,6 +363,7 @@ def stop_secondary_demodulator(self): if self.secondary_processes_running == False: return self.try_delete_pipes(self.secondary_pipe_names) + self.try_delete_configs() if self.secondary_process_fft: try: os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM) @@ -567,6 +570,18 @@ def try_create_configs(self, command): file.close() else: self.direwolf_config = None + self.direwolf_port = None + + def try_delete_configs(self): + if self.direwolf_config: + try: + os.unlink(self.direwolf_config) + except FileNotFoundError: + # result suits our expectations. fine :) + pass + except Exception: + logger.exception("try_delete_configs()") + self.direwolf_config = None def start(self): self.modification_lock.acquire() From 5fab3e3d3666c3b4ba185808c604bec2334075a7 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 18 Aug 2019 00:15:07 +0200 Subject: [PATCH 0334/2616] add igate functionality --- config_webrx.py | 9 +++++++++ csdr.py | 8 ++------ owrx/kiss.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index a5cf0ad3a..11d8d30a6 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -254,3 +254,12 @@ services_enabled = False services_decoders = ["ft8", "ft4", "wspr", "packet"] + +# === aprs igate settings === +# if you want to share your APRS decodes with the aprs network, configure these settings accordingly +aprs_callsign = "N0CALL" +aprs_igate_enabled = False +aprs_igate_server = "euro.aprs2.net" +aprs_igate_password = "" +# beacon uses the receiver_gps setting, so if you enable this, make sure the location is correct there +aprs_igate_beacon = False diff --git a/csdr.py b/csdr.py index 40348be2a..4aedc8876 100644 --- a/csdr.py +++ b/csdr.py @@ -26,7 +26,7 @@ import threading from functools import partial -from owrx.kiss import KissClient +from owrx.kiss import KissClient, DirewolfConfig from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper import logging @@ -562,11 +562,7 @@ def try_create_configs(self, command): self.direwolf_config = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(tmp_dir=self.temporary_directory, myid=id(self)) self.direwolf_port = KissClient.getFreePort() file = open(self.direwolf_config, "w") - file.write(""" -MODEM 1200 -KISSPORT {port} -AGWPORT off - """.format(port=self.direwolf_port)) + file.write(DirewolfConfig().getConfig(self.direwolf_port)) file.close() else: self.direwolf_config = None diff --git a/owrx/kiss.py b/owrx/kiss.py index c03671584..67dc31992 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -2,6 +2,7 @@ import time import logging import random +from owrx.config import PropertyManager logger = logging.getLogger(__name__) @@ -11,6 +12,42 @@ TFESC = 0xDD +class DirewolfConfig(object): + def getConfig(self, port): + pm = PropertyManager.getSharedInstance() + + config = """ +MYCALL {callsign} +MODEM 1200 +KISSPORT {port} +AGWPORT off + """.format( + port=port, + callsign=pm["aprs_callsign"], + ) + + if pm["aprs_igate_enabled"]: + config += """ +IGSERVER {server} +IGLOGIN {callsign} {password} + """.format( + server=pm["aprs_igate_server"], + callsign=pm["aprs_callsign"], + password=pm["aprs_igate_password"], + ) + + if pm["aprs_igate_beacon"]: + (lat, lon) = pm["receiver_gps"] + lat = "{0}^{1:.2f}{2}".format(int(lat), (lat - int(lat)) * 60, "N" if lat > 0 else "S") + lon = "{0}^{1:.2f}{2}".format(int(lon), (lon - int(lon)) * 60, "E" if lon > 0 else "W") + + config += """ +PBEACON sendto=IG delay=0:30 every=60:00 symbol="igate" overlay=R lat={lat} long={lon} comment="OpenWebRX APRS gateway" + """.format(lat=lat, lon=lon) + + return config + + class KissClient(object): @staticmethod def getFreePort(): From 73102053dc2c0b7d557c1f3aeab71c20b8d86bb7 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 18 Aug 2019 00:16:08 +0200 Subject: [PATCH 0335/2616] code formatting --- csdr.py | 4 +++- owrx/aprs.py | 7 ++----- owrx/kiss.py | 13 ++++++------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/csdr.py b/csdr.py index 4aedc8876..f921239e2 100644 --- a/csdr.py +++ b/csdr.py @@ -559,7 +559,9 @@ def try_delete_pipes(self, pipe_names): def try_create_configs(self, command): if "{direwolf_config}" in command: - self.direwolf_config = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(tmp_dir=self.temporary_directory, myid=id(self)) + self.direwolf_config = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format( + tmp_dir=self.temporary_directory, myid=id(self) + ) self.direwolf_port = KissClient.getFreePort() file = open(self.direwolf_config, "w") file.write(DirewolfConfig().getConfig(self.direwolf_port)) diff --git a/owrx/aprs.py b/owrx/aprs.py index ac48d556c..084318e78 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -195,10 +195,7 @@ def parseRegularAprsData(self, information): comment = information[19:] def decodeHeightGainDirectivity(comment): - res = { - "height": 2 ** int(comment[4]) * 10 * feetToMeters, - "gain": int(comment[5]), - } + res = {"height": 2 ** int(comment[4]) * 10 * feetToMeters, "gain": int(comment[5])} directivity = int(comment[6]) if directivity == 0: res["directivity"] = "omni" @@ -353,7 +350,7 @@ def parse(self, data): altitude = next((a for a in [altitude, insideAltitude] if a is not None), None) return { - "fix": information[0] == ord("`") or information[0] == 0x1c, + "fix": information[0] == ord("`") or information[0] == 0x1C, "lat": lat, "lon": lon, "comment": comment, diff --git a/owrx/kiss.py b/owrx/kiss.py index 67dc31992..02f01c075 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -22,8 +22,7 @@ def getConfig(self, port): KISSPORT {port} AGWPORT off """.format( - port=port, - callsign=pm["aprs_callsign"], + port=port, callsign=pm["aprs_callsign"] ) if pm["aprs_igate_enabled"]: @@ -31,9 +30,7 @@ def getConfig(self, port): IGSERVER {server} IGLOGIN {callsign} {password} """.format( - server=pm["aprs_igate_server"], - callsign=pm["aprs_callsign"], - password=pm["aprs_igate_password"], + server=pm["aprs_igate_server"], callsign=pm["aprs_callsign"], password=pm["aprs_igate_password"] ) if pm["aprs_igate_beacon"]: @@ -43,7 +40,9 @@ def getConfig(self, port): config += """ PBEACON sendto=IG delay=0:30 every=60:00 symbol="igate" overlay=R lat={lat} long={lon} comment="OpenWebRX APRS gateway" - """.format(lat=lat, lon=lon) + """.format( + lat=lat, lon=lon + ) return config @@ -57,7 +56,7 @@ def getFreePort(): port = random.randrange(1024, 49151) # test if port is available for use s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind(('localhost', port)) + s.bind(("localhost", port)) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.close() return port From 94533e277c81df6262329f2ae378161d6d9fb714 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 18 Aug 2019 01:39:23 +0200 Subject: [PATCH 0336/2616] improve config --- owrx/kiss.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/owrx/kiss.py b/owrx/kiss.py index 02f01c075..82d21b187 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -17,8 +17,12 @@ def getConfig(self, port): pm = PropertyManager.getSharedInstance() config = """ +ACHANNELS 1 + +CHANNEL 0 MYCALL {callsign} MODEM 1200 + KISSPORT {port} AGWPORT off """.format( From f07bc9e6de2dd017df6aafc25ea2ca75bdeb3e4b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 18 Aug 2019 21:40:46 +0200 Subject: [PATCH 0337/2616] update wsjt-x version in docker build --- docker/scripts/install-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 2c7883e4e..7b8765e24 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -79,7 +79,7 @@ cmakebuild digiham git clone https://github.com/f4exb/dsd.git cmakebuild dsd -WSJT_DIR=wsjtx-2.0.1 +WSJT_DIR=wsjtx-2.1.0 WSJT_TGZ=${WSJT_DIR}.tgz wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ tar xvfz $WSJT_TGZ From 379251d29d66318b37246d49211661353fec8b41 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 18 Aug 2019 21:41:26 +0200 Subject: [PATCH 0338/2616] filter smallest possible to avoid traffic from the network --- owrx/kiss.py | 1 + 1 file changed, 1 insertion(+) diff --git a/owrx/kiss.py b/owrx/kiss.py index 82d21b187..094b4465f 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -33,6 +33,7 @@ def getConfig(self, port): config += """ IGSERVER {server} IGLOGIN {callsign} {password} +IGFILTER m/1 """.format( server=pm["aprs_igate_server"], callsign=pm["aprs_callsign"], password=pm["aprs_igate_password"] ) From e4ef364aa87aa0aaa2718200e0aaa67a11527136 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 18 Aug 2019 22:03:41 +0000 Subject: [PATCH 0339/2616] looks like we have some additional dependencies now --- docker/scripts/install-dependencies.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 2c7883e4e..972de0303 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -14,8 +14,8 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport" -BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev asciidoctor asciidoc" +STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport qt5-qttools" +BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc" apk add --no-cache $STATIC_PACKAGES apk add --no-cache --virtual .build-deps $BUILD_PACKAGES @@ -79,7 +79,7 @@ cmakebuild digiham git clone https://github.com/f4exb/dsd.git cmakebuild dsd -WSJT_DIR=wsjtx-2.0.1 +WSJT_DIR=wsjtx-2.1.0 WSJT_TGZ=${WSJT_DIR}.tgz wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ tar xvfz $WSJT_TGZ From c5cc36491842acefbff7632af43a0b78fce66ba6 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 22 Aug 2019 20:51:09 +0200 Subject: [PATCH 0340/2616] filters don't seem to work --- owrx/kiss.py | 1 - 1 file changed, 1 deletion(-) diff --git a/owrx/kiss.py b/owrx/kiss.py index 094b4465f..82d21b187 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -33,7 +33,6 @@ def getConfig(self, port): config += """ IGSERVER {server} IGLOGIN {callsign} {password} -IGFILTER m/1 """.format( server=pm["aprs_igate_server"], callsign=pm["aprs_callsign"], password=pm["aprs_igate_password"] ) From faaef9d9f8b6341bab23bf187aa9518e0b6d4504 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 22 Aug 2019 20:51:36 +0200 Subject: [PATCH 0341/2616] let's be nice --- owrx/wsjt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index e18257e8e..4542cf8ad 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -78,7 +78,7 @@ def decoder_commandline(self, file): def decode(self): def decode_and_unlink(file): - decoder = subprocess.Popen(self.decoder_commandline(file), stdout=subprocess.PIPE, cwd=self.tmp_dir) + decoder = subprocess.Popen(self.decoder_commandline(file), stdout=subprocess.PIPE, cwd=self.tmp_dir, preexec_fn=lambda : os.nice(10)) while True: line = decoder.stdout.readline() if line is None or (isinstance(line, bytes) and len(line) == 0): From 24d134ad6cd39f36a3bed2270193d8adbf92b98c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 22 Aug 2019 21:16:43 +0200 Subject: [PATCH 0342/2616] try to avoid stressing out the cpu by using a proper queue --- owrx/wsjt.py | 69 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 4542cf8ad..7bd3c5cfd 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -8,6 +8,7 @@ from multiprocessing.connection import Pipe from owrx.map import Map, LocatorLocation import re +from queue import Queue from owrx.config import PropertyManager from owrx.bands import Bandplan from owrx.metrics import Metrics @@ -17,6 +18,42 @@ logger = logging.getLogger(__name__) +class WsjtQueueWorker(threading.Thread): + def __init__(self, queue): + self.queue = queue + self.doRun = True + super().__init__(daemon=True) + + def run(self) -> None: + while self.doRun: + (processor, file) = self.queue.get() + logger.debug("processing file %s", file) + processor.decode(file) + self.queue.task_done() + + +class WsjtQueue(Queue): + sharedInstance = None + + @staticmethod + def getSharedInstance(): + if WsjtQueue.sharedInstance is None: + WsjtQueue.sharedInstance = WsjtQueue(maxsize=10, workers=2) + return WsjtQueue.sharedInstance + + def __init__(self, maxsize, workers): + super().__init__(maxsize) + self.workers = [self.newWorker() for _ in range(0, workers)] + + def put(self, item): + super(WsjtQueue, self).put(item, block=False) + + def newWorker(self): + worker = WsjtQueueWorker(self) + worker.start() + return worker + + class WsjtChopper(threading.Thread): def __init__(self, source): self.source = source @@ -24,7 +61,6 @@ def __init__(self, source): (self.wavefilename, self.wavefile) = self.getWaveFile() self.switchingLock = threading.Lock() self.scheduler = sched.scheduler(time.time, time.sleep) - self.fileQueue = [] (self.outputReader, self.outputWriter) = Pipe() self.doRun = True super().__init__() @@ -67,7 +103,7 @@ def switchFiles(self): self.switchingLock.release() file.close() - self.fileQueue.append(filename) + WsjtQueue.getSharedInstance().put((self, filename)) self._scheduleNextSwitch() def decoder_commandline(self, file): @@ -76,23 +112,17 @@ def decoder_commandline(self, file): """ return [] - def decode(self): - def decode_and_unlink(file): - decoder = subprocess.Popen(self.decoder_commandline(file), stdout=subprocess.PIPE, cwd=self.tmp_dir, preexec_fn=lambda : os.nice(10)) - while True: - line = decoder.stdout.readline() - if line is None or (isinstance(line, bytes) and len(line) == 0): - break - self.outputWriter.send(line) - rc = decoder.wait() - if rc != 0: - logger.warning("decoder return code: %i", rc) - os.unlink(file) - - if self.fileQueue: - file = self.fileQueue.pop() - logger.debug("processing file {0}".format(file)) - threading.Thread(target=decode_and_unlink, args=[file]).start() + def decode(self, file): + decoder = subprocess.Popen(self.decoder_commandline(file), stdout=subprocess.PIPE, cwd=self.tmp_dir, preexec_fn=lambda : os.nice(10)) + while True: + line = decoder.stdout.readline() + if line is None or (isinstance(line, bytes) and len(line) == 0): + break + self.outputWriter.send(line) + rc = decoder.wait() + if rc != 0: + logger.warning("decoder return code: %i", rc) + os.unlink(file) def run(self) -> None: logger.debug("WSJT chopper starting up") @@ -106,7 +136,6 @@ def run(self) -> None: self.wavefile.writeframes(data) self.switchingLock.release() - self.decode() logger.debug("WSJT chopper shutting down") self.outputReader.close() self.outputWriter.close() From fadcb9b43f5f55c847f0747266644672f9a42bd1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 22 Aug 2019 21:24:36 +0200 Subject: [PATCH 0343/2616] handle a full queue --- owrx/wsjt.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 7bd3c5cfd..34f7f4fb0 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -8,7 +8,7 @@ from multiprocessing.connection import Pipe from owrx.map import Map, LocatorLocation import re -from queue import Queue +from queue import Queue, Full from owrx.config import PropertyManager from owrx.bands import Bandplan from owrx.metrics import Metrics @@ -103,7 +103,11 @@ def switchFiles(self): self.switchingLock.release() file.close() - WsjtQueue.getSharedInstance().put((self, filename)) + try: + WsjtQueue.getSharedInstance().put((self, filename)) + except Full: + logger.warning("wsjt decoding queue overflow; dropping one file") + os.unlink(filename) self._scheduleNextSwitch() def decoder_commandline(self, file): From 62e9a395577e189d72f5864ba15d40c4357bfd54 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 23 Aug 2019 22:21:30 +0200 Subject: [PATCH 0344/2616] add direwolf to docker build --- docker/Dockerfiles/Dockerfile-base | 1 + docker/scripts/direwolf-1.5.patch | 241 +++++++++++++++++++++++++ docker/scripts/install-dependencies.sh | 13 +- 3 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 docker/scripts/direwolf-1.5.patch diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base index f58d41e88..4dbca2fb1 100644 --- a/docker/Dockerfiles/Dockerfile-base +++ b/docker/Dockerfiles/Dockerfile-base @@ -3,6 +3,7 @@ FROM $BASE_IMAGE RUN apk add --no-cache bash +ADD docker/scripts/direwolf-1.5.patch / ADD docker/scripts/install-dependencies.sh / RUN /install-dependencies.sh diff --git a/docker/scripts/direwolf-1.5.patch b/docker/scripts/direwolf-1.5.patch new file mode 100644 index 000000000..4a63915b2 --- /dev/null +++ b/docker/scripts/direwolf-1.5.patch @@ -0,0 +1,241 @@ +diff --git a/Makefile.linux b/Makefile.linux +index 5010833..3f61de9 100644 +--- a/Makefile.linux ++++ b/Makefile.linux +@@ -585,102 +585,102 @@ install : $(APPS) direwolf.conf tocalls.txt symbols-new.txt symbolsX.txt dw-icon + # Applications, not installed with package manager, normally go in /usr/local/bin. + # /usr/bin is used instead when installing from .DEB or .RPM package. + # +- $(INSTALL) -D --mode=755 direwolf $(DESTDIR)/bin/direwolf +- $(INSTALL) -D --mode=755 decode_aprs $(DESTDIR)/bin/decode_aprs +- $(INSTALL) -D --mode=755 text2tt $(DESTDIR)/bin/text2tt +- $(INSTALL) -D --mode=755 tt2text $(DESTDIR)/bin/tt2text +- $(INSTALL) -D --mode=755 ll2utm $(DESTDIR)/bin/ll2utm +- $(INSTALL) -D --mode=755 utm2ll $(DESTDIR)/bin/utm2ll +- $(INSTALL) -D --mode=755 aclients $(DESTDIR)/bin/aclients +- $(INSTALL) -D --mode=755 log2gpx $(DESTDIR)/bin/log2gpx +- $(INSTALL) -D --mode=755 gen_packets $(DESTDIR)/bin/gen_packets +- $(INSTALL) -D --mode=755 atest $(DESTDIR)/bin/atest +- $(INSTALL) -D --mode=755 ttcalc $(DESTDIR)/bin/ttcalc +- $(INSTALL) -D --mode=755 kissutil $(DESTDIR)/bin/kissutil +- $(INSTALL) -D --mode=755 cm108 $(DESTDIR)/bin/cm108 +- $(INSTALL) -D --mode=755 dwespeak.sh $(DESTDIR)/bin/dwspeak.sh ++ $(INSTALL) -D -m=755 direwolf $(DESTDIR)/bin/direwolf ++ $(INSTALL) -D -m=755 decode_aprs $(DESTDIR)/bin/decode_aprs ++ $(INSTALL) -D -m=755 text2tt $(DESTDIR)/bin/text2tt ++ $(INSTALL) -D -m=755 tt2text $(DESTDIR)/bin/tt2text ++ $(INSTALL) -D -m=755 ll2utm $(DESTDIR)/bin/ll2utm ++ $(INSTALL) -D -m=755 utm2ll $(DESTDIR)/bin/utm2ll ++ $(INSTALL) -D -m=755 aclients $(DESTDIR)/bin/aclients ++ $(INSTALL) -D -m=755 log2gpx $(DESTDIR)/bin/log2gpx ++ $(INSTALL) -D -m=755 gen_packets $(DESTDIR)/bin/gen_packets ++ $(INSTALL) -D -m=755 atest $(DESTDIR)/bin/atest ++ $(INSTALL) -D -m=755 ttcalc $(DESTDIR)/bin/ttcalc ++ $(INSTALL) -D -m=755 kissutil $(DESTDIR)/bin/kissutil ++ $(INSTALL) -D -m=755 cm108 $(DESTDIR)/bin/cm108 ++ $(INSTALL) -D -m=755 dwespeak.sh $(DESTDIR)/bin/dwspeak.sh + # + # Telemetry Toolkit executables. Other .conf and .txt files will go into doc directory. + # +- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-balloon.pl $(DESTDIR)/bin/telem-balloon.pl +- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-bits.pl $(DESTDIR)/bin/telem-bits.pl +- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-data.pl $(DESTDIR)/bin/telem-data.pl +- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-data91.pl $(DESTDIR)/bin/telem-data91.pl +- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-eqns.pl $(DESTDIR)/bin/telem-eqns.pl +- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-parm.pl $(DESTDIR)/bin/telem-parm.pl +- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-seq.sh $(DESTDIR)/bin/telem-seq.sh +- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-unit.pl $(DESTDIR)/bin/telem-unit.pl +- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-volts.py $(DESTDIR)/bin/telem-volts.py ++ $(INSTALL) -D -m=755 telemetry-toolkit/telem-balloon.pl $(DESTDIR)/bin/telem-balloon.pl ++ $(INSTALL) -D -m=755 telemetry-toolkit/telem-bits.pl $(DESTDIR)/bin/telem-bits.pl ++ $(INSTALL) -D -m=755 telemetry-toolkit/telem-data.pl $(DESTDIR)/bin/telem-data.pl ++ $(INSTALL) -D -m=755 telemetry-toolkit/telem-data91.pl $(DESTDIR)/bin/telem-data91.pl ++ $(INSTALL) -D -m=755 telemetry-toolkit/telem-eqns.pl $(DESTDIR)/bin/telem-eqns.pl ++ $(INSTALL) -D -m=755 telemetry-toolkit/telem-parm.pl $(DESTDIR)/bin/telem-parm.pl ++ $(INSTALL) -D -m=755 telemetry-toolkit/telem-seq.sh $(DESTDIR)/bin/telem-seq.sh ++ $(INSTALL) -D -m=755 telemetry-toolkit/telem-unit.pl $(DESTDIR)/bin/telem-unit.pl ++ $(INSTALL) -D -m=755 telemetry-toolkit/telem-volts.py $(DESTDIR)/bin/telem-volts.py + # + # Misc. data such as "tocall" to system mapping. + # +- $(INSTALL) -D --mode=644 tocalls.txt $(DESTDIR)/share/direwolf/tocalls.txt +- $(INSTALL) -D --mode=644 symbols-new.txt $(DESTDIR)/share/direwolf/symbols-new.txt +- $(INSTALL) -D --mode=644 symbolsX.txt $(DESTDIR)/share/direwolf/symbolsX.txt ++ $(INSTALL) -D -m=644 tocalls.txt $(DESTDIR)/share/direwolf/tocalls.txt ++ $(INSTALL) -D -m=644 symbols-new.txt $(DESTDIR)/share/direwolf/symbols-new.txt ++ $(INSTALL) -D -m=644 symbolsX.txt $(DESTDIR)/share/direwolf/symbolsX.txt + # + # For desktop icon. + # +- $(INSTALL) -D --mode=644 dw-icon.png $(DESTDIR)/share/direwolf/pixmaps/dw-icon.png +- $(INSTALL) -D --mode=644 direwolf.desktop $(DESTDIR)/share/applications/direwolf.desktop ++ $(INSTALL) -D -m=644 dw-icon.png $(DESTDIR)/share/direwolf/pixmaps/dw-icon.png ++ $(INSTALL) -D -m=644 direwolf.desktop $(DESTDIR)/share/applications/direwolf.desktop + # + # Documentation. Various plain text files and PDF. + # +- $(INSTALL) -D --mode=644 CHANGES.md $(DESTDIR)/share/doc/direwolf/CHANGES.md +- $(INSTALL) -D --mode=644 LICENSE-dire-wolf.txt $(DESTDIR)/share/doc/direwolf/LICENSE-dire-wolf.txt +- $(INSTALL) -D --mode=644 LICENSE-other.txt $(DESTDIR)/share/doc/direwolf/LICENSE-other.txt ++ $(INSTALL) -D -m=644 CHANGES.md $(DESTDIR)/share/doc/direwolf/CHANGES.md ++ $(INSTALL) -D -m=644 LICENSE-dire-wolf.txt $(DESTDIR)/share/doc/direwolf/LICENSE-dire-wolf.txt ++ $(INSTALL) -D -m=644 LICENSE-other.txt $(DESTDIR)/share/doc/direwolf/LICENSE-other.txt + # + # ./README.md is an overview for the project main page. + # Maybe we could stick it in some other place. + # doc/README.md contains an overview of the PDF file contents and is more useful here. + # +- $(INSTALL) -D --mode=644 doc/README.md $(DESTDIR)/share/doc/direwolf/README.md +- $(INSTALL) -D --mode=644 doc/2400-4800-PSK-for-APRS-Packet-Radio.pdf $(DESTDIR)/share/doc/direwolf/2400-4800-PSK-for-APRS-Packet-Radio.pdf +- $(INSTALL) -D --mode=644 doc/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf +- $(INSTALL) -D --mode=644 doc/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf +- $(INSTALL) -D --mode=644 doc/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf $(DESTDIR)/share/doc/direwolf/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf +- $(INSTALL) -D --mode=644 doc/APRS-Telemetry-Toolkit.pdf $(DESTDIR)/share/doc/direwolf/APRS-Telemetry-Toolkit.pdf +- $(INSTALL) -D --mode=644 doc/APRStt-Implementation-Notes.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Implementation-Notes.pdf +- $(INSTALL) -D --mode=644 doc/APRStt-interface-for-SARTrack.pdf $(DESTDIR)/share/doc/direwolf/APRStt-interface-for-SARTrack.pdf +- $(INSTALL) -D --mode=644 doc/APRStt-Listening-Example.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Listening-Example.pdf +- $(INSTALL) -D --mode=644 doc/Bluetooth-KISS-TNC.pdf $(DESTDIR)/share/doc/direwolf/Bluetooth-KISS-TNC.pdf +- $(INSTALL) -D --mode=644 doc/Going-beyond-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/Going-beyond-9600-baud.pdf +- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-APRS.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS.pdf +- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-APRS-Tracker.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS-Tracker.pdf +- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-SDR-IGate.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-SDR-IGate.pdf +- $(INSTALL) -D --mode=644 doc/Successful-APRS-IGate-Operation.pdf $(DESTDIR)/share/doc/direwolf/Successful-APRS-IGate-Operation.pdf +- $(INSTALL) -D --mode=644 doc/User-Guide.pdf $(DESTDIR)/share/doc/direwolf/User-Guide.pdf +- $(INSTALL) -D --mode=644 doc/WA8LMF-TNC-Test-CD-Results.pdf $(DESTDIR)/share/doc/direwolf/WA8LMF-TNC-Test-CD-Results.pdf ++ $(INSTALL) -D -m=644 doc/README.md $(DESTDIR)/share/doc/direwolf/README.md ++ $(INSTALL) -D -m=644 doc/2400-4800-PSK-for-APRS-Packet-Radio.pdf $(DESTDIR)/share/doc/direwolf/2400-4800-PSK-for-APRS-Packet-Radio.pdf ++ $(INSTALL) -D -m=644 doc/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf ++ $(INSTALL) -D -m=644 doc/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf ++ $(INSTALL) -D -m=644 doc/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf $(DESTDIR)/share/doc/direwolf/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf ++ $(INSTALL) -D -m=644 doc/APRS-Telemetry-Toolkit.pdf $(DESTDIR)/share/doc/direwolf/APRS-Telemetry-Toolkit.pdf ++ $(INSTALL) -D -m=644 doc/APRStt-Implementation-Notes.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Implementation-Notes.pdf ++ $(INSTALL) -D -m=644 doc/APRStt-interface-for-SARTrack.pdf $(DESTDIR)/share/doc/direwolf/APRStt-interface-for-SARTrack.pdf ++ $(INSTALL) -D -m=644 doc/APRStt-Listening-Example.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Listening-Example.pdf ++ $(INSTALL) -D -m=644 doc/Bluetooth-KISS-TNC.pdf $(DESTDIR)/share/doc/direwolf/Bluetooth-KISS-TNC.pdf ++ $(INSTALL) -D -m=644 doc/Going-beyond-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/Going-beyond-9600-baud.pdf ++ $(INSTALL) -D -m=644 doc/Raspberry-Pi-APRS.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS.pdf ++ $(INSTALL) -D -m=644 doc/Raspberry-Pi-APRS-Tracker.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS-Tracker.pdf ++ $(INSTALL) -D -m=644 doc/Raspberry-Pi-SDR-IGate.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-SDR-IGate.pdf ++ $(INSTALL) -D -m=644 doc/Successful-APRS-IGate-Operation.pdf $(DESTDIR)/share/doc/direwolf/Successful-APRS-IGate-Operation.pdf ++ $(INSTALL) -D -m=644 doc/User-Guide.pdf $(DESTDIR)/share/doc/direwolf/User-Guide.pdf ++ $(INSTALL) -D -m=644 doc/WA8LMF-TNC-Test-CD-Results.pdf $(DESTDIR)/share/doc/direwolf/WA8LMF-TNC-Test-CD-Results.pdf + # + # Various sample config and other files go into examples under the doc directory. + # When building from source, these can be put in home directory with "make install-conf". + # When installed from .DEB or .RPM package, the user will need to copy these to + # the home directory or other desired location. + # +- $(INSTALL) -D --mode=644 direwolf.conf $(DESTDIR)/share/doc/direwolf/examples/direwolf.conf +- $(INSTALL) -D --mode=755 dw-start.sh $(DESTDIR)/share/doc/direwolf/examples/dw-start.sh +- $(INSTALL) -D --mode=644 sdr.conf $(DESTDIR)/share/doc/direwolf/examples/sdr.conf +- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-m0xer-3.txt $(DESTDIR)/share/doc/direwolf/examples/telem-m0xer-3.txt +- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-balloon.conf $(DESTDIR)/share/doc/direwolf/examples/telem-balloon.conf +- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-volts.conf $(DESTDIR)/share/doc/direwolf/examples/telem-volts.conf ++ $(INSTALL) -D -m=644 direwolf.conf $(DESTDIR)/share/doc/direwolf/examples/direwolf.conf ++ $(INSTALL) -D -m=755 dw-start.sh $(DESTDIR)/share/doc/direwolf/examples/dw-start.sh ++ $(INSTALL) -D -m=644 sdr.conf $(DESTDIR)/share/doc/direwolf/examples/sdr.conf ++ $(INSTALL) -D -m=644 telemetry-toolkit/telem-m0xer-3.txt $(DESTDIR)/share/doc/direwolf/examples/telem-m0xer-3.txt ++ $(INSTALL) -D -m=644 telemetry-toolkit/telem-balloon.conf $(DESTDIR)/share/doc/direwolf/examples/telem-balloon.conf ++ $(INSTALL) -D -m=644 telemetry-toolkit/telem-volts.conf $(DESTDIR)/share/doc/direwolf/examples/telem-volts.conf + # + # "man" pages + # +- $(INSTALL) -D --mode=644 man1/aclients.1 $(DESTDIR)/share/man/man1/aclients.1 +- $(INSTALL) -D --mode=644 man1/atest.1 $(DESTDIR)/share/man/man1/atest.1 +- $(INSTALL) -D --mode=644 man1/decode_aprs.1 $(DESTDIR)/share/man/man1/decode_aprs.1 +- $(INSTALL) -D --mode=644 man1/direwolf.1 $(DESTDIR)/share/man/man1/direwolf.1 +- $(INSTALL) -D --mode=644 man1/gen_packets.1 $(DESTDIR)/share/man/man1/gen_packets.1 +- $(INSTALL) -D --mode=644 man1/kissutil.1 $(DESTDIR)/share/man/man1/kissutil.1 +- $(INSTALL) -D --mode=644 man1/ll2utm.1 $(DESTDIR)/share/man/man1/ll2utm.1 +- $(INSTALL) -D --mode=644 man1/log2gpx.1 $(DESTDIR)/share/man/man1/log2gpx.1 +- $(INSTALL) -D --mode=644 man1/text2tt.1 $(DESTDIR)/share/man/man1/text2tt.1 +- $(INSTALL) -D --mode=644 man1/tt2text.1 $(DESTDIR)/share/man/man1/tt2text.1 +- $(INSTALL) -D --mode=644 man1/utm2ll.1 $(DESTDIR)/share/man/man1/utm2ll.1 ++ $(INSTALL) -D -m=644 man1/aclients.1 $(DESTDIR)/share/man/man1/aclients.1 ++ $(INSTALL) -D -m=644 man1/atest.1 $(DESTDIR)/share/man/man1/atest.1 ++ $(INSTALL) -D -m=644 man1/decode_aprs.1 $(DESTDIR)/share/man/man1/decode_aprs.1 ++ $(INSTALL) -D -m=644 man1/direwolf.1 $(DESTDIR)/share/man/man1/direwolf.1 ++ $(INSTALL) -D -m=644 man1/gen_packets.1 $(DESTDIR)/share/man/man1/gen_packets.1 ++ $(INSTALL) -D -m=644 man1/kissutil.1 $(DESTDIR)/share/man/man1/kissutil.1 ++ $(INSTALL) -D -m=644 man1/ll2utm.1 $(DESTDIR)/share/man/man1/ll2utm.1 ++ $(INSTALL) -D -m=644 man1/log2gpx.1 $(DESTDIR)/share/man/man1/log2gpx.1 ++ $(INSTALL) -D -m=644 man1/text2tt.1 $(DESTDIR)/share/man/man1/text2tt.1 ++ $(INSTALL) -D -m=644 man1/tt2text.1 $(DESTDIR)/share/man/man1/tt2text.1 ++ $(INSTALL) -D -m=644 man1/utm2ll.1 $(DESTDIR)/share/man/man1/utm2ll.1 + # + # Set group and mode of HID devices corresponding to C-Media USB Audio adapters. + # This will allow us to use the CM108/CM119 GPIO pins for PTT. + # +- $(INSTALL) -D --mode=644 99-direwolf-cmedia.rules /etc/udev/rules.d/99-direwolf-cmedia.rules ++ $(INSTALL) -D -m=644 99-direwolf-cmedia.rules /etc/udev/rules.d/99-direwolf-cmedia.rules + # + @echo " " + @echo "If this is your first install, not an upgrade, type this to put a copy" +diff --git a/cdigipeater.c b/cdigipeater.c +index 9c40d95..94112e9 100644 +--- a/cdigipeater.c ++++ b/cdigipeater.c +@@ -49,7 +49,7 @@ + #include + #include /* for isdigit, isupper */ + #include "regex.h" +-#include ++#include + + #include "ax25_pad.h" + #include "cdigipeater.h" +diff --git a/decode_aprs.c b/decode_aprs.c +index 35c186b..a620cb3 100644 +--- a/decode_aprs.c ++++ b/decode_aprs.c +@@ -3872,11 +3872,7 @@ static void decode_tocall (decode_aprs_t *A, char *dest) + * models before getting to the more generic APY. + */ + +-#if defined(__WIN32__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__APPLE__) + qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), tocall_cmp); +-#else +- qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), (__compar_fn_t)tocall_cmp); +-#endif + } + else { + if ( ! A->g_quiet) { +diff --git a/digipeater.c b/digipeater.c +index 36970d7..5195582 100644 +--- a/digipeater.c ++++ b/digipeater.c +@@ -62,7 +62,7 @@ + #include + #include /* for isdigit, isupper */ + #include "regex.h" +-#include ++#include + + #include "ax25_pad.h" + #include "digipeater.h" +diff --git a/direwolf.h b/direwolf.h +index 514bcc5..52f5ae9 100644 +--- a/direwolf.h ++++ b/direwolf.h +@@ -274,7 +274,7 @@ char *strtok_r(char *str, const char *delim, char **saveptr); + char *strcasestr(const char *S, const char *FIND); + + +-#if defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__APPLE__) ++#if 1 + + // strlcpy and strlcat should be in string.h and the C library. + +diff --git a/multi_modem.c b/multi_modem.c +index 5d96c79..24261b9 100644 +--- a/multi_modem.c ++++ b/multi_modem.c +@@ -80,7 +80,7 @@ + #include + #include + #include +-#include ++#include + + #include "ax25_pad.h" + #include "textcolor.h" diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 972de0303..35dd92af8 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -14,8 +14,8 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport qt5-qttools" -BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc" +STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport qt5-qttools alsa-lib" +BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc alsa-lib-dev linux-headers" apk add --no-cache $STATIC_PACKAGES apk add --no-cache --virtual .build-deps $BUILD_PACKAGES @@ -85,4 +85,13 @@ wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ tar xvfz $WSJT_TGZ cmakebuild $WSJT_DIR +git clone https://github.com/wb2osz/direwolf.git +cd direwolf +git checkout 1.5 +patch -Np1 < /direwolf-1.5.patch +make +make install +cd .. +rm -rf direwolf + apk del .build-deps From 5cc67aba1572f7e0fb63e0436e5b786f55dd3745 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 23 Aug 2019 22:32:46 +0200 Subject: [PATCH 0345/2616] handle execptions during decode to avoid worker drain --- owrx/wsjt.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 34f7f4fb0..5c6ea8b3a 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -27,8 +27,11 @@ def __init__(self, queue): def run(self) -> None: while self.doRun: (processor, file) = self.queue.get() - logger.debug("processing file %s", file) - processor.decode(file) + try: + logger.debug("processing file %s", file) + processor.decode(file) + except Exception: + logger.exception("failed to decode job") self.queue.task_done() From fdd74e2e096051b9346c116087554b8d79541ecc Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 25 Aug 2019 16:30:01 +0200 Subject: [PATCH 0346/2616] remove patch (included in git now) --- docker/scripts/install-dependencies.sh | 36 -------------------------- 1 file changed, 36 deletions(-) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 35dd92af8..3befe6c5c 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -25,42 +25,6 @@ cmakebuild itpp git clone https://github.com/jketterl/csdr.git -b 48khz_filter cd csdr -patch -Np1 <<'EOF' ---- a/csdr.c -+++ b/csdr.c -@@ -38,6 +38,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - #include - #include - #include -+#include - #include - #include - #include -diff --git a/ddcd_old.h b/ddcd_old.h -index af4cfb5..b70092b 100644 ---- a/ddcd_old.h -+++ b/ddcd_old.h -@@ -19,6 +19,7 @@ - #include - #include - #include -+#include - - typedef struct client_s - { -diff --git a/nmux.h b/nmux.h -index 038bc51..079e416 100644 ---- a/nmux.h -+++ b/nmux.h -@@ -11,6 +11,7 @@ - #include - #include - #include -+#include - #include "tsmpool.h" - - #define MSG_START "nmux: " -EOF make make install cd .. From 2a09462f6fa272f0b37eae3fcd7e6522e78b0b25 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 26 Aug 2019 00:10:43 +0200 Subject: [PATCH 0347/2616] first work on the thirdparty header --- owrx/aprs.py | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 084318e78..085a17516 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -19,6 +19,9 @@ # regex for altitute in comment field altitudeRegex = re.compile("(^.*)\\/A=([0-9]{6})(.*$)") +# regex for parsing third-party headers +thirdpartyeRegex = re.compile("^([A-Z0-9-]+)>((([A-Z0-9-]+\\*?,)*)([A-Z0-9-]+\\*?)):(.*)$") + def decodeBase91(input): base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 @@ -73,16 +76,20 @@ def parse(self, raw): aprsData = self.parseAprsData(data) logger.debug("decoded APRS data: %s", aprsData) - if "lat" in aprsData and "lon" in aprsData: - loc = LatLngLocation( - aprsData["lat"], aprsData["lon"], aprsData["comment"] if "comment" in aprsData else None - ) - Map.getSharedInstance().updateLocation(data["source"], loc, "APRS", self.band) - + self.updateMap(aprsData) self.handler.write_aprs_data(aprsData) except Exception: logger.exception("exception while parsing aprs data") + def updateMap(self, mapData): + if "type" in mapData and mapData["type"] == "thirdparty" and "data" in mapData: + mapData = mapData["data"] + if "lat" in mapData and "lon" in mapData: + loc = LatLngLocation( + mapData["lat"], mapData["lon"], mapData["comment"] if "comment" in mapData else None + ) + Map.getSharedInstance().updateLocation(mapData["source"], loc, "APRS", self.band) + def hasCompressedCoordinates(self, raw): return raw[0] == "/" or raw[0] == "\\" @@ -156,10 +163,31 @@ def parseAprsData(self, data): aprsData.update(self.parseStatusUpate(information[1:])) elif dti == "}": # third party - aprsData["type"] = "thirdparty" + aprsData.update(self.parseThirdpartyAprsData(information[1:])) return aprsData + def parseThirdpartyAprsData(self, information): + matches = thirdpartyeRegex.match(information) + if matches: + logger.debug(matches) + path = matches[2].split(",") + destination = next((c for c in path if c.endswith("*")), None) + data = self.parseAprsData({ + "source": matches[1], + "destination": destination, + "path": path, + "data": matches[6].encode(encoding) + }) + return { + "type": "thirdparty", + "data": data, + } + + return { + "type": "thirdparty", + } + def parseRegularAprsData(self, information): if self.hasCompressedCoordinates(information): aprsData = self.parseCompressedCoordinates(information[0:10]) From a81c5f44a2aef0a1658eb6e60a7c7ef641b6b5a8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 26 Aug 2019 11:41:22 +0200 Subject: [PATCH 0348/2616] improve thirtparty header parsing --- owrx/aprs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 085a17516..13f88eb89 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -20,7 +20,7 @@ altitudeRegex = re.compile("(^.*)\\/A=([0-9]{6})(.*$)") # regex for parsing third-party headers -thirdpartyeRegex = re.compile("^([A-Z0-9-]+)>((([A-Z0-9-]+\\*?,)*)([A-Z0-9-]+\\*?)):(.*)$") +thirdpartyeRegex = re.compile("^([a-zA-Z0-9-]+)>((([a-zA-Z0-9-]+\\*?,)*)([a-zA-Z0-9-]+\\*?)):(.*)$") def decodeBase91(input): @@ -172,9 +172,9 @@ def parseThirdpartyAprsData(self, information): if matches: logger.debug(matches) path = matches[2].split(",") - destination = next((c for c in path if c.endswith("*")), None) + destination = next((c.strip("*").upper() for c in path if c.endswith("*")), None) data = self.parseAprsData({ - "source": matches[1], + "source": matches[1].upper(), "destination": destination, "path": path, "data": matches[6].encode(encoding) From 272c305ec24906bc75478390491dbb2f9fefcc1f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 26 Aug 2019 13:24:23 +0200 Subject: [PATCH 0349/2616] handle exceptions that may occur when parsing strings to numbers --- owrx/aprs.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 13f88eb89..c4e2ddd64 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -236,23 +236,35 @@ def decodeHeightGainDirectivity(comment): if comment[3] == "/": # course and speed # for a weather report, this would be wind direction and speed - aprsData["course"] = int(comment[0:3]) - aprsData["speed"] = int(comment[4:7]) * knotsToKilometers + try: + aprsData["course"] = int(comment[0:3]) + aprsData["speed"] = int(comment[4:7]) * knotsToKilometers + except ValueError: + pass comment = comment[7:] elif comment[0:3] == "PHG": # station power and effective antenna height/gain/directivity - powerCodes = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] - aprsData["power"] = powerCodes[int(comment[3])] - aprsData.update(decodeHeightGainDirectivity(comment)) + try: + powerCodes = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + aprsData["power"] = powerCodes[int(comment[3])] + aprsData.update(decodeHeightGainDirectivity(comment)) + except ValueError: + pass comment = comment[7:] elif comment[0:3] == "RNG": # pre-calculated radio range - aprsData["range"] = int(comment[3:7]) * milesToKilometers + try: + aprsData["range"] = int(comment[3:7]) * milesToKilometers + except ValueError: + pass comment = comment[7:] elif comment[0:3] == "DFS": # direction finding signal strength and antenna height/gain - aprsData["strength"] = int(comment[3]) - aprsData.update(decodeHeightGainDirectivity(comment)) + try: + aprsData["strength"] = int(comment[3]) + aprsData.update(decodeHeightGainDirectivity(comment)) + except ValueError: + pass comment = comment[7:] matches = altitudeRegex.match(comment) From 4409a369fa96d5066e83549ca13b337a3f337a08 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 26 Aug 2019 23:43:08 +0200 Subject: [PATCH 0350/2616] implement weather report parsing --- owrx/aprs.py | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index c4e2ddd64..d0989e48c 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -55,6 +55,66 @@ def extractCallsign(self, input): return cs +class WeatherMapping(object): + def __init__(self, char, key, length, scale = None): + self.char = char + self.key = key + self.length = length + self.scale = scale + + def matches(self, input): + return self.char == input[0] and len(input) > self.length + 1 + + def updateWeather(self, weather, input): + def deepApply(obj, key, v): + keys = key.split(".") + if len(keys) > 1: + if not keys[0] in obj: + obj[keys[0]] = {} + deepApply(obj[keys[0]], ".".join(keys[1:]), v) + else: + obj[key] = v + value = int(input[1:1 + self.length]) + if self.scale: + value = self.scale(value) + deepApply(weather, self.key, value) + remain = input[1 + self.length:] + return weather, remain + + +class WeatherParser(object): + mappings = [ + WeatherMapping("c", "wind.direction", 3), + WeatherMapping("s", "wind.speed", 3, lambda x: x * milesToKilometers), + WeatherMapping("g", "wind.gust", 3, lambda x: x * milesToKilometers), + WeatherMapping("t", "temperature", 3), + WeatherMapping("r", "rain.hour", 3, lambda x: x / 100 * 25.4), + WeatherMapping("p", "rain.day", 3, lambda x: x / 100 * 25.4), + WeatherMapping("P", "rain.sincemidnight", 3, lambda x: x / 100 * 25.4), + WeatherMapping("h", "humidity", 2), + WeatherMapping("b", "barometricpressure", 5, lambda x: x/10), + WeatherMapping("s", "snowfall", 3, lambda x: x * 25.4), + ] + + def __init__(self, data): + self.data = data + + def getWeather(self): + doWork = True + weather = {} + while doWork: + mapping = next((m for m in WeatherParser.mappings if m.matches(self.data)), None) + if mapping: + (weather, remain) = mapping.updateWeather(weather, self.data) + self.data = remain + else: + doWork = False + return weather + + def getRemainder(self): + return self.data + + class AprsParser(object): def __init__(self, handler): self.ax25parser = Ax25Parser() @@ -232,7 +292,24 @@ def decodeHeightGainDirectivity(comment): return res # aprs data extensions - if len(comment) > 6: + if "symbol" in aprsData and aprsData["symbol"] == "_": + # weather report + weather = {} + if len(comment) > 6 and comment[3] == "/": + try: + weather["wind"] = { + "direction": int(comment[0:3]), + "speed": int(comment[4:7]) * milesToKilometers, + } + except ValueError: + pass + comment = comment[7:] + + parser = WeatherParser(comment) + weather.update(parser.getWeather()) + comment = parser.getRemainder() + aprsData["weather"] = weather + elif len(comment) > 6: if comment[3] == "/": # course and speed # for a weather report, this would be wind direction and speed From 1a2f6b4970300e8b906afd8f263cf8a53c89f9ea Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 27 Aug 2019 11:32:50 +0200 Subject: [PATCH 0351/2616] improve weather decoding --- owrx/aprs.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index d0989e48c..583d58301 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -12,6 +12,7 @@ knotsToKilometers = 1.852 feetToMeters = 0.3048 milesToKilometers = 1.609344 +inchesToMilimeters = 25.4 # not sure what the correct encoding is. it seems TAPR has set utf-8 as a standard, but not everybody is following it. encoding = "utf-8" @@ -63,7 +64,7 @@ def __init__(self, char, key, length, scale = None): self.scale = scale def matches(self, input): - return self.char == input[0] and len(input) > self.length + 1 + return self.char == input[0] and len(input) > self.length def updateWeather(self, weather, input): def deepApply(obj, key, v): @@ -74,10 +75,13 @@ def deepApply(obj, key, v): deepApply(obj[keys[0]], ".".join(keys[1:]), v) else: obj[key] = v - value = int(input[1:1 + self.length]) - if self.scale: - value = self.scale(value) - deepApply(weather, self.key, value) + try: + value = int(input[1:1 + self.length]) + if self.scale: + value = self.scale(value) + deepApply(weather, self.key, value) + except ValueError: + pass remain = input[1 + self.length:] return weather, remain @@ -88,9 +92,9 @@ class WeatherParser(object): WeatherMapping("s", "wind.speed", 3, lambda x: x * milesToKilometers), WeatherMapping("g", "wind.gust", 3, lambda x: x * milesToKilometers), WeatherMapping("t", "temperature", 3), - WeatherMapping("r", "rain.hour", 3, lambda x: x / 100 * 25.4), - WeatherMapping("p", "rain.day", 3, lambda x: x / 100 * 25.4), - WeatherMapping("P", "rain.sincemidnight", 3, lambda x: x / 100 * 25.4), + WeatherMapping("r", "rain.hour", 3, lambda x: x / 100 * inchesToMilimeters), + WeatherMapping("p", "rain.day", 3, lambda x: x / 100 * inchesToMilimeters), + WeatherMapping("P", "rain.sincemidnight", 3, lambda x: x / 100 * inchesToMilimeters), WeatherMapping("h", "humidity", 2), WeatherMapping("b", "barometricpressure", 5, lambda x: x/10), WeatherMapping("s", "snowfall", 3, lambda x: x * 25.4), @@ -107,6 +111,7 @@ def getWeather(self): if mapping: (weather, remain) = mapping.updateWeather(weather, self.data) self.data = remain + doWork = len(self.data) > 0 else: doWork = False return weather From 707fcdb1abe5836e97feef77d636e75c53bfea9f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 27 Aug 2019 11:42:48 +0200 Subject: [PATCH 0352/2616] convert fahrenheit to celsius --- owrx/aprs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 583d58301..e257119f3 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -13,6 +13,8 @@ feetToMeters = 0.3048 milesToKilometers = 1.609344 inchesToMilimeters = 25.4 +def fahrenheitToCelsius(f): + return (f - 32) * 5 / 9 # not sure what the correct encoding is. it seems TAPR has set utf-8 as a standard, but not everybody is following it. encoding = "utf-8" @@ -91,7 +93,7 @@ class WeatherParser(object): WeatherMapping("c", "wind.direction", 3), WeatherMapping("s", "wind.speed", 3, lambda x: x * milesToKilometers), WeatherMapping("g", "wind.gust", 3, lambda x: x * milesToKilometers), - WeatherMapping("t", "temperature", 3), + WeatherMapping("t", "temperature", 3, fahrenheitToCelsius), WeatherMapping("r", "rain.hour", 3, lambda x: x / 100 * inchesToMilimeters), WeatherMapping("p", "rain.day", 3, lambda x: x / 100 * inchesToMilimeters), WeatherMapping("P", "rain.sincemidnight", 3, lambda x: x / 100 * inchesToMilimeters), From 1d8fea891ac887b457d1a8daa683d4eb74cbe989 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 27 Aug 2019 23:13:26 +0200 Subject: [PATCH 0353/2616] additional types; parse messages --- owrx/aprs.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/owrx/aprs.py b/owrx/aprs.py index e257119f3..fce978cba 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -25,6 +25,9 @@ def fahrenheitToCelsius(f): # regex for parsing third-party headers thirdpartyeRegex = re.compile("^([a-zA-Z0-9-]+)>((([a-zA-Z0-9-]+\\*?,)*)([a-zA-Z0-9-]+\\*?)):(.*)$") +# regex for getting the message id out of message +messageIdRegex = re.compile("^(.*){([0-9]{1,5})$") + def decodeBase91(input): base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 @@ -231,9 +234,37 @@ def parseAprsData(self, data): elif dti == "}": # third party aprsData.update(self.parseThirdpartyAprsData(information[1:])) + elif dti == ":": + # message + aprsData.update(self.parseMessage(information[1:])) + elif dti == ";": + # object + aprsData["type"] = "object" + elif dti == ")": + # item + aprsData["type"] = "item" return aprsData + def parseMessage(self, information): + result = {"type": "message"} + if len(information) > 10 and information[10] == ":": + result["adressee"] = information[0:9] + message = information[10:] + if len(message) > 3 and message[0:3] == "ack": + result["type"] = "messageacknowledgement" + result["messageid"] = int(message[3:8]) + elif len(message) > 3 and message[0:3] == "rej": + result["type"] = "messagerejection" + result["messageid"] = int(message[3:8]) + else: + matches = messageIdRegex.match(message) + if matches: + result["messageid"] = int(matches[2]) + message = matches[1] + result["message"] = message + return result + def parseThirdpartyAprsData(self, information): matches = thirdpartyeRegex.match(information) if matches: From 5530c96f8eccd1f6a21eb5c05b066901bae26b97 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 27 Aug 2019 23:32:21 +0200 Subject: [PATCH 0354/2616] fix message offsets --- owrx/aprs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index fce978cba..2aacb9b6e 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -248,7 +248,7 @@ def parseAprsData(self, data): def parseMessage(self, information): result = {"type": "message"} - if len(information) > 10 and information[10] == ":": + if len(information) > 9 and information[9] == ":": result["adressee"] = information[0:9] message = information[10:] if len(message) > 3 and message[0:3] == "ack": From b24e56803c4f63a3238e2cba96f825da09374cf0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 27 Aug 2019 23:52:51 +0200 Subject: [PATCH 0355/2616] avoid overriding weather dict keys --- owrx/aprs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 2aacb9b6e..fe04db212 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -105,12 +105,13 @@ class WeatherParser(object): WeatherMapping("s", "snowfall", 3, lambda x: x * 25.4), ] - def __init__(self, data): + def __init__(self, data, weather={}): self.data = data + self.weather = weather def getWeather(self): doWork = True - weather = {} + weather = self.weather while doWork: mapping = next((m for m in WeatherParser.mappings if m.matches(self.data)), None) if mapping: @@ -343,10 +344,9 @@ def decodeHeightGainDirectivity(comment): pass comment = comment[7:] - parser = WeatherParser(comment) - weather.update(parser.getWeather()) + parser = WeatherParser(comment, weather) + aprsData["weather"] = parser.getWeather() comment = parser.getRemainder() - aprsData["weather"] = weather elif len(comment) > 6: if comment[3] == "/": # course and speed From de22169ea8b1f2570e4efcefc4db2f53f4ce7ba0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 28 Aug 2019 21:56:50 +0200 Subject: [PATCH 0356/2616] implement item and object parsing --- owrx/aprs.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index fe04db212..940480d9f 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -240,13 +240,39 @@ def parseAprsData(self, data): aprsData.update(self.parseMessage(information[1:])) elif dti == ";": # object - aprsData["type"] = "object" + aprsData.update(self.parseObject(information[1:])) elif dti == ")": # item - aprsData["type"] = "item" + aprsData.update(self.parseItem(information[1:])) return aprsData + def parseObject(self, information): + result = {"type": "object"} + if len(information) > 16: + result["object"] = information[0:9].strip() + result["live"] = information[9] == "*" + result["timestamp"] = self.parseTimestamp(information[10:17]) + result.update(self.parseRegularAprsData(information[17:])) + # override type, losing information about compression + result["type"] = "object" + return result + + def parseItem(self, information): + result = {"type": "item"} + if len(information) > 3: + indexes = [information[0:9].find(p) for p in ["!", "_"]] + filtered = [i for i in indexes if i >= 3] + filtered.sort() + if len(filtered): + index = filtered[0] + result["item"] = information[0:index] + result["live"] = information[index] == "!" + result.update(self.parseRegularAprsData(information[index + 1:])) + # override type, losing information about compression + result["type"] = "item" + return result + def parseMessage(self, information): result = {"type": "message"} if len(information) > 9 and information[9] == ":": From db8d4cd3fe2e0df7145a60a4e49239e606746948 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 28 Aug 2019 22:01:01 +0200 Subject: [PATCH 0357/2616] display items and objects on the map --- owrx/aprs.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 940480d9f..fc9dac328 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -159,7 +159,13 @@ def updateMap(self, mapData): loc = LatLngLocation( mapData["lat"], mapData["lon"], mapData["comment"] if "comment" in mapData else None ) - Map.getSharedInstance().updateLocation(mapData["source"], loc, "APRS", self.band) + source = mapData["source"] + if "type" in mapData: + if mapData["type"] == "item": + source = mapData["item"] + elif mapData["type"] == "object": + source = mapData["object"] + Map.getSharedInstance().updateLocation(source, loc, "APRS", self.band) def hasCompressedCoordinates(self, raw): return raw[0] == "/" or raw[0] == "\\" From 2dcdad3a49c132c41ab0f2f4f966ddd4d4196bb6 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 28 Aug 2019 22:09:52 +0200 Subject: [PATCH 0358/2616] fix message parsing range --- owrx/aprs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index fc9dac328..254485228 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -267,7 +267,7 @@ def parseObject(self, information): def parseItem(self, information): result = {"type": "item"} if len(information) > 3: - indexes = [information[0:9].find(p) for p in ["!", "_"]] + indexes = [information[0:10].find(p) for p in ["!", "_"]] filtered = [i for i in indexes if i >= 3] filtered.sort() if len(filtered): From aac618bfee6f3c0b34ef4db72cb80791283eb86a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 2 Sep 2019 16:20:49 +0100 Subject: [PATCH 0359/2616] fix for python 3.5 --- owrx/aprs.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 254485228..a2ddf020c 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -293,8 +293,8 @@ def parseMessage(self, information): else: matches = messageIdRegex.match(message) if matches: - result["messageid"] = int(matches[2]) - message = matches[1] + result["messageid"] = int(matches.group(2)) + message = matches.group(1) result["message"] = message return result @@ -302,13 +302,13 @@ def parseThirdpartyAprsData(self, information): matches = thirdpartyeRegex.match(information) if matches: logger.debug(matches) - path = matches[2].split(",") + path = matches.group(2).split(",") destination = next((c.strip("*").upper() for c in path if c.endswith("*")), None) data = self.parseAprsData({ - "source": matches[1].upper(), + "source": matches.group(1).upper(), "destination": destination, "path": path, - "data": matches[6].encode(encoding) + "data": matches.group(6).encode(encoding) }) return { "type": "thirdparty", @@ -416,8 +416,8 @@ def decodeHeightGainDirectivity(comment): matches = altitudeRegex.match(comment) if matches: - aprsData["altitude"] = int(matches[2]) * feetToMeters - comment = matches[1] + matches[3] + aprsData["altitude"] = int(matches.group(2)) * feetToMeters + comment = matches.group(1) + matches.group(3) aprsData["comment"] = comment From 942ee637b05dbb45660d09a0e53db20c47acf878 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 3 Sep 2019 23:38:27 +0200 Subject: [PATCH 0360/2616] fix alternate spaces --- htdocs/openwebrx.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 081910571..7ed0b37c4 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -435,12 +435,12 @@ function demodulator_default_analog(offset_frequency,subtype) this.low_cut=-4000; this.high_cut=4000; } - else if(subtype=="dmr" || subtype=="ysf") + else if(subtype=="dmr" || subtype=="ysf") { this.low_cut=-4000; this.high_cut=4000; } - else if(subtype=="dstar" || subtype=="nxdn") + else if(subtype=="dstar" || subtype=="nxdn") { this.low_cut=-3250; this.high_cut=3250; From 08cf8977f71a37135907fe18b28758077453e9ec Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 9 Sep 2019 23:07:38 +0200 Subject: [PATCH 0361/2616] fix ft4 frequency on 80m --- bands.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bands.json b/bands.json index bddfbef9b..29867c7ce 100644 --- a/bands.json +++ b/bands.json @@ -21,7 +21,7 @@ "wspr": 3592600, "jt65": 3570000, "jt9": 3572000, - "ft4": [3568000, 3568000] + "ft4": [3568000, 3575000] } }, { From 6d44aa3f580af1ee736b4a260b7d23c7ab4a0d58 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 11 Sep 2019 00:27:49 +0200 Subject: [PATCH 0362/2616] don't decimate at factor 1 --- csdr.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/csdr.py b/csdr.py index 832d06925..ab4688b46 100644 --- a/csdr.py +++ b/csdr.py @@ -127,11 +127,10 @@ def chain(self, which): if self.fft_compression == "adpcm": chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"] return chain - chain += [ - "csdr shift_addition_cc --fifo {shift_pipe}", - "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING", - "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING", - ] + chain += ["csdr shift_addition_cc --fifo {shift_pipe}"] + if self.decimation > 1: + chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"] + chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"] if self.output.supports_type("smeter"): chain += [ "csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}" From d87e5da75c2b2588895f2d575889b4aa39220ab4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 11 Sep 2019 00:30:14 +0200 Subject: [PATCH 0363/2616] attempt to reduce cpu usage by pre-selecting parts of the spectrum with resamplers --- owrx/service.py | 98 +++++++++++++++++++++++++++++++++++++++++++++---- owrx/source.py | 87 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 7 deletions(-) diff --git a/owrx/service.py b/owrx/service.py index b64e504bc..ab3e59bca 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -1,9 +1,11 @@ import threading +import socket from owrx.source import SdrService from owrx.bands import Bandplan from csdr import dsp, output from owrx.wsjt import WsjtParser from owrx.config import PropertyManager +from owrx.source import Resampler import logging @@ -63,28 +65,110 @@ def scheduleServiceStartup(self): self.startupTimer = threading.Timer(10, self.updateServices) self.startupTimer.start() + def getAvailablePort(self): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(("", 0)) + s.listen(1) + port = s.getsockname()[1] + s.close() + return port + def updateServices(self): logger.debug("re-scheduling services due to sdr changes") self.stopServices() cf = self.source.getProps()["center_freq"] - srh = self.source.getProps()["samp_rate"] / 2 + sr = self.source.getProps()["samp_rate"] + srh = sr / 2 frequency_range = (cf - srh, cf + srh) - self.services = [ - self.setupService(dial["mode"], dial["frequency"]) + + dials = [ + dial for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range) if self.isSupported(dial["mode"]) ] - def setupService(self, mode, frequency): + if not dials: + logger.debug("no services available") + return + + self.services = [] + + for group in self.optimizeResampling(dials, sr): + frequencies = sorted([f["frequency"] for f in group]) + min = frequencies[0] + max = frequencies[-1] + cf = (min + max) / 2 + bw = max - min + logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw)) + resampler_props = PropertyManager() + resampler_props["center_freq"] = cf + # TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths + resampler_props["samp_rate"] = bw + 24000 + resampler = Resampler(resampler_props, self.getAvailablePort(), self.source) + resampler.start() + self.services.append(resampler) + + for dial in group: + self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler)) + + def optimizeResampling(self, freqs, bandwidth): + freqs = sorted(freqs, key=lambda f: f["frequency"]) + distances = [{ + "frequency": freqs[i]["frequency"], + "distance": freqs[i+1]["frequency"] - freqs[i]["frequency"], + } for i in range(0, len(freqs)-1)] + + distances = [d for d in distances if d["distance"] > 0] + + distances = sorted(distances, key=lambda f: f["distance"], reverse=True) + + + def calculate_usage(num_splits): + splits = sorted([f["frequency"] for f in distances[0:num_splits]]) + previous = 0 + groups = [] + for split in splits: + groups.append([f for f in freqs if previous < f["frequency"] <= split]) + previous = split + groups.append([f for f in freqs if previous < f["frequency"]]) + + def get_bandwitdh(group): + freqs = sorted([f["frequency"] for f in group]) + # the group will process the full BW once, plus the reduced BW once for each group member + return bandwidth + len(group) * (freqs[-1] - freqs[0] + 24000) + + total_bandwidth = sum([get_bandwitdh(group) for group in groups]) + return { + "num_splits": num_splits, + "total_bandwidth": total_bandwidth, + "groups": groups, + } + + + usages = [calculate_usage(i) for i in range(0, len(freqs))] + # this is simulating no resampling. i haven't seen this as the best result yet + usages += [{ + "num_splits": None, + "total_bandwidth": bandwidth * len(freqs), + "groups": [freqs] + }] + results = sorted(usages, key=lambda f: f["total_bandwidth"]) + + for r in results: + logger.debug("splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"])) + + return results[0]["groups"] + + def setupService(self, mode, frequency, source): logger.debug("setting up service {0} on frequency {1}".format(mode, frequency)) d = dsp(ServiceOutput(frequency)) - d.nc_port = self.source.getPort() - d.set_offset_freq(frequency - self.source.getProps()["center_freq"]) + d.nc_port = source.getPort() + d.set_offset_freq(frequency - source.getProps()["center_freq"]) d.set_demodulator("usb") d.set_bpf(0, 3000) d.set_secondary_demodulator(mode) d.set_audio_compression("none") - d.set_samp_rate(self.source.getProps()["samp_rate"]) + d.set_samp_rate(source.getProps()["samp_rate"]) d.start() return d diff --git a/owrx/source.py b/owrx/source.py index 8f5dc6534..c73beac86 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -274,6 +274,93 @@ def writeSpectrumData(self, data): c.write_spectrum_data(data) +class Resampler(SdrSource): + def __init__(self, props, port, sdr): + sdrProps = sdr.getProps() + self.shift = (sdrProps["center_freq"] - props["center_freq"]) / sdrProps["samp_rate"] + self.decimation = int(float(sdrProps["samp_rate"]) / props["samp_rate"]) + if_samp_rate = sdrProps["samp_rate"] / self.decimation + self.transition_bw = 0.15 * (if_samp_rate / float(sdrProps["samp_rate"])) + props["samp_rate"] = if_samp_rate + + self.sdr = sdr + super().__init__(props, port) + + def start(self): + self.modificationLock.acquire() + if self.monitor: + self.modificationLock.release() + return + + props = self.rtlProps + + resampler_command = [ + "nc -v 127.0.0.1 {nc_port}".format(nc_port=self.sdr.getPort()), + "csdr shift_addition_cc {shift}".format(shift=self.shift), + "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING".format( + decimation=self.decimation, + ddc_transition_bw=self.transition_bw, + ), + ] + + nmux_bufcnt = nmux_bufsize = 0 + while nmux_bufsize < props["samp_rate"] / 4: + nmux_bufsize += 4096 + while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6: + nmux_bufcnt += 1 + if nmux_bufcnt == 0 or nmux_bufsize == 0: + logger.error( + "Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py" + ) + self.modificationLock.release() + return + logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)) + resampler_command += ["nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % ( + nmux_bufsize, + nmux_bufcnt, + self.port, + )] + cmd = " | ".join(resampler_command) + logger.debug("resampler command: %s", cmd) + self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp) + logger.info("Started resampler source: " + cmd) + + available = False + + def wait_for_process_to_end(): + rc = self.process.wait() + logger.debug("shut down with RC={0}".format(rc)) + self.monitor = None + + self.monitor = threading.Thread(target=wait_for_process_to_end) + self.monitor.start() + + retries = 1000 + while retries > 0: + retries -= 1 + if self.monitor is None: + break + testsock = socket.socket() + try: + testsock.connect(("127.0.0.1", self.getPort())) + testsock.close() + available = True + break + except: + time.sleep(0.1) + + self.modificationLock.release() + + if not available: + raise SdrSourceException("resampler source failed to start up") + + for c in self.clients: + c.onSdrAvailable() + + def activateProfile(self, profile_id=None): + pass + + class RtlSdrSource(SdrSource): def getCommand(self): return "rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -" From a11875145b32a56b97f1265046f4fcf6dd6f3ef1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 12 Sep 2019 15:32:54 +0200 Subject: [PATCH 0364/2616] make wsjt queue configurable --- config_webrx.py | 11 +++++++++++ owrx/wsjt.py | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/config_webrx.py b/config_webrx.py index 11d8d30a6..227cad140 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -250,6 +250,17 @@ # in seconds; default: 2 hours map_position_retention_time = 2 * 60 * 60 +# wsjt decoder queue configuration +# due to the nature of the wsjt operating modes (ft8, ft8, jt9, jt65 and wspr), the data is recorded for a given amount +# of time (6.5 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads. +# to mitigate this, the recordings will be queued and processed in sequence. +# the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread) +wsjt_queue_workers = 2 +# the maximum queue length will cause decodes to be dumped if the workers cannot keep up +# if you are running background services, make sure this number is high enough to accept the task influx during peaks +# i.e. this should be higher than the number of wsjt services running at the same time +wsjt_queue_length = 10 + temporary_directory = "/tmp" services_enabled = False diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 5c6ea8b3a..a12bd9752 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -41,7 +41,8 @@ class WsjtQueue(Queue): @staticmethod def getSharedInstance(): if WsjtQueue.sharedInstance is None: - WsjtQueue.sharedInstance = WsjtQueue(maxsize=10, workers=2) + pm = PropertyManager.getSharedInstance() + WsjtQueue.sharedInstance = WsjtQueue(maxsize=pm["wsjt_queue_length"], workers=pm["wsjt_queue_workers"]) return WsjtQueue.sharedInstance def __init__(self, maxsize, workers): From bc5b16b5e3e7b2e2a9f27eaefe2a7e3260107671 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 12 Sep 2019 22:50:29 +0200 Subject: [PATCH 0365/2616] rewire the metrics; make queue length metric available --- owrx/metrics.py | 60 +++++++++++++++++++++++++++++++++++++------------ owrx/wsjt.py | 26 ++++++++++++++++++--- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/owrx/metrics.py b/owrx/metrics.py index 11f503f92..b1b577882 100644 --- a/owrx/metrics.py +++ b/owrx/metrics.py @@ -1,3 +1,29 @@ +class Metric(object): + def getValue(self): + return 0 + + +class CounterMetric(Metric): + def __init__(self): + self.counter = 0 + + def inc(self, increment=1): + self.counter += increment + + def getValue(self): + return { + "count": self.counter, + } + + +class DirectMetric(Metric): + def __init__(self, getter): + self.getter = getter + + def getValue(self): + return self.getter() + + class Metrics(object): sharedInstance = None @@ -10,21 +36,27 @@ def getSharedInstance(): def __init__(self): self.metrics = {} - def pushDecodes(self, band, mode, count=1): - if band is None: - band = "unknown" - else: - band = band.getName() + def addMetric(self, name, metric): + self.metrics[name] = metric - if mode is None: - mode = "unknown" + def hasMetric(self, name): + return name in self.metrics - if not band in self.metrics: - self.metrics[band] = {} - if not mode in self.metrics[band]: - self.metrics[band][mode] = {"count": 0} - - self.metrics[band][mode]["count"] += count + def getMetric(self, name): + if not self.hasMetric(name): + return None + return self.metrics[name] def getMetrics(self): - return self.metrics + result = {} + + for (key, metric) in self.metrics.items(): + partial = result + keys = key.split(".") + for keypart in keys[0:-1]: + if not keypart in partial: + partial[keypart] = {} + partial = partial[keypart] + partial[keys[-1]] = metric.getValue() + + return result diff --git a/owrx/wsjt.py b/owrx/wsjt.py index a12bd9752..9c2907065 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -11,7 +11,7 @@ from queue import Queue, Full from owrx.config import PropertyManager from owrx.bands import Bandplan -from owrx.metrics import Metrics +from owrx.metrics import Metrics, CounterMetric, DirectMetric import logging @@ -48,6 +48,7 @@ def getSharedInstance(): def __init__(self, maxsize, workers): super().__init__(maxsize) self.workers = [self.newWorker() for _ in range(0, workers)] + Metrics.getSharedInstance().addMetric("wsjt.queue.length", DirectMetric(self.qsize)) def put(self, item): super(WsjtQueue, self).put(item, block=False) @@ -249,6 +250,24 @@ def parse_timestamp(self, instring, dateformat): ts = datetime.strptime(instring, dateformat) return int(datetime.combine(date.today(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000) + def pushDecode(self, mode): + metrics = Metrics.getSharedInstance() + band = self.band.getName() + if band is None: + band = "unknown" + + if mode is None: + mode = "unknown" + + name = "wsjt.decodes.{band}.{mode}".format(band=band, mode=mode) + metric = metrics.getMetric(name) + if metric is None: + metric = CounterMetric() + metrics.addMetric(name, metric) + + metric.inc() + + def parse_from_jt9(self, msg): # ft8 sample # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' @@ -266,7 +285,8 @@ def parse_from_jt9(self, msg): mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" wsjt_msg = msg[17:53].strip() self.parseLocator(wsjt_msg, mode) - Metrics.getSharedInstance().pushDecodes(self.band, mode) + + self.pushDecode(mode) return { "timestamp": timestamp, "db": float(msg[0:3]), @@ -292,7 +312,7 @@ def parse_from_wsprd(self, msg): # '0052 -29 2.6 0.001486 0 G02CWT IO92 23' wsjt_msg = msg[29:].strip() self.parseWsprMessage(wsjt_msg) - Metrics.getSharedInstance().pushDecodes(self.band, "WSPR") + self.pushDecode("WSPR") return { "timestamp": self.parse_timestamp(msg[0:4], "%H%M"), "db": float(msg[5:8]), From 338a19373c4ca5af6c5a1ef6578251f95a8c39f6 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 12 Sep 2019 23:23:50 +0200 Subject: [PATCH 0366/2616] count aprs decodes, too --- owrx/aprs.py | 16 ++++++++++++++++ owrx/wsjt.py | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index a2ddf020c..896440c12 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -1,6 +1,7 @@ from owrx.kiss import KissDeframer from owrx.map import Map, LatLngLocation from owrx.bands import Bandplan +from owrx.metrics import Metrics, CounterMetric from datetime import datetime, timezone import re import logging @@ -133,10 +134,24 @@ def __init__(self, handler): self.dial_freq = None self.band = None self.handler = handler + self.metric = self.getMetric() def setDialFrequency(self, freq): self.dial_freq = freq self.band = Bandplan.getSharedInstance().findBand(freq) + self.metric = self.getMetric() + + def getMetric(self): + band = "unknown" + if self.band is not None: + band = self.band.getName() + name = "aprs.decodes.{band}.aprs".format(band=band) + metrics = Metrics.getSharedInstance() + metric = metrics.getMetric(name) + if metric is None: + metric = CounterMetric() + metrics.addMetric(name, metric) + return metric def parse(self, raw): for frame in self.deframer.parse(raw): @@ -148,6 +163,7 @@ def parse(self, raw): logger.debug("decoded APRS data: %s", aprsData) self.updateMap(aprsData) + self.metric.inc() self.handler.write_aprs_data(aprsData) except Exception: logger.exception("exception while parsing aprs data") diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 9c2907065..7b002c26a 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -252,7 +252,9 @@ def parse_timestamp(self, instring, dateformat): def pushDecode(self, mode): metrics = Metrics.getSharedInstance() - band = self.band.getName() + band = "unknown" + if self.band is not None: + band = self.band.getName() if band is None: band = "unknown" From 6ff55e1279eb3e1903fc49eaebbcef2e75a8aa63 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 13 Sep 2019 00:16:36 +0200 Subject: [PATCH 0367/2616] queue in / out stats --- owrx/wsjt.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 7b002c26a..49b7e3b20 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -47,12 +47,24 @@ def getSharedInstance(): def __init__(self, maxsize, workers): super().__init__(maxsize) + metrics = Metrics.getSharedInstance() + metrics.addMetric("wsjt.queue.length", DirectMetric(self.qsize)) + self.inCounter = CounterMetric() + metrics.addMetric("wsjt.queue.in", self.inCounter) + self.outCounter = CounterMetric() + metrics.addMetric("wsjt.queue.out", self.outCounter) self.workers = [self.newWorker() for _ in range(0, workers)] - Metrics.getSharedInstance().addMetric("wsjt.queue.length", DirectMetric(self.qsize)) def put(self, item): + self.inCounter.inc() super(WsjtQueue, self).put(item, block=False) + def get(self, **kwargs): + # super.get() is blocking, so it would mess up the stats to inc() first + out = super(WsjtQueue, self).get(**kwargs) + self.outCounter.inc() + return out + def newWorker(self): worker = WsjtQueueWorker(self) worker.start() From be05b54053011df8523b021110a21a9136a153f6 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 13 Sep 2019 20:58:37 +0200 Subject: [PATCH 0368/2616] jt65 seems very prone to false decodes --- owrx/wsjt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 49b7e3b20..9d71921b2 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -203,7 +203,7 @@ def __init__(self, source): def decoder_commandline(self, file): # TODO expose decoding quality parameters through config - return ["jt9", "--jt65", "-d", "3", file] + return ["jt9", "--jt65", "-d", "1", file] class Jt9Chopper(WsjtChopper): From 5bcad1ef2f3f60907b329dfec982aaaeca84d2f5 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 13 Sep 2019 21:04:00 +0200 Subject: [PATCH 0369/2616] hide output text for packet --- htdocs/css/openwebrx.css | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index bba9ce315..f517f60e4 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -848,31 +848,29 @@ img.openwebrx-mirror-img width: 70px; } -#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container, -#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container, -#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container, -#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-content-container, -#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container, -#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, -#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel, -#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel, -#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel, -#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel { display: none; } -#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container, -#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container, -#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-canvas-container, -#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container, -#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container +#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container, +#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container, +#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-canvas-container, +#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container, +#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container, +#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container { height: 200px; margin: -10px; } - -#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container { - height: 250px; -} From 311f22f6ba386fa13251801424c279c530b56a47 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 13 Sep 2019 22:28:17 +0200 Subject: [PATCH 0370/2616] flag services (avoid connecting to aprs network twice) --- csdr.py | 10 +++++++--- owrx/kiss.py | 4 ++-- owrx/service.py | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/csdr.py b/csdr.py index c542197e5..412fda25a 100644 --- a/csdr.py +++ b/csdr.py @@ -65,7 +65,7 @@ def supports_type(self, t): class dsp(object): def __init__(self, output): self.samp_rate = 250000 - self.output_rate = 11025 # this is default, and cannot be set at the moment + self.output_rate = 11025 self.fft_size = 1024 self.fft_fps = 5 self.offset_freq = 0 @@ -80,7 +80,7 @@ def __init__(self, output): self.demodulator = "nfm" self.name = "csdr" self.base_bufsize = 512 - self.nc_port = 4951 + self.nc_port = None self.csdr_dynamic_bufsize = False self.csdr_print_bufsizes = False self.csdr_through = False @@ -108,9 +108,13 @@ def __init__(self, output): self.modification_lock = threading.Lock() self.output = output self.temporary_directory = "/tmp" + self.is_service = False self.direwolf_config = None self.direwolf_port = None + def set_service(self, flag=True): + self.is_service = flag + def set_temporary_directory(self, what): self.temporary_directory = what @@ -563,7 +567,7 @@ def try_create_configs(self, command): ) self.direwolf_port = KissClient.getFreePort() file = open(self.direwolf_config, "w") - file.write(DirewolfConfig().getConfig(self.direwolf_port)) + file.write(DirewolfConfig().getConfig(self.direwolf_port, self.is_service)) file.close() else: self.direwolf_config = None diff --git a/owrx/kiss.py b/owrx/kiss.py index 82d21b187..1ea9408b3 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -13,7 +13,7 @@ class DirewolfConfig(object): - def getConfig(self, port): + def getConfig(self, port, is_service): pm = PropertyManager.getSharedInstance() config = """ @@ -29,7 +29,7 @@ def getConfig(self, port): port=port, callsign=pm["aprs_callsign"] ) - if pm["aprs_igate_enabled"]: + if is_service and pm["aprs_igate_enabled"]: config += """ IGSERVER {server} IGLOGIN {callsign} {password} diff --git a/owrx/service.py b/owrx/service.py index 213d33590..167ae1bfd 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -196,6 +196,7 @@ def setupService(self, mode, frequency, source): d.set_secondary_demodulator(mode) d.set_audio_compression("none") d.set_samp_rate(source.getProps()["samp_rate"]) + d.set_service() d.start() return d From 8d47259f78d4e1edfe694e230a42acd3af210962 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 13 Sep 2019 22:29:04 +0200 Subject: [PATCH 0371/2616] show decoded aprs messages in the frontend --- htdocs/css/openwebrx.css | 30 +++++++++++++++--- htdocs/index.html | 9 ++++++ htdocs/openwebrx.js | 67 ++++++++++++++++++++++++++++++++++------ 3 files changed, 92 insertions(+), 14 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index f517f60e4..516facd3f 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -813,23 +813,32 @@ img.openwebrx-mirror-img display: inline-block; } -#openwebrx-panel-wsjt-message { +#openwebrx-panel-wsjt-message, +#openwebrx-panel-packet-message +{ height: 180px; } -#openwebrx-panel-wsjt-message tbody { +#openwebrx-panel-wsjt-message tbody, +#openwebrx-panel-packet-message tbody +{ display: block; overflow: auto; height: 150px; width: 100%; } -#openwebrx-panel-wsjt-message thead tr { +#openwebrx-panel-wsjt-message thead tr, +#openwebrx-panel-packet-message thead tr +{ display: block; } #openwebrx-panel-wsjt-message th, -#openwebrx-panel-wsjt-message td { +#openwebrx-panel-wsjt-message td, +#openwebrx-panel-packet-message th, +#openwebrx-panel-packet-message td +{ width: 50px; text-align: left; padding: 1px 3px; @@ -848,6 +857,19 @@ img.openwebrx-mirror-img width: 70px; } +#openwebrx-panel-packet-message .message { + width: 410px; +} + +#openwebrx-panel-packet-message .callsign { + width: 80px; +} + +#openwebrx-panel-packet-message .coord { + width: 40px; + text-align: center; +} + #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container, diff --git a/htdocs/index.html b/htdocs/index.html index 328735cf9..57247e0e0 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -163,6 +163,15 @@
    UTC dB DTFreqFreq Message
    ' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '' + msg['db'] + '' + msg['dt'] + '' + msg['freq'] + '' + msg['freq'] + '' + linkedmsg + '
    + + + + + + + + +
    UTCCallsignCoordComment
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 7ed0b37c4..f4111fd2b 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1253,6 +1253,9 @@ function on_ws_recv(evt) dial_frequencies = json.value; update_dial_button(); break; + case "aprs_data": + update_packet_panel(json.value); + break; default: console.warn('received message of unknown type: ' + json.type); } @@ -1439,20 +1442,61 @@ function update_wsjt_panel(msg) { $b.scrollTop($b[0].scrollHeight); } -var wsjt_removal_interval; +var digital_removal_interval; // remove old wsjt messages in fixed intervals -function init_wsjt_removal_timer() { - if (wsjt_removal_interval) clearInterval(wsjt_removal_interval); - wsjt_removal_interval = setInterval(function(){ - var $elements = $('#openwebrx-panel-wsjt-message tbody tr'); - // limit to 1000 entries in the list since browsers get laggy at some point - var toRemove = $elements.length - 1000; - if (toRemove <= 0) return; - $elements.slice(0, toRemove).remove(); +function init_digital_removal_timer() { + if (digital_removal_interval) clearInterval(digital_removal_interval); + digital_removal_interval = setInterval(function(){ + ['#openwebrx-panel-wsjt-message', '#openwebrx-panel-packet-message'].forEach(function(root){ + var $elements = $(root + ' tbody tr'); + // limit to 1000 entries in the list since browsers get laggy at some point + var toRemove = $elements.length - 1000; + if (toRemove <= 0) return; + $elements.slice(0, toRemove).remove(); + }); }, 15000); } +function update_packet_panel(msg) { + var $b = $('#openwebrx-panel-packet-message tbody'); + var pad = function(i) { return ('' + i).padStart(2, "0"); } + + if (msg.type && msg.type == 'thirdparty' && msg.data) { + msg = msg.data; + } + var source = msg.source; + if (msg.type) { + if (msg.type == 'item') { + source = msg.item; + } + if (msg.type == 'object') { + source = msg.object; + } + } + + var timestamp = ''; + if (msg.timestamp) { + var t = new Date(msg.timestamp); + timestamp = pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + } + + var link = ''; + if (msg.lat && msg.lon) { + link = ''; + } + + $b.append($( + '' + + '' + timestamp + '' + + '' + source + '' + + '' + link + '' + + '' + (msg.comment || msg.message || '') + '' + + '' + )); + $b.scrollTop($b[0].scrollHeight); +} + function hide_digitalvoice_panels() { $(".openwebrx-meta-panel").each(function(_, p){ toggle_panel(p.id, false); @@ -2752,6 +2796,7 @@ function demodulator_digital_replace(subtype) $('#openwebrx-panel-digimodes').attr('data-mode', subtype); toggle_panel("openwebrx-panel-digimodes", true); toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(subtype) >= 0); + toggle_panel("openwebrx-panel-packet-message", subtype == "packet"); } function secondary_demod_create_canvas() @@ -2807,6 +2852,7 @@ function secondary_demod_init() { $("#openwebrx-panel-digimodes")[0].openwebrxHidden = true; $("#openwebrx-panel-wsjt-message")[0].openwebrxHidden = true; + $("#openwebrx-panel-packet-message")[0].openwebrxHidden = true; secondary_demod_canvas_container = $("#openwebrx-digimode-canvas-container")[0]; $(secondary_demod_canvas_container) .mousemove(secondary_demod_canvas_container_mousemove) @@ -2814,7 +2860,7 @@ function secondary_demod_init() .mousedown(secondary_demod_canvas_container_mousedown) .mouseenter(secondary_demod_canvas_container_mousein) .mouseleave(secondary_demod_canvas_container_mouseout); - init_wsjt_removal_timer(); + init_digital_removal_timer(); } function secondary_demod_start(subtype) @@ -2874,6 +2920,7 @@ function secondary_demod_close_window() secondary_demod_stop(); toggle_panel("openwebrx-panel-digimodes", false); toggle_panel("openwebrx-panel-wsjt-message", false); + toggle_panel("openwebrx-panel-packet-message", false); } secondary_demod_fft_offset_db=30; //need to calculate that later From 98f1545fcadadfe09d4559d0bbc957327388ad95 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 13 Sep 2019 23:03:05 +0200 Subject: [PATCH 0372/2616] code format --- owrx/aprs.py | 46 +++++++++++++++++++++------------------------- owrx/metrics.py | 4 +--- owrx/service.py | 22 ++++++---------------- owrx/source.py | 11 ++++------- owrx/wsjt.py | 5 +++-- 5 files changed, 35 insertions(+), 53 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 896440c12..967eae555 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -14,9 +14,12 @@ feetToMeters = 0.3048 milesToKilometers = 1.609344 inchesToMilimeters = 25.4 + + def fahrenheitToCelsius(f): return (f - 32) * 5 / 9 + # not sure what the correct encoding is. it seems TAPR has set utf-8 as a standard, but not everybody is following it. encoding = "utf-8" @@ -63,7 +66,7 @@ def extractCallsign(self, input): class WeatherMapping(object): - def __init__(self, char, key, length, scale = None): + def __init__(self, char, key, length, scale=None): self.char = char self.key = key self.length = length @@ -81,14 +84,15 @@ def deepApply(obj, key, v): deepApply(obj[keys[0]], ".".join(keys[1:]), v) else: obj[key] = v + try: - value = int(input[1:1 + self.length]) + value = int(input[1 : 1 + self.length]) if self.scale: value = self.scale(value) deepApply(weather, self.key, value) except ValueError: pass - remain = input[1 + self.length:] + remain = input[1 + self.length :] return weather, remain @@ -102,7 +106,7 @@ class WeatherParser(object): WeatherMapping("p", "rain.day", 3, lambda x: x / 100 * inchesToMilimeters), WeatherMapping("P", "rain.sincemidnight", 3, lambda x: x / 100 * inchesToMilimeters), WeatherMapping("h", "humidity", 2), - WeatherMapping("b", "barometricpressure", 5, lambda x: x/10), + WeatherMapping("b", "barometricpressure", 5, lambda x: x / 10), WeatherMapping("s", "snowfall", 3, lambda x: x * 25.4), ] @@ -172,9 +176,7 @@ def updateMap(self, mapData): if "type" in mapData and mapData["type"] == "thirdparty" and "data" in mapData: mapData = mapData["data"] if "lat" in mapData and "lon" in mapData: - loc = LatLngLocation( - mapData["lat"], mapData["lon"], mapData["comment"] if "comment" in mapData else None - ) + loc = LatLngLocation(mapData["lat"], mapData["lon"], mapData["comment"] if "comment" in mapData else None) source = mapData["source"] if "type" in mapData: if mapData["type"] == "item": @@ -290,7 +292,7 @@ def parseItem(self, information): index = filtered[0] result["item"] = information[0:index] result["live"] = information[index] == "!" - result.update(self.parseRegularAprsData(information[index + 1:])) + result.update(self.parseRegularAprsData(information[index + 1 :])) # override type, losing information about compression result["type"] = "item" return result @@ -320,20 +322,17 @@ def parseThirdpartyAprsData(self, information): logger.debug(matches) path = matches.group(2).split(",") destination = next((c.strip("*").upper() for c in path if c.endswith("*")), None) - data = self.parseAprsData({ - "source": matches.group(1).upper(), - "destination": destination, - "path": path, - "data": matches.group(6).encode(encoding) - }) - return { - "type": "thirdparty", - "data": data, - } + data = self.parseAprsData( + { + "source": matches.group(1).upper(), + "destination": destination, + "path": path, + "data": matches.group(6).encode(encoding), + } + ) + return {"type": "thirdparty", "data": data} - return { - "type": "thirdparty", - } + return {"type": "thirdparty"} def parseRegularAprsData(self, information): if self.hasCompressedCoordinates(information): @@ -384,10 +383,7 @@ def decodeHeightGainDirectivity(comment): weather = {} if len(comment) > 6 and comment[3] == "/": try: - weather["wind"] = { - "direction": int(comment[0:3]), - "speed": int(comment[4:7]) * milesToKilometers, - } + weather["wind"] = {"direction": int(comment[0:3]), "speed": int(comment[4:7]) * milesToKilometers} except ValueError: pass comment = comment[7:] diff --git a/owrx/metrics.py b/owrx/metrics.py index b1b577882..1f1780928 100644 --- a/owrx/metrics.py +++ b/owrx/metrics.py @@ -11,9 +11,7 @@ def inc(self, increment=1): self.counter += increment def getValue(self): - return { - "count": self.counter, - } + return {"count": self.counter} class DirectMetric(Metric): diff --git a/owrx/service.py b/owrx/service.py index 167ae1bfd..446088045 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -131,16 +131,15 @@ def updateServices(self): def optimizeResampling(self, freqs, bandwidth): freqs = sorted(freqs, key=lambda f: f["frequency"]) - distances = [{ - "frequency": freqs[i]["frequency"], - "distance": freqs[i+1]["frequency"] - freqs[i]["frequency"], - } for i in range(0, len(freqs)-1)] + distances = [ + {"frequency": freqs[i]["frequency"], "distance": freqs[i + 1]["frequency"] - freqs[i]["frequency"]} + for i in range(0, len(freqs) - 1) + ] distances = [d for d in distances if d["distance"] > 0] distances = sorted(distances, key=lambda f: f["distance"], reverse=True) - def calculate_usage(num_splits): splits = sorted([f["frequency"] for f in distances[0:num_splits]]) previous = 0 @@ -156,20 +155,11 @@ def get_bandwitdh(group): return bandwidth + len(group) * (freqs[-1] - freqs[0] + 24000) total_bandwidth = sum([get_bandwitdh(group) for group in groups]) - return { - "num_splits": num_splits, - "total_bandwidth": total_bandwidth, - "groups": groups, - } - + return {"num_splits": num_splits, "total_bandwidth": total_bandwidth, "groups": groups} usages = [calculate_usage(i) for i in range(0, len(freqs))] # this is simulating no resampling. i haven't seen this as the best result yet - usages += [{ - "num_splits": None, - "total_bandwidth": bandwidth * len(freqs), - "groups": [freqs] - }] + usages += [{"num_splits": None, "total_bandwidth": bandwidth * len(freqs), "groups": [freqs]}] results = sorted(usages, key=lambda f: f["total_bandwidth"]) for r in results: diff --git a/owrx/source.py b/owrx/source.py index 8a1be3fc2..0ec65fa91 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -299,8 +299,7 @@ def start(self): "nc -v 127.0.0.1 {nc_port}".format(nc_port=self.sdr.getPort()), "csdr shift_addition_cc {shift}".format(shift=self.shift), "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING".format( - decimation=self.decimation, - ddc_transition_bw=self.transition_bw, + decimation=self.decimation, ddc_transition_bw=self.transition_bw ), ] @@ -316,11 +315,9 @@ def start(self): self.modificationLock.release() return logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)) - resampler_command += ["nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % ( - nmux_bufsize, - nmux_bufcnt, - self.port, - )] + resampler_command += [ + "nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, self.port) + ] cmd = " | ".join(resampler_command) logger.debug("resampler command: %s", cmd) self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 9d71921b2..082766dba 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -134,7 +134,9 @@ def decoder_commandline(self, file): return [] def decode(self, file): - decoder = subprocess.Popen(self.decoder_commandline(file), stdout=subprocess.PIPE, cwd=self.tmp_dir, preexec_fn=lambda : os.nice(10)) + decoder = subprocess.Popen( + self.decoder_commandline(file), stdout=subprocess.PIPE, cwd=self.tmp_dir, preexec_fn=lambda: os.nice(10) + ) while True: line = decoder.stdout.readline() if line is None or (isinstance(line, bytes) and len(line) == 0): @@ -281,7 +283,6 @@ def pushDecode(self, mode): metric.inc() - def parse_from_jt9(self, msg): # ft8 sample # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' From 711bd18d06f9b8704d09eb7f8e1aa7b8b93dcdde Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 13 Sep 2019 23:14:44 +0200 Subject: [PATCH 0373/2616] update readme with latest features --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 68178af70..67f9cbd93 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,15 @@ It has the following features: - [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN) - [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9) +**News (2019-09-13 by DD5JFK)** +- New set of APRS-related features + - Decode Packet transmissions using [direwolf](https://github.com/wb2osz/direwolf) (1k2 only for now) + - APRS packets are mostly decoded and shown both in a new panel and on the map + - APRS is also available as a background service + - direwolfs I-gate functionality can be enabled, which allows your receiver to work as a receive-only I-gate for the APRS network in the background +- Demodulation for background services has been optimized to use less total bandwidth, saving CPU +- More metrics have been added; they can be used together with collectd and its curl_json plugin for now, with some limitations. + **News (2019-07-21 by DD5JFK)** - Latest Features: - More WSJT-X modes have been added, including the new FT4 mode From 7689d1a2e2372baf10ee5794ee3745314c060a9c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 15 Sep 2019 12:23:11 +0200 Subject: [PATCH 0374/2616] narrow bandpass specifically for wspr --- htdocs/openwebrx.js | 10 +++++++++- owrx/service.py | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index f4111fd2b..4c62590fe 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -2779,7 +2779,6 @@ function demodulator_digital_replace(subtype) case "bpsk31": case "rtty": case "ft8": - case "wspr": case "jt65": case "jt9": case "ft4": @@ -2787,6 +2786,15 @@ function demodulator_digital_replace(subtype) demodulator_analog_replace('usb', true); demodulator_buttons_update(); break; + case "wspr": + secondary_demod_start(subtype); + demodulator_analog_replace('usb', true); + // WSPR only samples between 1400 and 1600 Hz + demodulators[0].low_cut = 1350; + demodulators[0].high_cut = 1650; + demodulators[0].set(); + demodulator_buttons_update(); + break; case "packet": secondary_demod_start(subtype); demodulator_analog_replace('nfm', true); diff --git a/owrx/service.py b/owrx/service.py index 446088045..751724962 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -180,6 +180,10 @@ def setupService(self, mode, frequency, source): if mode == "packet": d.set_demodulator("nfm") d.set_bpf(-4000, 4000) + elif mode == "wspr": + d.set_demodulator("usb") + # WSPR only samples between 1400 and 1600 Hz + d.set_bpf(1350, 1650) else: d.set_demodulator("usb") d.set_bpf(0, 3000) From 392c226cbeca2e5f2d928cf479b92a05b3586296 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 15 Sep 2019 12:23:35 +0200 Subject: [PATCH 0375/2616] overflow metrics --- owrx/wsjt.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 082766dba..1af4ff8a1 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -53,11 +53,17 @@ def __init__(self, maxsize, workers): metrics.addMetric("wsjt.queue.in", self.inCounter) self.outCounter = CounterMetric() metrics.addMetric("wsjt.queue.out", self.outCounter) + self.overflowCounter = CounterMetric() + metrics.addMetric("wsjt.queue.overflow", self.overflowCounter) self.workers = [self.newWorker() for _ in range(0, workers)] def put(self, item): self.inCounter.inc() - super(WsjtQueue, self).put(item, block=False) + try: + super(WsjtQueue, self).put(item, block=False) + except Full: + self.overflowCounter.inc() + raise def get(self, **kwargs): # super.get() is blocking, so it would mess up the stats to inc() first From 5b6edd110d2ab7ff46e6f24e63b4bda9098542ab Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 15 Sep 2019 16:37:12 +0200 Subject: [PATCH 0376/2616] wsjt decoding depth configuration --- config_webrx.py | 7 +++++++ owrx/wsjt.py | 30 ++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index 227cad140..b6d4e1362 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -260,6 +260,13 @@ # if you are running background services, make sure this number is high enough to accept the task influx during peaks # i.e. this should be higher than the number of wsjt services running at the same time wsjt_queue_length = 10 +# wsjt decoding depth will allow more results, but will also consume more cpu +wsjt_decoding_depth = 3 +# can also be set for each mode separately +# jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent +wsjt_decoding_depths = { + "jt65": 1 +} temporary_directory = "/tmp" diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 1af4ff8a1..65d077efc 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -180,6 +180,17 @@ def read(self): except EOFError: return None + def decoding_depth(self, mode): + pm = PropertyManager.getSharedInstance() + # mode-specific setting? + if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]: + return pm["wsjt_decoding_depths"][mode] + # return global default + if "wsjt_decoding_depth" in pm: + return pm["wsjt_decoding_depth"] + # default when no setting is provided + return 3 + class Ft8Chopper(WsjtChopper): def __init__(self, source): @@ -188,8 +199,7 @@ def __init__(self, source): super().__init__(source) def decoder_commandline(self, file): - # TODO expose decoding quality parameters through config - return ["jt9", "--ft8", "-d", "3", file] + return ["jt9", "--ft8", "-d", str(self.decoding_depth("ft8")), file] class WsprChopper(WsjtChopper): @@ -199,8 +209,11 @@ def __init__(self, source): super().__init__(source) def decoder_commandline(self, file): - # TODO expose decoding quality parameters through config - return ["wsprd", "-d", file] + cmd = ["wsprd"] + if self.decoding_depth("wspr") > 1: + cmd += ["-d"] + cmd += [file] + return cmd class Jt65Chopper(WsjtChopper): @@ -210,8 +223,7 @@ def __init__(self, source): super().__init__(source) def decoder_commandline(self, file): - # TODO expose decoding quality parameters through config - return ["jt9", "--jt65", "-d", "1", file] + return ["jt9", "--jt65", "-d", str(self.decoding_depth("jt65")), file] class Jt9Chopper(WsjtChopper): @@ -221,8 +233,7 @@ def __init__(self, source): super().__init__(source) def decoder_commandline(self, file): - # TODO expose decoding quality parameters through config - return ["jt9", "--jt9", "-d", "3", file] + return ["jt9", "--jt9", "-d", str(self.decoding_depth("jt9")), file] class Ft4Chopper(WsjtChopper): @@ -232,8 +243,7 @@ def __init__(self, source): super().__init__(source) def decoder_commandline(self, file): - # TODO expose decoding quality parameters through config - return ["jt9", "--ft4", "-d", "3", file] + return ["jt9", "--ft4", "-d", str(self.decoding_depth("ft4")), file] class WsjtParser(object): From b0b2df5422ceb022b206317934431a3a724823c3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 15 Sep 2019 21:10:30 +0200 Subject: [PATCH 0377/2616] no need for shared instances here --- openwebrx.py | 4 ++-- owrx/service.py | 11 ++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/openwebrx.py b/openwebrx.py index 9fa685bd8..a01334923 100755 --- a/openwebrx.py +++ b/openwebrx.py @@ -7,7 +7,7 @@ from owrx.source import SdrService, ClientRegistry from socketserver import ThreadingMixIn from owrx.sdrhu import SdrHuUpdater -from owrx.service import ServiceManager +from owrx.service import Services import logging @@ -48,7 +48,7 @@ def main(): updater = SdrHuUpdater() updater.start() - ServiceManager.getSharedInstance().start() + Services.start() server = ThreadedHttpServer(("0.0.0.0", pm.getPropertyValue("web_port")), RequestHandler) server.serve_forever() diff --git a/owrx/service.py b/owrx/service.py index 751724962..89e68aae9 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -205,16 +205,9 @@ def write_aprs_data(self, data): pass -class ServiceManager(object): - sharedInstance = None - +class Services(object): @staticmethod - def getSharedInstance(): - if ServiceManager.sharedInstance is None: - ServiceManager.sharedInstance = ServiceManager() - return ServiceManager.sharedInstance - - def start(self): + def start(): if not PropertyManager.getSharedInstance()["services_enabled"]: return for source in SdrService.getSources().values(): From 8df4f9ce5252ac20a7777ec0b9efd1bfebe9de87 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 16 Sep 2019 00:31:35 +0200 Subject: [PATCH 0378/2616] add the ability to schedule profiles to be used when sources are idle --- owrx/service.py | 181 ++++++++++++++++++++++++++++++++++++++++-------- owrx/source.py | 15 +++- 2 files changed, 167 insertions(+), 29 deletions(-) diff --git a/owrx/service.py b/owrx/service.py index 89e68aae9..752d1b3f2 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -1,5 +1,6 @@ import threading import socket +from datetime import datetime, timezone, timedelta from owrx.source import SdrService from owrx.bands import Bandplan from csdr import dsp, output @@ -44,32 +45,154 @@ def supports_type(self, t): return t == "packet_demod" +class ScheduleEntry(object): + def __init__(self, startTime, endTime, profile): + self.startTime = startTime + self.endTime = endTime + self.profile = profile + + def isCurrent(self, time): + if self.startTime < self.endTime: + return self.startTime <= time < self.endTime + else: + return self.startTime <= time or time < self.endTime + + def getProfile(self): + return self.profile + + def getScheduledEnd(self): + now = datetime.utcnow() + end = now.combine(date=now.date(), time=self.endTime) + while end < now: + end += timedelta(days=1) + return end + + def getNextActivation(self): + now = datetime.utcnow() + start = now.combine(date=now.date(), time=self.startTime) + while start < now: + start += timedelta(days=1) + return start + + +class Schedule(object): + @staticmethod + def parse(scheduleDict): + entries = [] + for time, profile in scheduleDict.items(): + if len(time) != 9: + logger.warning("invalid schedule spec: %s", time) + continue + + startTime = datetime.strptime(time[0:4], "%H%M").replace(tzinfo=timezone.utc).time() + endTime = datetime.strptime(time[5:9], "%H%M").replace(tzinfo=timezone.utc).time() + entries.append(ScheduleEntry(startTime, endTime, profile)) + return Schedule(entries) + + def __init__(self, entries): + self.entries = entries + + def getCurrentEntry(self): + current = [p for p in self.entries if p.isCurrent(datetime.utcnow().time())] + if current: + return current[0] + return None + + def getNextEntry(self): + s = sorted(self.entries, key=lambda e: e.getNextActivation()) + if s: + return s[0] + return None + + +class ServiceScheduler(object): + def __init__(self, source, schedule): + self.source = source + self.schedule = Schedule.parse(schedule) + self.active = False + self.source.addClient(self) + self.selectionTimer = None + self.scheduleSelection() + + def scheduleSelection(self, time=None): + seconds = 10 + if time is not None: + delta = time - datetime.utcnow() + seconds = delta.total_seconds() + if self.selectionTimer: + self.selectionTimer.cancel() + self.selectionTimer = threading.Timer(seconds, self.selectProfile) + self.selectionTimer.start() + + def isActive(self): + return self.active + + def onSdrAvailable(self): + pass + + def onSdrUnavailable(self): + self.scheduleSelection() + + def selectProfile(self): + self.active = False + if self.source.hasActiveClients(): + logger.debug("source has active clients; not touching") + return + logger.debug("source seems to be idle, selecting profile for background services") + entry = self.schedule.getCurrentEntry() + + if entry is None: + logger.debug("schedule did not return a profile. checking next entry...") + nextEntry = self.schedule.getNextEntry() + if nextEntry is not None: + self.scheduleSelection(nextEntry.getNextActivation()) + return + + logger.debug("scheduling end for current profile: %s", entry.getScheduledEnd()) + self.scheduleSelection(entry.getScheduledEnd()) + + try: + self.active = True + self.source.activateProfile(entry.getProfile()) + self.source.start() + except KeyError: + pass + + class ServiceHandler(object): def __init__(self, source): + self.lock = threading.Lock() self.services = [] self.source = source self.startupTimer = None self.source.addClient(self) - self.source.getProps().collect("center_freq", "samp_rate").wire(self.onFrequencyChange) - self.scheduleServiceStartup() + props = self.source.getProps() + props.collect("center_freq", "samp_rate").wire(self.onFrequencyChange) + if self.source.isAvailable(): + self.scheduleServiceStartup() + if "schedule" in props: + ServiceScheduler(self.source, props["schedule"]) + + def isActive(self): + return False def onSdrAvailable(self): self.scheduleServiceStartup() def onSdrUnavailable(self): + logger.debug("sdr source becoming unavailable; stopping services.") self.stopServices() def isSupported(self, mode): return mode in PropertyManager.getSharedInstance()["services_decoders"] def stopServices(self): - for service in self.services: - service.stop() - self.services = [] + with self.lock: + services = self.services + self.services = [] - def startServices(self): - for service in self.services: - service.start() + for service in services: + service.stop() def onFrequencyChange(self, key, value): self.stopServices() @@ -94,6 +217,9 @@ def getAvailablePort(self): def updateServices(self): logger.debug("re-scheduling services due to sdr changes") self.stopServices() + if not self.source.isAvailable(): + logger.debug("sdr source is unavailable") + return cf = self.source.getProps()["center_freq"] sr = self.source.getProps()["samp_rate"] srh = sr / 2 @@ -109,25 +235,26 @@ def updateServices(self): logger.debug("no services available") return - self.services = [] - - for group in self.optimizeResampling(dials, sr): - frequencies = sorted([f["frequency"] for f in group]) - min = frequencies[0] - max = frequencies[-1] - cf = (min + max) / 2 - bw = max - min - logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw)) - resampler_props = PropertyManager() - resampler_props["center_freq"] = cf - # TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths - resampler_props["samp_rate"] = bw + 24000 - resampler = Resampler(resampler_props, self.getAvailablePort(), self.source) - resampler.start() - self.services.append(resampler) - - for dial in group: - self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler)) + with self.lock: + self.services = [] + + for group in self.optimizeResampling(dials, sr): + frequencies = sorted([f["frequency"] for f in group]) + min = frequencies[0] + max = frequencies[-1] + cf = (min + max) / 2 + bw = max - min + logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw)) + resampler_props = PropertyManager() + resampler_props["center_freq"] = cf + # TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths + resampler_props["samp_rate"] = bw + 24000 + resampler = Resampler(resampler_props, self.getAvailablePort(), self.source) + resampler.start() + self.services.append(resampler) + + for dial in group: + self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler)) def optimizeResampling(self, freqs, bandwidth): freqs = sorted(freqs, key=lambda f: f["frequency"]) diff --git a/owrx/source.py b/owrx/source.py index 0ec65fa91..ba31bdc1a 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -243,16 +243,21 @@ def stop(self): def sleepOnRestart(self): pass + def hasActiveClients(self): + activeClients = [c for c in self.clients if c.isActive()] + return len(activeClients) > 0 + def addClient(self, c): self.clients.append(c) - self.start() + if self.hasActiveClients(): + self.start() def removeClient(self, c): try: self.clients.remove(c) except ValueError: pass - if not self.clients: + if not self.hasActiveClients(): self.stop() def addSpectrumClient(self, c): @@ -478,6 +483,9 @@ def stop(self): c.cancel() self.subscriptions = [] + def isActive(self): + return True + def onSdrAvailable(self): self.dsp.start() @@ -606,6 +614,9 @@ def stop(self): def setProperty(self, prop, value): self.localProps.getProperty(prop).setValue(value) + def isActive(self): + return True + def onSdrAvailable(self): logger.debug("received onSdrAvailable, attempting DspSource restart") self.dsp.start() From 243e73064a9b8e50d33898e1e502744920b0f122 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 17 Sep 2019 18:44:37 +0200 Subject: [PATCH 0379/2616] add band information to ysf locations --- owrx/meta.py | 19 +++++++++++++++---- owrx/source.py | 1 + 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/owrx/meta.py b/owrx/meta.py index 1d7a63f1d..4cf507e07 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -5,6 +5,7 @@ import logging import threading from owrx.map import Map, LatLngLocation +from owrx.bands import Bandplan logger = logging.getLogger(__name__) @@ -71,19 +72,29 @@ def enrich(self, meta): class YsfMetaEnricher(object): + def __init__(self, parser): + self.parser = parser + def enrich(self, meta): if "source" in meta and "lat" in meta and "lon" in meta: # TODO parsing the float values should probably happen earlier loc = LatLngLocation(float(meta["lat"]), float(meta["lon"])) - Map.getSharedInstance().updateLocation(meta["source"], loc, "YSF") + Map.getSharedInstance().updateLocation(meta["source"], loc, "YSF", self.parser.getBand()) return None class MetaParser(object): - enrichers = {"DMR": DmrMetaEnricher(), "YSF": YsfMetaEnricher()} def __init__(self, handler): self.handler = handler + self.enrichers = {"DMR": DmrMetaEnricher(), "YSF": YsfMetaEnricher(self)} + self.band = None + + def setDialFrequency(self, freq): + self.band = Bandplan.getSharedInstance().findBand(freq) + + def getBand(self): + return self.band def parse(self, meta): fields = meta.split(";") @@ -91,8 +102,8 @@ def parse(self, meta): if "protocol" in meta: protocol = meta["protocol"] - if protocol in MetaParser.enrichers: - additional_data = MetaParser.enrichers[protocol].enrich(meta) + if protocol in self.enrichers: + additional_data = self.enrichers[protocol].enrich(meta) if additional_data is not None: meta["additional"] = additional_data self.handler.write_metadata(meta) diff --git a/owrx/source.py b/owrx/source.py index ba31bdc1a..ad39480e7 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -537,6 +537,7 @@ def set_dial_freq(key, value): freq = self.localProps["center_freq"] + self.localProps["offset_freq"] self.wsjtParser.setDialFrequency(freq) self.aprsParser.setDialFrequency(freq) + self.metaParser.setDialFrequency(freq) self.subscriptions = [ self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression), From 3814767e28ece9c43c34b978936a66c1e68abe84 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 18 Sep 2019 01:46:09 +0200 Subject: [PATCH 0380/2616] count errors --- owrx/wsjt.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 65d077efc..531c37264 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -32,6 +32,7 @@ def run(self) -> None: processor.decode(file) except Exception: logger.exception("failed to decode job") + self.queue.onError() self.queue.task_done() @@ -55,6 +56,8 @@ def __init__(self, maxsize, workers): metrics.addMetric("wsjt.queue.out", self.outCounter) self.overflowCounter = CounterMetric() metrics.addMetric("wsjt.queue.overflow", self.overflowCounter) + self.errorCounter = CounterMetric() + metrics.addMetric("wsjt.queue.error", self.errorCounter) self.workers = [self.newWorker() for _ in range(0, workers)] def put(self, item): @@ -76,6 +79,9 @@ def newWorker(self): worker.start() return worker + def onError(self): + self.errorCounter.inc() + class WsjtChopper(threading.Thread): def __init__(self, source): From 6f983ccb6b8b534bacab173913eda7accfccf821 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 18 Sep 2019 01:46:31 +0200 Subject: [PATCH 0381/2616] synchronize scheduler access --- owrx/wsjt.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 531c37264..7b588ff08 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -90,6 +90,7 @@ def __init__(self, source): (self.wavefilename, self.wavefile) = self.getWaveFile() self.switchingLock = threading.Lock() self.scheduler = sched.scheduler(time.time, time.sleep) + self.schedulerLock = threading.Lock() (self.outputReader, self.outputWriter) = Pipe() self.doRun = True super().__init__() @@ -118,11 +119,13 @@ def startScheduler(self): threading.Thread(target=self.scheduler.run).start() def emptyScheduler(self): - for event in self.scheduler.queue: - self.scheduler.cancel(event) + with self.schedulerLock: + for event in self.scheduler.queue: + self.scheduler.cancel(event) def _scheduleNextSwitch(self): - self.scheduler.enterabs(self.getNextDecodingTime(), 1, self.switchFiles) + with self.schedulerLock: + self.scheduler.enterabs(self.getNextDecodingTime(), 1, self.switchFiles) def switchFiles(self): self.switchingLock.acquire() From 30512e347a78c444a14c03450490de6888c1eb04 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 18 Sep 2019 15:40:23 +0200 Subject: [PATCH 0382/2616] fix more threading issues; add users metric --- owrx/metrics.py | 8 ++++++-- owrx/source.py | 8 ++++++-- owrx/wsjt.py | 11 +++++++---- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/owrx/metrics.py b/owrx/metrics.py index 1f1780928..28444047e 100644 --- a/owrx/metrics.py +++ b/owrx/metrics.py @@ -1,3 +1,5 @@ +import threading + class Metric(object): def getValue(self): return 0 @@ -24,11 +26,13 @@ def getValue(self): class Metrics(object): sharedInstance = None + creationLock = threading.Lock() @staticmethod def getSharedInstance(): - if Metrics.sharedInstance is None: - Metrics.sharedInstance = Metrics() + with Metrics.creationLock: + if Metrics.sharedInstance is None: + Metrics.sharedInstance = Metrics() return Metrics.sharedInstance def __init__(self): diff --git a/owrx/source.py b/owrx/source.py index ad39480e7..eff9e29f3 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -4,6 +4,7 @@ from owrx.meta import MetaParser from owrx.wsjt import WsjtParser from owrx.aprs import AprsParser +from owrx.metrics import Metrics, DirectMetric import threading import csdr import time @@ -698,15 +699,18 @@ class TooManyClientsException(Exception): class ClientRegistry(object): sharedInstance = None + creationLock = threading.Lock() @staticmethod def getSharedInstance(): - if ClientRegistry.sharedInstance is None: - ClientRegistry.sharedInstance = ClientRegistry() + with ClientRegistry.creationLock: + if ClientRegistry.sharedInstance is None: + ClientRegistry.sharedInstance = ClientRegistry() return ClientRegistry.sharedInstance def __init__(self): self.clients = [] + Metrics.getSharedInstance().addMetric("openwebrx.users", DirectMetric(self.clientCount)) super().__init__() def broadcast(self): diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 7b588ff08..f73e10511 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -38,12 +38,14 @@ def run(self) -> None: class WsjtQueue(Queue): sharedInstance = None + creationLock = threading.Lock() @staticmethod def getSharedInstance(): - if WsjtQueue.sharedInstance is None: - pm = PropertyManager.getSharedInstance() - WsjtQueue.sharedInstance = WsjtQueue(maxsize=pm["wsjt_queue_length"], workers=pm["wsjt_queue_workers"]) + with WsjtQueue.creationLock: + if WsjtQueue.sharedInstance is None: + pm = PropertyManager.getSharedInstance() + WsjtQueue.sharedInstance = WsjtQueue(maxsize=pm["wsjt_queue_length"], workers=pm["wsjt_queue_workers"]) return WsjtQueue.sharedInstance def __init__(self, maxsize, workers): @@ -125,7 +127,8 @@ def emptyScheduler(self): def _scheduleNextSwitch(self): with self.schedulerLock: - self.scheduler.enterabs(self.getNextDecodingTime(), 1, self.switchFiles) + if self.doRun: + self.scheduler.enterabs(self.getNextDecodingTime(), 1, self.switchFiles) def switchFiles(self): self.switchingLock.acquire() From c6c4012a3629fd5e98756c6a67a63b2afb9c1ecf Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 18 Sep 2019 17:22:35 +0200 Subject: [PATCH 0383/2616] add aprs symbols to http server --- config_webrx.py | 3 +++ owrx/controllers.py | 21 +++++++++++++++++++-- owrx/http.py | 10 ++++++---- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index b6d4e1362..bfd362d1b 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -281,3 +281,6 @@ aprs_igate_password = "" # beacon uses the receiver_gps setting, so if you enable this, make sure the location is correct there aprs_igate_beacon = False + +# path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols) +aprs_symbols_path = "/opt/aprs-symbols/png" diff --git a/owrx/controllers.py b/owrx/controllers.py index f7ce7e0df..a3b232f0d 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -55,9 +55,15 @@ def handle_request(self): class AssetsController(Controller): + def __init__(self, handler, request, path): + if not path.endswith("/"): + path += "/" + self.path = path + super().__init__(handler, request) + def serve_file(self, file, content_type=None): try: - modified = datetime.fromtimestamp(os.path.getmtime("htdocs/" + file)) + modified = datetime.fromtimestamp(os.path.getmtime(self.path + file)) if "If-Modified-Since" in self.handler.headers: client_modified = datetime.strptime( @@ -67,7 +73,7 @@ def serve_file(self, file, content_type=None): self.send_response("", code=304) return - f = open("htdocs/" + file, "rb") + f = open(self.path + file, "rb") data = f.read() f.close() @@ -82,6 +88,17 @@ def handle_request(self): self.serve_file(filename) +class OwrxAssetsController(AssetsController): + def __init__(self, handler, request): + super().__init__(handler, request, "htdocs/") + + +class AprsSymbolsController(AssetsController): + def __init__(self, handler, request): + pm = PropertyManager.getSharedInstance() + super().__init__(handler, request, pm["aprs_symbols_path"]) + + class TemplateController(Controller): def render_template(self, file, **vars): f = open("htdocs/" + file, "r") diff --git a/owrx/http.py b/owrx/http.py index 189dd9585..f15b974d7 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -1,12 +1,13 @@ from owrx.controllers import ( StatusController, IndexController, - AssetsController, + OwrxAssetsController, WebSocketController, MapController, FeatureController, ApiController, MetricsController, + AprsSymbolsController, ) from http.server import BaseHTTPRequestHandler import re @@ -36,11 +37,12 @@ class Router(object): mappings = [ {"route": "/", "controller": IndexController}, {"route": "/status", "controller": StatusController}, - {"regex": "/static/(.+)", "controller": AssetsController}, + {"regex": "/static/(.+)", "controller": OwrxAssetsController}, + {"regex": "/aprs-symbols/(.+)", "controller": AprsSymbolsController}, {"route": "/ws/", "controller": WebSocketController}, - {"regex": "(/favicon.ico)", "controller": AssetsController}, + {"regex": "(/favicon.ico)", "controller": OwrxAssetsController}, # backwards compatibility for the sdr.hu portal - {"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController}, + {"regex": "/(gfx/openwebrx-avatar.png)", "controller": OwrxAssetsController}, {"route": "/map", "controller": MapController}, {"route": "/features", "controller": FeatureController}, {"route": "/api/features", "controller": ApiController}, From 3e8e0c92241b73c1fc65da8492771546d7c782ed Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 18 Sep 2019 18:50:48 +0200 Subject: [PATCH 0384/2616] first work on custom aprs icons --- htdocs/map.js | 13 ++++++++++++- owrx/aprs.py | 38 +++++++++++++++++++++++++++++--------- owrx/map.py | 5 +---- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/htdocs/map.js b/htdocs/map.js index 190b0baf2..da0ccbb0e 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -107,11 +107,22 @@ }); markers[update.callsign] = marker; } + var iconOptions = {} + if (update.location.symbol) { + var index = update.location.symbol.index; + var tableId = update.location.symbol.table == '/' ? 0 : 1; + iconOptions.icon = { + url: '/aprs-symbols/aprs-symbols-24-' + tableId + '.png', + size: new google.maps.Size(24, 24), + origin: new google.maps.Point((index % 16) * 24, Math.floor(index / 16) * 24), + anchor: new google.maps.Point(12, 12), + }; + } marker.setOptions($.extend({ position: pos, map: map, title: update.callsign - }, getMarkerOpacityOptions(update.lastseen) )); + }, iconOptions, getMarkerOpacityOptions(update.lastseen) )); marker.lastseen = update.lastseen; marker.mode = update.mode; marker.band = update.band; diff --git a/owrx/aprs.py b/owrx/aprs.py index 967eae555..d84304d94 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -131,6 +131,21 @@ def getRemainder(self): return self.data +class AprsLocation(LatLngLocation): + def __init__(self, data): + super().__init__(data["lat"], data["lon"]) + self.comment = data["comment"] if "comment" in data else None + self.symbol = data["symbol"] if "symbol" in data else None + + def __dict__(self): + res = super(AprsLocation, self).__dict__() + if self.comment is not None: + res["comment"] = self.comment + if self.symbol is not None: + res["symbol"] = self.symbol + return res + + class AprsParser(object): def __init__(self, handler): self.ax25parser = Ax25Parser() @@ -176,7 +191,7 @@ def updateMap(self, mapData): if "type" in mapData and mapData["type"] == "thirdparty" and "data" in mapData: mapData = mapData["data"] if "lat" in mapData and "lon" in mapData: - loc = LatLngLocation(mapData["lat"], mapData["lon"], mapData["comment"] if "comment" in mapData else None) + loc = AprsLocation(mapData) source = mapData["source"] if "type" in mapData: if mapData["type"] == "item": @@ -195,14 +210,17 @@ def parseUncompressedCoordinates(self, raw): lon = int(raw[9:12]) + float(raw[12:17]) / 60 if raw[17] == "W": lon *= -1 - return {"lat": lat, "lon": lon, "symboltable": raw[8], "symbol": raw[18]} + return {"lat": lat, "lon": lon, "symbol": {"table": raw[8], "symbol": raw[18], "index": ord(raw[18]) - 33}} def parseCompressedCoordinates(self, raw): return { "lat": 90 - decodeBase91(raw[1:5]) / 380926, "lon": -180 + decodeBase91(raw[5:9]) / 190463, - "symboltable": raw[0], - "symbol": raw[9], + "symbol": { + "table": raw[0], + "symbol": raw[9], + "index": ord(raw[9]) - 33 + }, } def parseTimestamp(self, raw): @@ -219,7 +237,6 @@ def parseTimestamp(self, raw): ts = ts.replace(tzinfo=now.tzinfo) else: logger.warning("invalid timezone info byte: %s", raw[6]) - logger.debug(ts) return int(ts.timestamp() * 1000) def parseStatusUpate(self, raw): @@ -319,7 +336,6 @@ def parseMessage(self, information): def parseThirdpartyAprsData(self, information): matches = thirdpartyeRegex.match(information) if matches: - logger.debug(matches) path = matches.group(2).split(",") destination = next((c.strip("*").upper() for c in path if c.endswith("*")), None) data = self.parseAprsData( @@ -378,7 +394,8 @@ def decodeHeightGainDirectivity(comment): return res # aprs data extensions - if "symbol" in aprsData and aprsData["symbol"] == "_": + # yes, weather stations are officially identified by their symbols. go figure... + if "symbol" in aprsData and aprsData["symbol"]["index"] == 62: # weather report weather = {} if len(comment) > 6 and comment[3] == "/": @@ -558,6 +575,9 @@ def parse(self, data): "course": course, "device": device, "type": "Mic-E", - "symboltable": chr(information[8]), - "symbol": chr(information[7]), + "symbol": { + "table": chr(information[8]), + "symbol": chr(information[7]), + "index": information[7] - 33 + } } diff --git a/owrx/map.py b/owrx/map.py index 65cc2ca60..a27a0e0ab 100644 --- a/owrx/map.py +++ b/owrx/map.py @@ -92,15 +92,12 @@ def removeOldPositions(self): class LatLngLocation(Location): - def __init__(self, lat: float, lon: float, comment=None): + def __init__(self, lat: float, lon: float): self.lat = lat self.lon = lon - self.comment = comment def __dict__(self): res = {"type": "latlon", "lat": self.lat, "lon": self.lon} - if self.comment is not None: - res["comment"] = self.comment return res From e231c07c806d8da1faac00b72c466178d725e268 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 18 Sep 2019 19:41:37 +0200 Subject: [PATCH 0385/2616] 2x resolution for retina displays --- htdocs/map.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/htdocs/map.js b/htdocs/map.js index da0ccbb0e..59dcdb07b 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -112,10 +112,11 @@ var index = update.location.symbol.index; var tableId = update.location.symbol.table == '/' ? 0 : 1; iconOptions.icon = { - url: '/aprs-symbols/aprs-symbols-24-' + tableId + '.png', + url: '/aprs-symbols/aprs-symbols-24-' + tableId + '@2x.png', size: new google.maps.Size(24, 24), origin: new google.maps.Point((index % 16) * 24, Math.floor(index / 16) * 24), anchor: new google.maps.Point(12, 12), + scaledSize: new google.maps.Size(384, 144), }; } marker.setOptions($.extend({ From 996422ff4b06ce7bb8108f2757b4061e07d477ce Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 19 Sep 2019 00:18:51 +0200 Subject: [PATCH 0386/2616] show aprs symbols in decoding list, too --- htdocs/css/openwebrx.css | 19 +++++++++++++++++++ htdocs/openwebrx.js | 28 +++++++++++++++++++++++++++- owrx/aprs.py | 23 ++++++++++++----------- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 516facd3f..26941f028 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -870,6 +870,25 @@ img.openwebrx-mirror-img text-align: center; } +.aprs-symbol { + display: inline-block; + width: 15px; + height: 15px; + background-size: 240px 90px; +} + +.aprs-symboltable-normal { + background-image: url(../../aprs-symbols/aprs-symbols-24-0.png) +} + +.aprs-symboltable-alternate { + background-image: url(../../aprs-symbols/aprs-symbols-24-1.png) +} + +.aprs-symboltable-overlay { + background-image: url(../../aprs-symbols/aprs-symbols-24-2.png) +} + #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container, diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 4c62590fe..ea1cbbbc3 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1482,8 +1482,34 @@ function update_packet_panel(msg) { } var link = ''; + var classes = []; + var styles = {}; + var overlay = ''; + var stylesToString = function(s) { + return $.map(s, function(value, key){ return key + ':' + value + ';'}).join('') + } + if (msg.symbol) { + classes.push('aprs-symbol'); + classes.push('aprs-symboltable-' + (msg.symbol.table == '/' ? 'normal' : 'alternate')); + styles['background-position-x'] = -(msg.symbol.index % 16) * 15 + 'px'; + styles['background-position-y'] = -Math.floor(msg.symbol.index / 16) * 15 + 'px'; + if (msg.symbol.table != '/' && msg.symbol.table != '\\') { + s = {} + s['background-position-x'] = -(msg.symbol.tableindex % 16) * 15 + 'px'; + s['background-position-y'] = -Math.floor(msg.symbol.tableindex / 16) * 15 + 'px'; + overlay='
    '; + } + } else if (msg.lat && msg.lon) { + classes.push('openwebrx-maps-pin'); + } + var attrs = [ + 'class="' + classes.join(' ') + '"', + 'style="' + stylesToString(styles) + '"', + ].join(' '); if (msg.lat && msg.lon) { - link = ''; + link = '' + overlay + ''; + } else { + link = '
    ' + overlay + '
    ' } $b.append($( diff --git a/owrx/aprs.py b/owrx/aprs.py index d84304d94..2653f9a82 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -37,6 +37,15 @@ def decodeBase91(input): base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 return base + (ord(input[-1]) - 33) +def getSymbolData(symbol, table): + return { + "symbol": symbol, + "table": table, + "index": ord(symbol) - 33, + "tableindex": ord(table) - 33, + } + + class Ax25Parser(object): def parse(self, ax25frame): @@ -210,17 +219,13 @@ def parseUncompressedCoordinates(self, raw): lon = int(raw[9:12]) + float(raw[12:17]) / 60 if raw[17] == "W": lon *= -1 - return {"lat": lat, "lon": lon, "symbol": {"table": raw[8], "symbol": raw[18], "index": ord(raw[18]) - 33}} + return {"lat": lat, "lon": lon, "symbol": getSymbolData(raw[18], raw[8])} def parseCompressedCoordinates(self, raw): return { "lat": 90 - decodeBase91(raw[1:5]) / 380926, "lon": -180 + decodeBase91(raw[5:9]) / 190463, - "symbol": { - "table": raw[0], - "symbol": raw[9], - "index": ord(raw[9]) - 33 - }, + "symbol": getSymbolData(raw[9], raw[0]), } def parseTimestamp(self, raw): @@ -575,9 +580,5 @@ def parse(self, data): "course": course, "device": device, "type": "Mic-E", - "symbol": { - "table": chr(information[8]), - "symbol": chr(information[7]), - "index": information[7] - 33 - } + "symbol": getSymbolData(chr(information[7]), chr(information[8])) } From 15c28b130deb985a1ce6bdfd30a5937a31d565eb Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 19 Sep 2019 01:35:58 +0200 Subject: [PATCH 0387/2616] use custom marker class to solve overlay problem (and enable rotation at a later point) --- htdocs/lib/AprsMarker.js | 71 ++++++++++++++++++++++++++++++++++++++++ htdocs/map.js | 26 +++++++-------- 2 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 htdocs/lib/AprsMarker.js diff --git a/htdocs/lib/AprsMarker.js b/htdocs/lib/AprsMarker.js new file mode 100644 index 000000000..fd4dde8fa --- /dev/null +++ b/htdocs/lib/AprsMarker.js @@ -0,0 +1,71 @@ +function AprsMarker() {} + +AprsMarker.prototype = new google.maps.OverlayView(); + +AprsMarker.prototype.draw = function() { + var div = this.div; + var overlay = this.overlay; + + if (this.symbol) { + var tableId = this.symbol.table == '/' ? 0 : 1; + div.style.background = 'url(/aprs-symbols/aprs-symbols-24-' + tableId + '@2x.png)'; + div.style['background-size'] = '384px 144px'; + div.style['background-position-x'] = -(this.symbol.index % 16) * 24 + 'px'; + div.style['background-position-y'] = -Math.floor(this.symbol.index / 16) * 24 + 'px'; + } + + if (this.symbol.table != '/' && this.symbol.table != '\\') { + overlay.style.display = 'block'; + overlay.style['background-position-x'] = -(this.symbol.tableindex % 16) * 24 + 'px'; + overlay.style['background-position-y'] = -Math.floor(this.symbol.tableindex / 16) * 24 + 'px'; + } else { + overlay.style.display = 'none'; + } + + var point = this.getProjection().fromLatLngToDivPixel(this.position); + + if (point) { + div.style.left = point.x - 12 + 'px'; + div.style.top = point.y - 12 + 'px'; + } +}; + +AprsMarker.prototype.onAdd = function() { + var div = this.div = document.createElement('div'); + + div.className = 'marker'; + + div.style.position = 'absolute'; + div.style.cursor = 'pointer'; + div.style.width = '24px'; + div.style.height = '24px'; + + var overlay = this.overlay = document.createElement('div'); + overlay.style.width = '24px'; + overlay.style.height = '24px'; + overlay.style.background = 'url(/aprs-symbols/aprs-symbols-24-2@2x.png)'; + overlay.style['background-size'] = '384px 144px'; + overlay.style.display = 'none'; + + div.appendChild(overlay); + + var self = this; + google.maps.event.addDomListener(div, "click", function(event) { + event.stopPropagation(); + google.maps.event.trigger(self, "click", event); + }); + + var panes = this.getPanes(); + panes.overlayImage.appendChild(div); +} + +AprsMarker.prototype.remove = function() { + if (this.div) { + this.div.parentNode.removeChild(this.div); + this.div = null; + } +}; + +AprsMarker.prototype.getAnchorPoint = function() { + return new google.maps.Point(0, -12); +} diff --git a/htdocs/map.js b/htdocs/map.js index 59dcdb07b..064351d24 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -98,27 +98,21 @@ case 'latlon': var pos = new google.maps.LatLng(update.location.lat, update.location.lon); var marker; + var markerClass = google.maps.Marker; + var iconOptions = {} + if (update.location.symbol) { + markerClass = AprsMarker; + iconOptions.symbol = update.location.symbol; + } if (markers[update.callsign]) { marker = markers[update.callsign]; } else { - marker = new google.maps.Marker(); + marker = new markerClass(); marker.addListener('click', function(){ showMarkerInfoWindow(update.callsign, pos); }); markers[update.callsign] = marker; } - var iconOptions = {} - if (update.location.symbol) { - var index = update.location.symbol.index; - var tableId = update.location.symbol.table == '/' ? 0 : 1; - iconOptions.icon = { - url: '/aprs-symbols/aprs-symbols-24-' + tableId + '@2x.png', - size: new google.maps.Size(24, 24), - origin: new google.maps.Point((index % 16) * 24, Math.floor(index / 16) * 24), - anchor: new google.maps.Point(12, 12), - scaledSize: new google.maps.Size(384, 144), - }; - } marker.setOptions($.extend({ position: pos, map: map, @@ -220,12 +214,14 @@ }, zoom: 5 }); - processUpdates(updateQueue); - updateQueue = []; $.getScript("/static/lib/nite-overlay.js").done(function(){ nite.init(map); setInterval(function() { nite.refresh() }, 10000); // every 10s }); + $.getScript('/static/lib/AprsMarker.js').done(function(){ + processUpdates(updateQueue); + updateQueue = []; + }); map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($(".openwebrx-map-legend")[0]); }); retention_time = config.map_position_retention_time * 1000; From ecbae5af2d3d0a2d9e2d1aba2af156ead26ea15e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 19 Sep 2019 02:25:32 +0200 Subject: [PATCH 0388/2616] implement icon rotation --- htdocs/lib/AprsMarker.js | 12 ++++++++++-- htdocs/map.js | 8 +++++--- owrx/aprs.py | 10 ++++------ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/htdocs/lib/AprsMarker.js b/htdocs/lib/AprsMarker.js index fd4dde8fa..1d7bd3dc5 100644 --- a/htdocs/lib/AprsMarker.js +++ b/htdocs/lib/AprsMarker.js @@ -14,6 +14,16 @@ AprsMarker.prototype.draw = function() { div.style['background-position-y'] = -Math.floor(this.symbol.index / 16) * 24 + 'px'; } + if (this.course) { + if (this.course > 180) { + div.style.transform = 'scalex(-1) rotate(' + (270 - this.course) + 'deg)' + } else { + div.style.transform = 'rotate(' + (this.course - 90) + 'deg)'; + } + } else { + div.style.transform = null; + } + if (this.symbol.table != '/' && this.symbol.table != '\\') { overlay.style.display = 'block'; overlay.style['background-position-x'] = -(this.symbol.tableindex % 16) * 24 + 'px'; @@ -33,8 +43,6 @@ AprsMarker.prototype.draw = function() { AprsMarker.prototype.onAdd = function() { var div = this.div = document.createElement('div'); - div.className = 'marker'; - div.style.position = 'absolute'; div.style.cursor = 'pointer'; div.style.width = '24px'; diff --git a/htdocs/map.js b/htdocs/map.js index 064351d24..20d18c1cf 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -99,10 +99,12 @@ var pos = new google.maps.LatLng(update.location.lat, update.location.lon); var marker; var markerClass = google.maps.Marker; - var iconOptions = {} + var aprsOptions = {} if (update.location.symbol) { markerClass = AprsMarker; - iconOptions.symbol = update.location.symbol; + aprsOptions.symbol = update.location.symbol; + aprsOptions.course = update.location.course; + aprsOptions.speed = update.location.speed; } if (markers[update.callsign]) { marker = markers[update.callsign]; @@ -117,7 +119,7 @@ position: pos, map: map, title: update.callsign - }, iconOptions, getMarkerOpacityOptions(update.lastseen) )); + }, aprsOptions, getMarkerOpacityOptions(update.lastseen) )); marker.lastseen = update.lastseen; marker.mode = update.mode; marker.band = update.band; diff --git a/owrx/aprs.py b/owrx/aprs.py index 2653f9a82..3d6ab522b 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -143,15 +143,13 @@ def getRemainder(self): class AprsLocation(LatLngLocation): def __init__(self, data): super().__init__(data["lat"], data["lon"]) - self.comment = data["comment"] if "comment" in data else None - self.symbol = data["symbol"] if "symbol" in data else None + self.data = data def __dict__(self): res = super(AprsLocation, self).__dict__() - if self.comment is not None: - res["comment"] = self.comment - if self.symbol is not None: - res["symbol"] = self.symbol + for key in ["comment", "symbol", "course", "speed"]: + if key in self.data: + res[key] = self.data[key] return res From cf273021ab3cb30c2f70018af2ba37962b5b8a22 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 19 Sep 2019 16:24:04 +0200 Subject: [PATCH 0389/2616] re-draw on update and apply opacity --- htdocs/lib/AprsMarker.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/htdocs/lib/AprsMarker.js b/htdocs/lib/AprsMarker.js index 1d7bd3dc5..aa3210ad2 100644 --- a/htdocs/lib/AprsMarker.js +++ b/htdocs/lib/AprsMarker.js @@ -5,6 +5,7 @@ AprsMarker.prototype = new google.maps.OverlayView(); AprsMarker.prototype.draw = function() { var div = this.div; var overlay = this.overlay; + if (!div || !overlay) return; if (this.symbol) { var tableId = this.symbol.table == '/' ? 0 : 1; @@ -32,6 +33,12 @@ AprsMarker.prototype.draw = function() { overlay.style.display = 'none'; } + if (this.opacity) { + div.style.opacity = this.opacity; + } else { + div.style.opacity = null; + } + var point = this.getProjection().fromLatLngToDivPixel(this.position); if (point) { @@ -40,6 +47,11 @@ AprsMarker.prototype.draw = function() { } }; +AprsMarker.prototype.setOptions = function(options) { + google.maps.OverlayView.prototype.setOptions.apply(this, arguments); + this.draw(); +}; + AprsMarker.prototype.onAdd = function() { var div = this.div = document.createElement('div'); @@ -65,7 +77,7 @@ AprsMarker.prototype.onAdd = function() { var panes = this.getPanes(); panes.overlayImage.appendChild(div); -} +}; AprsMarker.prototype.remove = function() { if (this.div) { @@ -76,4 +88,4 @@ AprsMarker.prototype.remove = function() { AprsMarker.prototype.getAnchorPoint = function() { return new google.maps.Point(0, -12); -} +}; From 428a9ca5099b1fc9893f172e55d2df0aaf6f1d49 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 21 Sep 2019 13:41:04 +0200 Subject: [PATCH 0390/2616] await the right condition --- htdocs/map.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/map.js b/htdocs/map.js index 20d18c1cf..41c110566 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -88,7 +88,7 @@ } var processUpdates = function(updates) { - if (!map) { + if (typeof(AprsMarker) == 'undefined') { updateQueue = updateQueue.concat(updates); return; } From 2edeffb76176f63c33cb5418a3dfb40759bf7c0b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 21 Sep 2019 13:49:37 +0200 Subject: [PATCH 0391/2616] close websocket connections in an improved way --- owrx/connection.py | 7 ++-- owrx/http.py | 2 +- owrx/websocket.py | 82 +++++++++++++++++++++++++++++----------------- 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/owrx/connection.py b/owrx/connection.py index f873dc07c..13a91dd33 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -20,13 +20,14 @@ def protected_send(self, data): self.conn.send(data) # these exception happen when the socket is closed except OSError: + logger.exception("OSError while sending data") self.close() except ValueError: + logger.exception("ValueError while sending data") self.close() def close(self): self.conn.close() - logger.debug("connection closed") class OpenWebRxReceiverClient(Client): @@ -288,7 +289,3 @@ def handleTextMessage(self, conn, message): def handleBinaryMessage(self, conn, data): logger.error("unsupported binary message, discarding") - - def handleClose(self, conn): - if self.client: - self.client.close() diff --git a/owrx/http.py b/owrx/http.py index f15b974d7..d14bfb1ad 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -10,8 +10,8 @@ AprsSymbolsController, ) from http.server import BaseHTTPRequestHandler -import re from urllib.parse import urlparse, parse_qs +import re import logging diff --git a/owrx/websocket.py b/owrx/websocket.py index c773cf992..d1ad61f95 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -1,16 +1,24 @@ import base64 import hashlib import json +import os +import select import logging logger = logging.getLogger(__name__) +class IncompleteRead(Exception): + pass + + class WebSocketConnection(object): def __init__(self, handler, messageHandler): self.handler = handler self.messageHandler = messageHandler + (self.interruptPipeRecv, self.interruptPipeSend) = os.pipe() + self.open = True my_headers = self.handler.headers.items() my_header_keys = list(map(lambda x: x[0], my_headers)) h_key_exists = lambda x: my_header_keys.count(x) @@ -78,33 +86,49 @@ def send(self, data): else: self.handler.wfile.flush() + def protected_read(self, num): + data = self.handler.rfile.read(num) + if len(data) != num: + raise IncompleteRead() + return data + + def interrupt(self): + os.write(self.interruptPipeSend, bytes(0x00)) + def read_loop(self): - open = True - while open: - header = self.handler.rfile.read(2) - opcode = header[0] & 0x0F - length = header[1] & 0x7F - mask = (header[1] & 0x80) >> 7 - if length == 126: - header = self.handler.rfile.read(2) - length = (header[0] << 8) + header[1] - if mask: - masking_key = self.handler.rfile.read(4) - data = self.handler.rfile.read(length) - if mask: - data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) - if opcode == 1: - message = data.decode("utf-8") - self.messageHandler.handleTextMessage(self, message) - elif opcode == 2: - self.messageHandler.handleBinaryMessage(self, data) - elif opcode == 8: - open = False - self.messageHandler.handleClose(self) - else: - logger.warning("unsupported opcode: {0}".format(opcode)) + self.open = True + while self.open: + try: + (read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], []) + if read[0] == self.handler.rfile: + header = self.protected_read(2) + opcode = header[0] & 0x0F + length = header[1] & 0x7F + mask = (header[1] & 0x80) >> 7 + if length == 126: + header = self.protected_read(2) + length = (header[0] << 8) + header[1] + if mask: + masking_key = self.protected_read(4) + data = self.protected_read(length) + if mask: + data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) + if opcode == 1: + message = data.decode("utf-8") + self.messageHandler.handleTextMessage(self, message) + elif opcode == 2: + self.messageHandler.handleBinaryMessage(self, data) + elif opcode == 8: + logger.debug("websocket close frame received; closing connection") + self.open = False + else: + logger.warning("unsupported opcode: {0}".format(opcode)) + except IncompleteRead: + logger.warning("incomplete websocket read; closing socket") + self.open = False + + logger.debug("websocket loop ended; sending close frame") - def close(self): try: header = self.get_header(0, 8) self.handler.wfile.write(header) @@ -114,11 +138,9 @@ def close(self): except OSError: logger.exception("OSError while writing close frame:") - try: - self.handler.finish() - self.handler.connection.close() - except Exception: - logger.exception("while closing connection:") + def close(self): + self.open = False + self.interrupt() class WebSocketException(Exception): From 671509df3b720a6728ba3782eab0f327e7507a18 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 21 Sep 2019 15:19:10 +0200 Subject: [PATCH 0392/2616] fix variable name --- htdocs/openwebrx.js | 2 +- owrx/threading.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 owrx/threading.py diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index ea1cbbbc3..5508d0e54 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1160,7 +1160,7 @@ function audio_calculate_resampling(targetRate) debug_ws_data_received=0; max_clients_num=0; -clients_num = 0; +client_num = 0; var COMPRESS_FFT_PAD_N=10; //should be the same as in csdr.c diff --git a/owrx/threading.py b/owrx/threading.py new file mode 100644 index 000000000..e69de29bb From 6ec85aa349bbd5a3247f3c080367e19e6a50ff52 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 21 Sep 2019 15:24:06 +0200 Subject: [PATCH 0393/2616] don't start up unnecesserily --- owrx/source.py | 3 ++- owrx/threading.py | 0 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 owrx/threading.py diff --git a/owrx/source.py b/owrx/source.py index eff9e29f3..2c475ece9 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -635,7 +635,6 @@ class CpuUsageThread(threading.Thread): def getSharedInstance(): if CpuUsageThread.sharedInstance is None: CpuUsageThread.sharedInstance = CpuUsageThread() - CpuUsageThread.sharedInstance.start() return CpuUsageThread.sharedInstance def __init__(self): @@ -679,6 +678,8 @@ def get_cpu_usage(self): def add_client(self, c): self.clients.append(c) + if not self.is_alive(): + self.start() def remove_client(self, c): try: diff --git a/owrx/threading.py b/owrx/threading.py deleted file mode 100644 index e69de29bb..000000000 From 1ed69de5b02ba91874b98b8fbd27de24c181bcfb Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 21 Sep 2019 22:10:16 +0200 Subject: [PATCH 0394/2616] un-couple messaging between connections; use non-blocking io --- owrx/connection.py | 41 +++++++++++++------ owrx/websocket.py | 98 ++++++++++++++++++++++++++++++---------------- 2 files changed, 92 insertions(+), 47 deletions(-) diff --git a/owrx/connection.py b/owrx/connection.py index 13a91dd33..199400156 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -3,8 +3,10 @@ from owrx.feature import FeatureDetector from owrx.version import openwebrx_version from owrx.bands import Bandplan -import json from owrx.map import Map +from multiprocessing import Queue +import json +import threading import logging @@ -14,20 +16,29 @@ class Client(object): def __init__(self, conn): self.conn = conn + self.multiprocessingPipe = Queue() + + def mp_passthru(): + run = True + while run: + try: + data = self.multiprocessingPipe.get() + self.protected_send(data) + except (EOFError, OSError): + run = False + + self.passThruThread = threading.Thread(target=mp_passthru) + self.passThruThread.start() def protected_send(self, data): - try: - self.conn.send(data) - # these exception happen when the socket is closed - except OSError: - logger.exception("OSError while sending data") - self.close() - except ValueError: - logger.exception("ValueError while sending data") - self.close() + self.conn.protected_send(data) def close(self): self.conn.close() + self.multiprocessingPipe.close() + + def mp_send(self, data): + self.multiprocessingPipe.put(data, block=False) class OpenWebRxReceiverClient(Client): @@ -171,10 +182,10 @@ def write_s_meter_level(self, level): self.protected_send({"type": "smeter", "value": level}) def write_cpu_usage(self, usage): - self.protected_send({"type": "cpuusage", "value": usage}) + self.mp_send({"type": "cpuusage", "value": usage}) def write_clients(self, clients): - self.protected_send({"type": "clients", "value": clients}) + self.mp_send({"type": "clients", "value": clients}) def write_secondary_fft(self, data): self.protected_send(bytes([0x03]) + data) @@ -227,7 +238,7 @@ def write_config(self, cfg): self.protected_send({"type": "config", "value": cfg}) def write_update(self, update): - self.protected_send({"type": "update", "value": update}) + self.mp_send({"type": "update", "value": update}) class WebSocketMessageHandler(object): @@ -289,3 +300,7 @@ def handleTextMessage(self, conn, message): def handleBinaryMessage(self, conn, data): logger.error("unsupported binary message, discarding") + + def handleClose(self): + if self.client: + self.client.close() diff --git a/owrx/websocket.py b/owrx/websocket.py index d1ad61f95..321330c2c 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -3,6 +3,7 @@ import json import os import select +import threading import logging @@ -16,9 +17,11 @@ class IncompleteRead(Exception): class WebSocketConnection(object): def __init__(self, handler, messageHandler): self.handler = handler + self.handler.connection.setblocking(0) self.messageHandler = messageHandler (self.interruptPipeRecv, self.interruptPipeSend) = os.pipe() self.open = True + self.sendLock = threading.Lock() my_headers = self.handler.headers.items() my_header_keys = list(map(lambda x: x[0], my_headers)) h_key_exists = lambda x: my_header_keys.count(x) @@ -79,53 +82,80 @@ def send(self, data): else: header = self.get_header(len(data), 2) data_to_send = header + data - written = self.handler.wfile.write(data_to_send) - if written != len(data_to_send): - logger.error("incomplete write! closing socket!") - self.close() - else: - self.handler.wfile.flush() + + def chunks(l, n): + """Yield successive n-sized chunks from l.""" + for i in range(0, len(l), n): + yield l[i : i + n] + + with self.sendLock: + for chunk in chunks(data_to_send, 1024): + (_, write, _) = select.select([], [self.handler.wfile], [], 10) + if self.handler.wfile in write: + written = self.handler.wfile.write(chunk) + if written != len(chunk): + logger.error("incomplete write! closing socket!") + self.close() + else: + logger.debug("socket not returned from select; closing") + self.close() def protected_read(self, num): data = self.handler.rfile.read(num) - if len(data) != num: + if data is None or len(data) != num: raise IncompleteRead() return data + def protected_send(self, data): + try: + self.send(data) + # these exception happen when the socket is closed + except OSError: + logger.exception("OSError while writing data") + self.close() + except ValueError: + logger.exception("ValueError while writing data") + self.close() + def interrupt(self): os.write(self.interruptPipeSend, bytes(0x00)) def read_loop(self): self.open = True while self.open: - try: - (read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], []) - if read[0] == self.handler.rfile: - header = self.protected_read(2) - opcode = header[0] & 0x0F - length = header[1] & 0x7F - mask = (header[1] & 0x80) >> 7 - if length == 126: + (read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], []) + if self.handler.rfile in read: + available = True + while available: + try: header = self.protected_read(2) - length = (header[0] << 8) + header[1] - if mask: - masking_key = self.protected_read(4) - data = self.protected_read(length) - if mask: - data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) - if opcode == 1: - message = data.decode("utf-8") - self.messageHandler.handleTextMessage(self, message) - elif opcode == 2: - self.messageHandler.handleBinaryMessage(self, data) - elif opcode == 8: - logger.debug("websocket close frame received; closing connection") - self.open = False - else: - logger.warning("unsupported opcode: {0}".format(opcode)) - except IncompleteRead: - logger.warning("incomplete websocket read; closing socket") - self.open = False + opcode = header[0] & 0x0F + length = header[1] & 0x7F + mask = (header[1] & 0x80) >> 7 + if length == 126: + header = self.protected_read(2) + length = (header[0] << 8) + header[1] + if mask: + masking_key = self.protected_read(4) + data = self.protected_read(length) + if mask: + data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) + if opcode == 1: + message = data.decode("utf-8") + self.messageHandler.handleTextMessage(self, message) + elif opcode == 2: + self.messageHandler.handleBinaryMessage(self, data) + elif opcode == 8: + logger.debug("websocket close frame received; closing connection") + self.open = False + else: + logger.warning("unsupported opcode: {0}".format(opcode)) + except IncompleteRead: + available = False + + logger.debug("websocket loop ended; shutting down") + + self.messageHandler.handleClose() logger.debug("websocket loop ended; sending close frame") From b4ffc6e2f001a2bd12371975cacb7c526d9e7f08 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 22 Sep 2019 12:56:35 +0200 Subject: [PATCH 0395/2616] replace os pipe with multiprocessing (seems to work better) --- owrx/websocket.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/owrx/websocket.py b/owrx/websocket.py index 321330c2c..dad40cd52 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -1,7 +1,7 @@ import base64 import hashlib import json -import os +from multiprocessing import Pipe import select import threading @@ -19,7 +19,7 @@ def __init__(self, handler, messageHandler): self.handler = handler self.handler.connection.setblocking(0) self.messageHandler = messageHandler - (self.interruptPipeRecv, self.interruptPipeSend) = os.pipe() + (self.interruptPipeRecv, self.interruptPipeSend) = Pipe(duplex=False) self.open = True self.sendLock = threading.Lock() my_headers = self.handler.headers.items() @@ -118,7 +118,7 @@ def protected_send(self, data): self.close() def interrupt(self): - os.write(self.interruptPipeSend, bytes(0x00)) + self.interruptPipeSend.send(bytes(0x00)) def read_loop(self): self.open = True From 57975b6f96337786613079ab3385f97c54acd680 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 22 Sep 2019 12:57:13 +0200 Subject: [PATCH 0396/2616] move connection tracking to all websockets --- openwebrx.py | 6 +++--- owrx/websocket.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/openwebrx.py b/openwebrx.py index a01334923..d4ebddd76 100755 --- a/openwebrx.py +++ b/openwebrx.py @@ -4,10 +4,11 @@ from owrx.http import RequestHandler from owrx.config import PropertyManager from owrx.feature import FeatureDetector -from owrx.source import SdrService, ClientRegistry +from owrx.source import SdrService from socketserver import ThreadingMixIn from owrx.sdrhu import SdrHuUpdater from owrx.service import Services +from owrx.websocket import WebSocketConnection import logging @@ -58,5 +59,4 @@ def main(): try: main() except KeyboardInterrupt: - for c in ClientRegistry.getSharedInstance().clients: - c.close() + WebSocketConnection.closeAll() diff --git a/owrx/websocket.py b/owrx/websocket.py index dad40cd52..d48784f1a 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -15,6 +15,16 @@ class IncompleteRead(Exception): class WebSocketConnection(object): + connections = [] + + @staticmethod + def closeAll(): + for c in WebSocketConnection.connections: + try: + c.close() + except: + logger.exception("exception while shutting down websocket connections") + def __init__(self, handler, messageHandler): self.handler = handler self.handler.connection.setblocking(0) @@ -121,6 +131,7 @@ def interrupt(self): self.interruptPipeSend.send(bytes(0x00)) def read_loop(self): + WebSocketConnection.connections.append(self) self.open = True while self.open: (read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], []) @@ -168,6 +179,11 @@ def read_loop(self): except OSError: logger.exception("OSError while writing close frame:") + try: + WebSocketConnection.connections.remove(self) + except ValueError: + pass + def close(self): self.open = False self.interrupt() From 52afe3fb027d1dd0990487a265a4406bdc6f1605 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 22 Sep 2019 12:57:59 +0200 Subject: [PATCH 0397/2616] tone down wsjt logging --- owrx/wsjt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index f73e10511..fc41cd02a 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -16,6 +16,7 @@ import logging logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) class WsjtQueueWorker(threading.Thread): From cfb4208db210965ffebc80d2f93c6364b7881ace Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 22 Sep 2019 13:16:24 +0200 Subject: [PATCH 0398/2616] improved api --- owrx/connection.py | 39 +++++++++++++++++++-------------------- owrx/websocket.py | 37 +++++++++++++++++-------------------- 2 files changed, 36 insertions(+), 40 deletions(-) diff --git a/owrx/connection.py b/owrx/connection.py index 199400156..e2e23d3fa 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -23,15 +23,14 @@ def mp_passthru(): while run: try: data = self.multiprocessingPipe.get() - self.protected_send(data) + self.send(data) except (EOFError, OSError): run = False - self.passThruThread = threading.Thread(target=mp_passthru) - self.passThruThread.start() + threading.Thread(target=mp_passthru).start() - def protected_send(self, data): - self.conn.protected_send(data) + def send(self, data): + self.conn.send(data) def close(self): self.conn.close() @@ -173,13 +172,13 @@ def setDspProperties(self, params): self.dsp.setProperty(key, value) def write_spectrum_data(self, data): - self.protected_send(bytes([0x01]) + data) + self.send(bytes([0x01]) + data) def write_dsp_data(self, data): - self.protected_send(bytes([0x02]) + data) + self.send(bytes([0x02]) + data) def write_s_meter_level(self, level): - self.protected_send({"type": "smeter", "value": level}) + self.send({"type": "smeter", "value": level}) def write_cpu_usage(self, usage): self.mp_send({"type": "cpuusage", "value": usage}) @@ -188,37 +187,37 @@ def write_clients(self, clients): self.mp_send({"type": "clients", "value": clients}) def write_secondary_fft(self, data): - self.protected_send(bytes([0x03]) + data) + self.send(bytes([0x03]) + data) def write_secondary_demod(self, data): - self.protected_send(bytes([0x04]) + data) + self.send(bytes([0x04]) + data) def write_secondary_dsp_config(self, cfg): - self.protected_send({"type": "secondary_config", "value": cfg}) + self.send({"type": "secondary_config", "value": cfg}) def write_config(self, cfg): - self.protected_send({"type": "config", "value": cfg}) + self.send({"type": "config", "value": cfg}) def write_receiver_details(self, details): - self.protected_send({"type": "receiver_details", "value": details}) + self.send({"type": "receiver_details", "value": details}) def write_profiles(self, profiles): - self.protected_send({"type": "profiles", "value": profiles}) + self.send({"type": "profiles", "value": profiles}) def write_features(self, features): - self.protected_send({"type": "features", "value": features}) + self.send({"type": "features", "value": features}) def write_metadata(self, metadata): - self.protected_send({"type": "metadata", "value": metadata}) + self.send({"type": "metadata", "value": metadata}) def write_wsjt_message(self, message): - self.protected_send({"type": "wsjt_message", "value": message}) + self.send({"type": "wsjt_message", "value": message}) def write_dial_frequendies(self, frequencies): - self.protected_send({"type": "dial_frequencies", "value": frequencies}) + self.send({"type": "dial_frequencies", "value": frequencies}) def write_aprs_data(self, data): - self.protected_send({"type": "aprs_data", "value": data}) + self.send({"type": "aprs_data", "value": data}) class MapConnection(Client): @@ -235,7 +234,7 @@ def close(self): super().close() def write_config(self, cfg): - self.protected_send({"type": "config", "value": cfg}) + self.send({"type": "config", "value": cfg}) def write_update(self, update): self.mp_send({"type": "update", "value": update}) diff --git a/owrx/websocket.py b/owrx/websocket.py index d48784f1a..71b0f382e 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -98,27 +98,18 @@ def chunks(l, n): for i in range(0, len(l), n): yield l[i : i + n] - with self.sendLock: - for chunk in chunks(data_to_send, 1024): - (_, write, _) = select.select([], [self.handler.wfile], [], 10) - if self.handler.wfile in write: - written = self.handler.wfile.write(chunk) - if written != len(chunk): - logger.error("incomplete write! closing socket!") - self.close() - else: - logger.debug("socket not returned from select; closing") - self.close() - - def protected_read(self, num): - data = self.handler.rfile.read(num) - if data is None or len(data) != num: - raise IncompleteRead() - return data - - def protected_send(self, data): try: - self.send(data) + with self.sendLock: + for chunk in chunks(data_to_send, 1024): + (_, write, _) = select.select([], [self.handler.wfile], [], 10) + if self.handler.wfile in write: + written = self.handler.wfile.write(chunk) + if written != len(chunk): + logger.error("incomplete write! closing socket!") + self.close() + else: + logger.debug("socket not returned from select; closing") + self.close() # these exception happen when the socket is closed except OSError: logger.exception("OSError while writing data") @@ -127,6 +118,12 @@ def protected_send(self, data): logger.exception("ValueError while writing data") self.close() + def protected_read(self, num): + data = self.handler.rfile.read(num) + if data is None or len(data) != num: + raise IncompleteRead() + return data + def interrupt(self): self.interruptPipeSend.send(bytes(0x00)) From 8b9121a5c1c228f8e828c123a1f45e3f968b028f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 22 Sep 2019 20:51:33 +0200 Subject: [PATCH 0399/2616] tone down http logging --- owrx/http.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/owrx/http.py b/owrx/http.py index d14bfb1ad..6ed11bfba 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -16,6 +16,7 @@ import logging logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) class RequestHandler(BaseHTTPRequestHandler): @@ -23,6 +24,9 @@ def __init__(self, request, client_address, server): self.router = Router() super().__init__(request, client_address, server) + def log_message(self, format, *args): + logger.debug("%s - - [%s] %s", self.address_string(), self.log_date_time_string(), format%args) + def do_GET(self): self.router.route(self) From 72f92a1c2b7cff97e388b89617c5e9918729eb44 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 23 Sep 2019 03:06:51 +0200 Subject: [PATCH 0400/2616] use events instead of simple sleep for clean shutdown --- owrx/source.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/owrx/source.py b/owrx/source.py index 2c475ece9..428955ca3 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -642,6 +642,7 @@ def __init__(self): self.doRun = True self.last_worktime = 0 self.last_idletime = 0 + self.endEvent = threading.Event() super().__init__() def run(self): @@ -652,7 +653,7 @@ def run(self): cpu_usage = 0 for c in self.clients: c.write_cpu_usage(cpu_usage) - time.sleep(3) + self.endEvent.wait(timeout=3) logger.debug("cpu usage thread shut down") def get_cpu_usage(self): @@ -692,6 +693,7 @@ def remove_client(self, c): def shutdown(self): CpuUsageThread.sharedInstance = None self.doRun = False + self.endEvent.set() class TooManyClientsException(Exception): From ae87185ad0345767642d42d8b1a429ad100aeb8f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 23 Sep 2019 03:15:24 +0200 Subject: [PATCH 0401/2616] run the formatter once more --- owrx/aprs.py | 11 +++-------- owrx/http.py | 2 +- owrx/meta.py | 1 - owrx/metrics.py | 1 + owrx/wsjt.py | 2 +- 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/owrx/aprs.py b/owrx/aprs.py index 3d6ab522b..c0ca5f6ba 100644 --- a/owrx/aprs.py +++ b/owrx/aprs.py @@ -37,14 +37,9 @@ def decodeBase91(input): base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 return base + (ord(input[-1]) - 33) -def getSymbolData(symbol, table): - return { - "symbol": symbol, - "table": table, - "index": ord(symbol) - 33, - "tableindex": ord(table) - 33, - } +def getSymbolData(symbol, table): + return {"symbol": symbol, "table": table, "index": ord(symbol) - 33, "tableindex": ord(table) - 33} class Ax25Parser(object): @@ -578,5 +573,5 @@ def parse(self, data): "course": course, "device": device, "type": "Mic-E", - "symbol": getSymbolData(chr(information[7]), chr(information[8])) + "symbol": getSymbolData(chr(information[7]), chr(information[8])), } diff --git a/owrx/http.py b/owrx/http.py index 6ed11bfba..196c6c4f2 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -25,7 +25,7 @@ def __init__(self, request, client_address, server): super().__init__(request, client_address, server) def log_message(self, format, *args): - logger.debug("%s - - [%s] %s", self.address_string(), self.log_date_time_string(), format%args) + logger.debug("%s - - [%s] %s", self.address_string(), self.log_date_time_string(), format % args) def do_GET(self): self.router.route(self) diff --git a/owrx/meta.py b/owrx/meta.py index 4cf507e07..f4979cf33 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -84,7 +84,6 @@ def enrich(self, meta): class MetaParser(object): - def __init__(self, handler): self.handler = handler self.enrichers = {"DMR": DmrMetaEnricher(), "YSF": YsfMetaEnricher(self)} diff --git a/owrx/metrics.py b/owrx/metrics.py index 28444047e..7055449b3 100644 --- a/owrx/metrics.py +++ b/owrx/metrics.py @@ -1,5 +1,6 @@ import threading + class Metric(object): def getValue(self): return 0 diff --git a/owrx/wsjt.py b/owrx/wsjt.py index fc41cd02a..95d4e6543 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -197,7 +197,7 @@ def decoding_depth(self, mode): pm = PropertyManager.getSharedInstance() # mode-specific setting? if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]: - return pm["wsjt_decoding_depths"][mode] + return pm["wsjt_decoding_depths"][mode] # return global default if "wsjt_decoding_depth" in pm: return pm["wsjt_decoding_depth"] From e24de8334f19d3d9cba8b59367a825a2ba634f45 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 23 Sep 2019 16:51:24 +0200 Subject: [PATCH 0402/2616] silence direwolf --- csdr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csdr.py b/csdr.py index 412fda25a..90232693d 100644 --- a/csdr.py +++ b/csdr.py @@ -240,7 +240,7 @@ def secondary_chain(self, which): chain = secondary_chain_base + "csdr fmdemod_quadri_cf | " if self.last_decimation != 1.0: chain += "csdr fractional_decimator_ff {last_decimation} | " - chain += "csdr convert_f_s16 | direwolf -c {direwolf_config} -r {audio_rate} -t 0 - 1>&2" + chain += "csdr convert_f_s16 | direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h - 1>&2" return chain def set_secondary_demodulator(self, what): From b1742dafc2137f01c4502d6c8291c83e40dff65a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 23 Sep 2019 16:51:38 +0200 Subject: [PATCH 0403/2616] incomplete implementation to extend a callsign location --- owrx/map.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/owrx/map.py b/owrx/map.py index a27a0e0ab..9dc5c48dc 100644 --- a/owrx/map.py +++ b/owrx/map.py @@ -77,6 +77,13 @@ def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None): ] ) + def touchLocation(self, callsign): + # not implemented on the client side yet, so do not use! + ts = datetime.now() + if callsign in self.positions: + self.positions[callsign]["updated"] = ts + self.broadcast([{"callsign": callsign, "lastseen": ts.timestamp() * 1000}]) + def removeLocation(self, callsign): self.positions.pop(callsign, None) # TODO broadcast removal to clients From 4be34e4dc1dd3201dd7cc311446ca84d76fe55c9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 23 Sep 2019 18:33:52 +0200 Subject: [PATCH 0404/2616] integrate pskreporter scheduling (no upload yet) --- owrx/pskreporter.py | 46 ++++++++++++++++++++++++++ owrx/wsjt.py | 78 +++++++++++++++++++++++++++------------------ 2 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 owrx/pskreporter.py diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py new file mode 100644 index 000000000..b92d5e393 --- /dev/null +++ b/owrx/pskreporter.py @@ -0,0 +1,46 @@ +import logging +import threading +import time +import random +from sched import scheduler + +logger = logging.getLogger(__name__) + + +class PskReporter(object): + sharedInstance = None + creationLock = threading.Lock() + interval = 300 + + @staticmethod + def getSharedInstance(): + with PskReporter.creationLock: + if PskReporter.sharedInstance is None: + PskReporter.sharedInstance = PskReporter() + return PskReporter.sharedInstance + + def __init__(self): + self.spots = [] + self.spotLock = threading.Lock() + self.scheduler = scheduler(time.time, time.sleep) + self.scheduleNextUpload() + threading.Thread(target=self.scheduler.run).start() + + def scheduleNextUpload(self): + delay = PskReporter.interval + random.uniform(-30, 30) + logger.debug("scheduling next pskreporter upload in %f seconds", delay) + self.scheduler.enter(delay, 1, self.upload) + + def spot(self, spot): + with self.spotLock: + self.spots.append(spot) + + def upload(self): + with self.spotLock: + spots = self.spots + self.spots = [] + + if spots: + logger.debug("would now upload %i spots", len(spots)) + + self.scheduleNextUpload() diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 95d4e6543..277236445 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -12,6 +12,7 @@ from owrx.config import PropertyManager from owrx.bands import Bandplan from owrx.metrics import Metrics, CounterMetric, DirectMetric +from owrx.pskreporter import PskReporter import logging @@ -106,7 +107,7 @@ def getWaveFile(self): wavefile.setnchannels(1) wavefile.setsampwidth(2) wavefile.setframerate(12000) - return (filename, wavefile) + return filename, wavefile def getNextDecodingTime(self): t = datetime.now() @@ -260,9 +261,6 @@ def decoder_commandline(self, file): class WsjtParser(object): - locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") - wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") - def __init__(self, handler): self.handler = handler self.dial_freq = None @@ -281,18 +279,22 @@ def parse(self, data): modes = list(WsjtParser.modes.keys()) if msg[21] in modes or msg[19] in modes: - out = self.parse_from_jt9(msg) + decoder = Jt9Decoder() else: - out = self.parse_from_wsprd(msg) + decoder = WsprDecoder() + out = decoder.parse(msg) + if "mode" in out: + self.pushDecode(out["mode"]) + if "callsign" in out and "locator" in out: + Map.getSharedInstance().updateLocation( + out["callsign"], LocatorLocation(out["locator"]), out["mode"], self.band + ) + PskReporter.getSharedInstance().spot(out) self.handler.write_wsjt_message(out) except ValueError: logger.exception("error while parsing wsjt message") - def parse_timestamp(self, instring, dateformat): - ts = datetime.strptime(instring, dateformat) - return int(datetime.combine(date.today(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000) - def pushDecode(self, mode): metrics = Metrics.getSharedInstance() band = "unknown" @@ -312,7 +314,21 @@ def pushDecode(self, mode): metric.inc() - def parse_from_jt9(self, msg): + def setDialFrequency(self, freq): + self.dial_freq = freq + self.band = Bandplan.getSharedInstance().findBand(freq) + + +class Decoder(object): + def parse_timestamp(self, instring, dateformat): + ts = datetime.strptime(instring, dateformat) + return int(datetime.combine(date.today(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000) + + +class Jt9Decoder(Decoder): + locator_pattern = re.compile("[A-Z0-9]+\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") + + def parse(self, msg): # ft8 sample # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' # jt65 sample @@ -328,10 +344,8 @@ def parse_from_jt9(self, msg): modeChar = msg[14:15] mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" wsjt_msg = msg[17:53].strip() - self.parseLocator(wsjt_msg, mode) - self.pushDecode(mode) - return { + result = { "timestamp": timestamp, "db": float(msg[0:3]), "dt": float(msg[4:8]), @@ -339,25 +353,29 @@ def parse_from_jt9(self, msg): "mode": mode, "msg": wsjt_msg, } + result.update(self.parseMessage(wsjt_msg)) + return result - def parseLocator(self, msg, mode): - m = WsjtParser.locator_pattern.match(msg) + def parseMessage(self, msg): + m = Jt9Decoder.locator_pattern.match(msg) if m is None: - return + return {} # this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very # likely this just means roger roger goodbye. if m.group(2) == "RR73": - return - Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2)), mode, self.band) + return {"callsign": m.group(1)} + return {"callsign": m.group(1), "locator": m.group(2)} + - def parse_from_wsprd(self, msg): +class WsprDecoder(Decoder): + wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") + + def parse(self, msg): # wspr sample # '2600 -24 0.4 0.001492 -1 G8AXA JO01 33' # '0052 -29 2.6 0.001486 0 G02CWT IO92 23' wsjt_msg = msg[29:].strip() - self.parseWsprMessage(wsjt_msg) - self.pushDecode("WSPR") - return { + result = { "timestamp": self.parse_timestamp(msg[0:4], "%H%M"), "db": float(msg[5:8]), "dt": float(msg[9:13]), @@ -366,13 +384,11 @@ def parse_from_wsprd(self, msg): "mode": "WSPR", "msg": wsjt_msg, } + result.update(self.parseMessage(wsjt_msg)) + return result - def parseWsprMessage(self, msg): - m = WsjtParser.wspr_splitter_pattern.match(msg) + def parseMessage(self, msg): + m = WsprDecoder.wspr_splitter_pattern.match(msg) if m is None: - return - Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2)), "WSPR", self.band) - - def setDialFrequency(self, freq): - self.dial_freq = freq - self.band = Bandplan.getSharedInstance().findBand(freq) + return {} + return {"callsign": m.group(1), "locator": m.group(2)} From f8dcff788b17424bdab01a3b8ffe7c387c6a0f7b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 23 Sep 2019 22:45:55 +0200 Subject: [PATCH 0405/2616] build valid packets (hopefully) --- owrx/pskreporter.py | 135 +++++++++++++++++++++++++++++++++++++++++++- owrx/wsjt.py | 10 ++-- 2 files changed, 138 insertions(+), 7 deletions(-) diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index b92d5e393..c966025fd 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -3,6 +3,8 @@ import time import random from sched import scheduler +from owrx.config import PropertyManager +from owrx.version import openwebrx_version logger = logging.getLogger(__name__) @@ -10,7 +12,8 @@ class PskReporter(object): sharedInstance = None creationLock = threading.Lock() - interval = 300 + interval = 60 + supportedModes = ["FT8", "FT4", "JT9", "JT65"] @staticmethod def getSharedInstance(): @@ -22,6 +25,7 @@ def getSharedInstance(): def __init__(self): self.spots = [] self.spotLock = threading.Lock() + self.uploader = Uploader() self.scheduler = scheduler(time.time, time.sleep) self.scheduleNextUpload() threading.Thread(target=self.scheduler.run).start() @@ -32,6 +36,8 @@ def scheduleNextUpload(self): self.scheduler.enter(delay, 1, self.upload) def spot(self, spot): + if not spot["mode"] in PskReporter.supportedModes: + return with self.spotLock: self.spots.append(spot) @@ -41,6 +47,131 @@ def upload(self): self.spots = [] if spots: - logger.debug("would now upload %i spots", len(spots)) + self.uploader.upload(spots) self.scheduleNextUpload() + + +class Uploader(object): + receieverDelimiter = [0x99, 0x92] + senderDelimiter = [0x99, 0x93] + + def __init__(self): + self.sequence = 0 + + def upload(self, spots): + logger.debug("would now upload %i spots", len(spots)) + for packet in self.getPackets(spots): + l = int.from_bytes(packet[2:4], "big") + logger.debug("packet length: %i; indicated length: %i", len(packet), l) + logger.debug(packet) + # TODO actually send the packet + + def getPackets(self, spots): + encoded = [self.encodeSpot(spot) for spot in spots] + + def chunks(l, n): + """Yield successive n-sized chunks from l.""" + for i in range(0, len(l), n): + yield l[i : i + n] + + rHeader = self.getReceiverInformationHeader() + rInfo = self.getReceiverInformation() + sHeader = self.getSenderInformationHeader() + + packets = [] + # 50 seems to be a safe bet + for chunk in chunks(encoded, 50): + sInfoLength = sum(map(len, chunk)) + length = sInfoLength + 16 + len(rHeader) + len(sHeader) + len(rInfo) + 4 + header = self.getHeader(length) + packets.append( + header + + rHeader + + sHeader + + rInfo + + bytes(Uploader.senderDelimiter) + + sInfoLength.to_bytes(2, "big") + + b"".join(chunk) + ) + + return packets + + def getHeader(self, length): + self.sequence += 1 + return bytes( + # protocol version + [0x00, 0x0A] + + list(length.to_bytes(2, "big")) + + list(int(time.time()).to_bytes(4, "big")) + + list(self.sequence.to_bytes(4, "big")) + + list((id(self) & 0xFFFFFFFF).to_bytes(4, "big")) + ) + + def encodeString(self, s): + return [len(s)] + list(s.encode("utf-8")) + + def encodeSpot(self, spot): + return bytes( + self.encodeString(spot["callsign"]) + + list(spot["freq"].to_bytes(4, "big")) + + list(int(spot["db"]).to_bytes(1, "big", signed=True)) + + self.encodeString(spot["mode"]) + + self.encodeString(spot["locator"]) + # informationsource. 1 means "automatically extracted + + [0x01] + + list(int(spot["timestamp"] / 1000).to_bytes(4, "big")) + ) + + def getReceiverInformationHeader(self): + return bytes( + # id, length + [0x00, 0x03, 0x00, 0x24] + + Uploader.receieverDelimiter + # number of fields + + [0x00, 0x03, 0x00, 0x00] + # receiverCallsign + + [0x80, 0x02, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # receiverLocator + + [0x80, 0x04, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # decodingSoftware + + [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # padding + + [0x00, 0x00] + ) + + def getReceiverInformation(self): + pm = PropertyManager.getSharedInstance() + callsign = pm["pskreporter_callsign"] + locator = pm["receiver_qra"] + decodingSoftware = "OpenWebRX " + openwebrx_version + body = [b for s in [callsign, locator, decodingSoftware] for b in self.encodeString(s)] + body = self.pad(body, 4) + body = bytes(Uploader.receieverDelimiter + list((len(body) + 4).to_bytes(2, "big")) + body) + return body + + def getSenderInformationHeader(self): + return bytes( + # id, length + [0x00, 0x02, 0x00, 0x3C] + + Uploader.senderDelimiter + # number of fields + + [0x00, 0x07] + # senderCallsign + + [0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # frequency + + [0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F] + # sNR + + [0x80, 0x05, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F] + # mode + + [0x80, 0x0A, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # senderLocator + + [0x80, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # informationSource + + [0x80, 0x0B, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F] + # flowStartSeconds + + [0x00, 0x96, 0x00, 0x04] + ) + + def pad(self, bytes, l): + return bytes + [0x00 for _ in range(0, -1 * len(bytes) % l)] diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 277236445..21fefb0fa 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -282,7 +282,7 @@ def parse(self, data): decoder = Jt9Decoder() else: decoder = WsprDecoder() - out = decoder.parse(msg) + out = decoder.parse(msg, self.dial_freq) if "mode" in out: self.pushDecode(out["mode"]) if "callsign" in out and "locator" in out: @@ -328,7 +328,7 @@ def parse_timestamp(self, instring, dateformat): class Jt9Decoder(Decoder): locator_pattern = re.compile("[A-Z0-9]+\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") - def parse(self, msg): + def parse(self, msg, dial_freq): # ft8 sample # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' # jt65 sample @@ -349,7 +349,7 @@ def parse(self, msg): "timestamp": timestamp, "db": float(msg[0:3]), "dt": float(msg[4:8]), - "freq": int(msg[9:13]), + "freq": dial_freq + int(msg[9:13]), "mode": mode, "msg": wsjt_msg, } @@ -370,7 +370,7 @@ def parseMessage(self, msg): class WsprDecoder(Decoder): wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") - def parse(self, msg): + def parse(self, msg, dial_freq): # wspr sample # '2600 -24 0.4 0.001492 -1 G8AXA JO01 33' # '0052 -29 2.6 0.001486 0 G02CWT IO92 23' @@ -379,7 +379,7 @@ def parse(self, msg): "timestamp": self.parse_timestamp(msg[0:4], "%H%M"), "db": float(msg[5:8]), "dt": float(msg[9:13]), - "freq": float(msg[14:24]), + "freq": dial_freq + int(float(msg[14:24]) * 1e6), "drift": int(msg[25:28]), "mode": "WSPR", "msg": wsjt_msg, From d8bc2cab2e318b478240ff9c0acb225b90b02653 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 23 Sep 2019 23:47:12 +0200 Subject: [PATCH 0406/2616] actual upload --- owrx/pskreporter.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index c966025fd..3ac0b9444 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -2,6 +2,7 @@ import threading import time import random +import socket from sched import scheduler from owrx.config import PropertyManager from owrx.version import openwebrx_version @@ -12,7 +13,7 @@ class PskReporter(object): sharedInstance = None creationLock = threading.Lock() - interval = 60 + interval = 300 supportedModes = ["FT8", "FT4", "JT9", "JT65"] @staticmethod @@ -58,14 +59,12 @@ class Uploader(object): def __init__(self): self.sequence = 0 + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) def upload(self, spots): logger.debug("would now upload %i spots", len(spots)) for packet in self.getPackets(spots): - l = int.from_bytes(packet[2:4], "big") - logger.debug("packet length: %i; indicated length: %i", len(packet), l) - logger.debug(packet) - # TODO actually send the packet + self.socket.sendto(packet, ("report.pskreporter.info", 4739)) def getPackets(self, spots): encoded = [self.encodeSpot(spot) for spot in spots] @@ -82,7 +81,8 @@ def chunks(l, n): packets = [] # 50 seems to be a safe bet for chunk in chunks(encoded, 50): - sInfoLength = sum(map(len, chunk)) + sInfo = self.padBytes(b"".join(chunk), 4) + sInfoLength = len(sInfo) length = sInfoLength + 16 + len(rHeader) + len(sHeader) + len(rInfo) + 4 header = self.getHeader(length) packets.append( @@ -92,7 +92,7 @@ def chunks(l, n): + rInfo + bytes(Uploader.senderDelimiter) + sInfoLength.to_bytes(2, "big") - + b"".join(chunk) + + sInfo ) return packets @@ -162,7 +162,7 @@ def getSenderInformationHeader(self): # frequency + [0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F] # sNR - + [0x80, 0x05, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F] + + [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F] # mode + [0x80, 0x0A, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] # senderLocator @@ -173,5 +173,8 @@ def getSenderInformationHeader(self): + [0x00, 0x96, 0x00, 0x04] ) - def pad(self, bytes, l): - return bytes + [0x00 for _ in range(0, -1 * len(bytes) % l)] + def pad(self, b, l): + return b + [0x00 for _ in range(0, -1 * len(b) % l)] + + def padBytes(self, b, l): + return b + bytes([0x00 for _ in range(0, -1 * len(b) % l)]) From bf59ed34cfe5aee8f476e8020638e9a91e0931df Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 23 Sep 2019 23:53:22 +0200 Subject: [PATCH 0407/2616] no more conditional --- owrx/pskreporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index 3ac0b9444..077d7733e 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -62,7 +62,7 @@ def __init__(self): self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) def upload(self, spots): - logger.debug("would now upload %i spots", len(spots)) + logger.debug("uploading %i spots", len(spots)) for packet in self.getPackets(spots): self.socket.sendto(packet, ("report.pskreporter.info", 4739)) From 22f4504629831c7851c874cd45d845922ef856c2 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 24 Sep 2019 21:41:31 +0200 Subject: [PATCH 0408/2616] set random to be at least 5 minutes --- owrx/pskreporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index 077d7733e..01cb157ac 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -32,7 +32,7 @@ def __init__(self): threading.Thread(target=self.scheduler.run).start() def scheduleNextUpload(self): - delay = PskReporter.interval + random.uniform(-30, 30) + delay = PskReporter.interval + random.uniform(0, 30) logger.debug("scheduling next pskreporter upload in %f seconds", delay) self.scheduler.enter(delay, 1, self.upload) From ba032435270d5a01109fc1f3aba78e6bcc6ac1f8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 24 Sep 2019 21:42:00 +0200 Subject: [PATCH 0409/2616] fix date --- owrx/wsjt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 21fefb0fa..e785e4e02 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -322,7 +322,7 @@ def setDialFrequency(self, freq): class Decoder(object): def parse_timestamp(self, instring, dateformat): ts = datetime.strptime(instring, dateformat) - return int(datetime.combine(date.today(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000) + return int(datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000) class Jt9Decoder(Decoder): From a68ba01320c5161198aa52620edceb7539f129f4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 24 Sep 2019 21:42:41 +0200 Subject: [PATCH 0410/2616] handle socket timeouts --- owrx/websocket.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/owrx/websocket.py b/owrx/websocket.py index 71b0f382e..257ba0e11 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -160,6 +160,9 @@ def read_loop(self): logger.warning("unsupported opcode: {0}".format(opcode)) except IncompleteRead: available = False + except TimeoutError: + logger.warning("websocket timed out; closing connection") + self.open = False logger.debug("websocket loop ended; shutting down") From bfcbd0265ab1ef25c1e8595c10d83c1e7a609b69 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 24 Sep 2019 21:44:14 +0200 Subject: [PATCH 0411/2616] update config --- config_webrx.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config_webrx.py b/config_webrx.py index bfd362d1b..fbd3dbd23 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -284,3 +284,10 @@ # path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols) aprs_symbols_path = "/opt/aprs-symbols/png" + +# === PSK Reporter setting === +# enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info +# this also uses the receiver_qra setting from above, so make sure it contains a correct locator +# TODO determine locator from gps coordinates +pskreporter_enabled = False +pskreporter_callsign = "N0CALL" From 41bd018191e1e642df0fac8eff67996c772e710f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 25 Sep 2019 00:35:57 +0200 Subject: [PATCH 0412/2616] determine locator from gps coordinates --- config_webrx.py | 3 +-- owrx/locator.py | 24 ++++++++++++++++++++++++ owrx/pskreporter.py | 3 ++- 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 owrx/locator.py diff --git a/config_webrx.py b/config_webrx.py index fbd3dbd23..17660d5fb 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -287,7 +287,6 @@ # === PSK Reporter setting === # enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info -# this also uses the receiver_qra setting from above, so make sure it contains a correct locator -# TODO determine locator from gps coordinates +# this also uses the receiver_gps setting from above, so make sure it contains a correct locator pskreporter_enabled = False pskreporter_callsign = "N0CALL" diff --git a/owrx/locator.py b/owrx/locator.py new file mode 100644 index 000000000..3e6f9cc33 --- /dev/null +++ b/owrx/locator.py @@ -0,0 +1,24 @@ +class Locator(object): + @staticmethod + def fromCoordinates(coordinates, depth=3): + + lat, lon = coordinates + + lon = lon + 180 + lat = lat + 90 + + res = "" + res += chr(65 + int(lon / 20)) + res += chr(65 + int(lat / 10)) + if depth >= 2: + lon = lon % 20 + lat = lat % 10 + res += str(int(lon / 2)) + res += str(int(lat)) + if depth >= 3: + lon = lon % 2 + lat = lat % 1 + res += chr(97 + int(lon * 12)) + res += chr(97 + int(lat * 24)) + + return res \ No newline at end of file diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index 01cb157ac..9cc766cd5 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -6,6 +6,7 @@ from sched import scheduler from owrx.config import PropertyManager from owrx.version import openwebrx_version +from owrx.locator import Locator logger = logging.getLogger(__name__) @@ -143,7 +144,7 @@ def getReceiverInformationHeader(self): def getReceiverInformation(self): pm = PropertyManager.getSharedInstance() callsign = pm["pskreporter_callsign"] - locator = pm["receiver_qra"] + locator = Locator.fromCoordinates(pm["receiver_gps"]) decodingSoftware = "OpenWebRX " + openwebrx_version body = [b for s in [callsign, locator, decodingSoftware] for b in self.encodeString(s)] body = self.pad(body, 4) From ecb754ab29759b796f9bb599b4e6638753e2c5a6 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 25 Sep 2019 00:36:22 +0200 Subject: [PATCH 0413/2616] disable reporting if not set in config --- owrx/pskreporter.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index 9cc766cd5..8acd1a11f 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -11,6 +11,15 @@ logger = logging.getLogger(__name__) +class PskReporterDummy(object): + """ + used in place of the PskReporter when reporting is disabled. + does nothing. + """ + def spot(self, spot): + pass + + class PskReporter(object): sharedInstance = None creationLock = threading.Lock() @@ -21,7 +30,10 @@ class PskReporter(object): def getSharedInstance(): with PskReporter.creationLock: if PskReporter.sharedInstance is None: - PskReporter.sharedInstance = PskReporter() + if PropertyManager.getSharedInstance()["pskreporter_enabled"]: + PskReporter.sharedInstance = PskReporter() + else: + PskReporter.sharedInstance = PskReporterDummy() return PskReporter.sharedInstance def __init__(self): From 68fbc436f2a23a8aeaa2293ee7b23e1087d39a32 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 25 Sep 2019 00:36:40 +0200 Subject: [PATCH 0414/2616] fix length problem --- owrx/pskreporter.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index 8acd1a11f..8b2edb99d 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -94,17 +94,14 @@ def chunks(l, n): packets = [] # 50 seems to be a safe bet for chunk in chunks(encoded, 50): - sInfo = self.padBytes(b"".join(chunk), 4) - sInfoLength = len(sInfo) - length = sInfoLength + 16 + len(rHeader) + len(sHeader) + len(rInfo) + 4 + sInfo = self.getSenderInformation(chunk) + length = 16 + len(rHeader) + len(sHeader) + len(rInfo) + len(sInfo) header = self.getHeader(length) packets.append( header + rHeader + sHeader + rInfo - + bytes(Uploader.senderDelimiter) - + sInfoLength.to_bytes(2, "big") + sInfo ) @@ -186,6 +183,11 @@ def getSenderInformationHeader(self): + [0x00, 0x96, 0x00, 0x04] ) + def getSenderInformation(self, chunk): + sInfo = self.padBytes(b"".join(chunk), 4) + sInfoLength = len(sInfo) + 4 + return bytes(Uploader.senderDelimiter) + sInfoLength.to_bytes(2, "big") + sInfo + def pad(self, b, l): return b + [0x00 for _ in range(0, -1 * len(b) % l)] From 6911ca407ee3ba948ccf9423b21e4f717026532a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 25 Sep 2019 00:47:34 +0200 Subject: [PATCH 0415/2616] code format --- owrx/locator.py | 2 +- owrx/pskreporter.py | 9 ++------- owrx/wsjt.py | 4 +++- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/owrx/locator.py b/owrx/locator.py index 3e6f9cc33..ec80b032f 100644 --- a/owrx/locator.py +++ b/owrx/locator.py @@ -21,4 +21,4 @@ def fromCoordinates(coordinates, depth=3): res += chr(97 + int(lon * 12)) res += chr(97 + int(lat * 24)) - return res \ No newline at end of file + return res diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index 8b2edb99d..958d2f136 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -16,6 +16,7 @@ class PskReporterDummy(object): used in place of the PskReporter when reporting is disabled. does nothing. """ + def spot(self, spot): pass @@ -97,13 +98,7 @@ def chunks(l, n): sInfo = self.getSenderInformation(chunk) length = 16 + len(rHeader) + len(sHeader) + len(rInfo) + len(sInfo) header = self.getHeader(length) - packets.append( - header - + rHeader - + sHeader - + rInfo - + sInfo - ) + packets.append(header + rHeader + sHeader + rInfo + sInfo) return packets diff --git a/owrx/wsjt.py b/owrx/wsjt.py index e785e4e02..6539f846a 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -322,7 +322,9 @@ def setDialFrequency(self, freq): class Decoder(object): def parse_timestamp(self, instring, dateformat): ts = datetime.strptime(instring, dateformat) - return int(datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000) + return int( + datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000 + ) class Jt9Decoder(Decoder): From 01fabd0342490a4ec7d231ffce47e1487cc1a94d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 25 Sep 2019 23:05:27 +0200 Subject: [PATCH 0416/2616] use the 60m frequency for europe for now (seems to be controversial) --- bands.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bands.json b/bands.json index 29867c7ce..776007afb 100644 --- a/bands.json +++ b/bands.json @@ -30,7 +30,7 @@ "upper_bound": 5366500, "frequencies": { "ft8": 5357000, - "wspr": 5287200 + "wspr": 5364700 } }, { From b27eb4a173a804733d5b5150ae4560e3e406bf80 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 25 Sep 2019 23:12:30 +0200 Subject: [PATCH 0417/2616] code formatting --- config_webrx.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index 17660d5fb..b0316cb52 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -264,9 +264,7 @@ wsjt_decoding_depth = 3 # can also be set for each mode separately # jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent -wsjt_decoding_depths = { - "jt65": 1 -} +wsjt_decoding_depths = {"jt65": 1} temporary_directory = "/tmp" From a761559fd3e341edeb1673e3c94c04574496131a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 25 Sep 2019 23:25:49 +0200 Subject: [PATCH 0418/2616] latest news for everybody to see --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 67f9cbd93..11beb8889 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,13 @@ It has the following features: - [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN) - [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9) +**News (2019-09-25 by DD5JFK)** +- Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is now possible. Please have a look at the configuration on how to set it up. +- Websocket communication has been overhauled in large parts. It should now be more reliable, and failing connections should now have no impact on other users. +- Profile scheduling allows to set up band-hopping if you are running background services. +- APRS now has the ability to show symbols on the map, if a corresponding symbol set has been installed. Check the config! +- Debug logging has been disabled in a handful of modules, expect vastly reduced output on the shell. + **News (2019-09-13 by DD5JFK)** - New set of APRS-related features - Decode Packet transmissions using [direwolf](https://github.com/wb2osz/direwolf) (1k2 only for now) From 2d1bcf221cae19658804c3ff0510434048c677d5 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 25 Sep 2019 23:40:17 +0200 Subject: [PATCH 0419/2616] add aprs images to the docker build --- docker/scripts/install-dependencies.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 3befe6c5c..854801d5d 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -58,4 +58,6 @@ make install cd .. rm -rf direwolf +git clone https://github.com/hessu/aprs-symbols /opt/aprs-symbols + apk del .build-deps From 6cb7e652311a4fbb3ccae98f16489afb786a0368 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 26 Sep 2019 00:24:55 +0100 Subject: [PATCH 0420/2616] differentiate between None and empty return --- owrx/websocket.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/owrx/websocket.py b/owrx/websocket.py index 257ba0e11..84c489fbf 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -14,6 +14,10 @@ class IncompleteRead(Exception): pass +class Drained(Exception): + pass + + class WebSocketConnection(object): connections = [] @@ -120,7 +124,9 @@ def chunks(l, n): def protected_read(self, num): data = self.handler.rfile.read(num) - if data is None or len(data) != num: + if data is None: + raise Drained() + if len(data) != num: raise IncompleteRead() return data @@ -134,7 +140,7 @@ def read_loop(self): (read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], []) if self.handler.rfile in read: available = True - while available: + while self.open and available: try: header = self.protected_read(2) opcode = header[0] & 0x0F @@ -158,8 +164,11 @@ def read_loop(self): self.open = False else: logger.warning("unsupported opcode: {0}".format(opcode)) - except IncompleteRead: + except Drained: available = False + except IncompleteRead: + logger.warning("incomplete read on websocket; closing connection") + self.open = False except TimeoutError: logger.warning("websocket timed out; closing connection") self.open = False From 2c4add6aad39009d904cee153b31feaa96472bc0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 26 Sep 2019 03:08:52 +0200 Subject: [PATCH 0421/2616] update with latest sd card image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 11beb8889..463fb4581 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ It has the following features: ### Raspberry Pi SD Card Images -Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-07-13-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+. +Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-09-26-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+. This is based off the Raspbian Lite distribution, so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply. From 76fe11741afce310fd505a2166d74575eb45dbd1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 26 Sep 2019 22:57:10 +0200 Subject: [PATCH 0422/2616] add ping / pong to keep the websockets running --- owrx/websocket.py | 81 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/owrx/websocket.py b/owrx/websocket.py index 84c489fbf..4e58b4f0c 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -9,6 +9,12 @@ logger = logging.getLogger(__name__) +OPCODE_TEXT_MESSAGE = 0x01 +OPCODE_BINARY_MESSAGE = 0x02 +OPCODE_CLOSE = 0x08 +OPCODE_PING = 0x09 +OPCODE_PONG = 0x0A + class IncompleteRead(Exception): pass @@ -55,6 +61,8 @@ def __init__(self, handler, messageHandler): ws_key_toreturn.decode() ).encode() ) + self.pingTimer = None + self.resetPing() def get_header(self, size, opcode): ws_first_byte = 0b10000000 | (opcode & 0x0F) @@ -90,13 +98,17 @@ def send(self, data): # string-type messages are sent as text frames if type(data) == str: - header = self.get_header(len(data), 1) + header = self.get_header(len(data), OPCODE_TEXT_MESSAGE) data_to_send = header + data.encode("utf-8") # anything else as binary else: - header = self.get_header(len(data), 2) + header = self.get_header(len(data), OPCODE_BINARY_MESSAGE) data_to_send = header + data + self._sendBytes(data_to_send) + + def _sendBytes(self, data_to_send): + def chunks(l, n): """Yield successive n-sized chunks from l.""" for i in range(0, len(l), n): @@ -122,44 +134,50 @@ def chunks(l, n): logger.exception("ValueError while writing data") self.close() - def protected_read(self, num): - data = self.handler.rfile.read(num) - if data is None: - raise Drained() - if len(data) != num: - raise IncompleteRead() - return data - def interrupt(self): self.interruptPipeSend.send(bytes(0x00)) def read_loop(self): + def protected_read(num): + data = self.handler.rfile.read(num) + if data is None: + raise Drained() + if len(data) != num: + raise IncompleteRead() + return data + WebSocketConnection.connections.append(self) self.open = True while self.open: (read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], []) if self.handler.rfile in read: available = True + self.resetPing() while self.open and available: try: - header = self.protected_read(2) + header = protected_read(2) opcode = header[0] & 0x0F length = header[1] & 0x7F mask = (header[1] & 0x80) >> 7 if length == 126: - header = self.protected_read(2) + header = protected_read(2) length = (header[0] << 8) + header[1] if mask: - masking_key = self.protected_read(4) - data = self.protected_read(length) + masking_key = protected_read(4) + data = protected_read(length) if mask: data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) - if opcode == 1: + if opcode == OPCODE_TEXT_MESSAGE: message = data.decode("utf-8") self.messageHandler.handleTextMessage(self, message) - elif opcode == 2: + elif opcode == OPCODE_BINARY_MESSAGE: self.messageHandler.handleBinaryMessage(self, data) - elif opcode == 8: + elif opcode == OPCODE_PING: + self.sendPong() + elif opcode == OPCODE_PONG: + # since every read resets the ping timer, there's nothing to do here. + pass + elif opcode == OPCODE_CLOSE: logger.debug("websocket close frame received; closing connection") self.open = False else: @@ -176,17 +194,12 @@ def read_loop(self): logger.debug("websocket loop ended; shutting down") self.messageHandler.handleClose() + self.cancelPing() logger.debug("websocket loop ended; sending close frame") - try: - header = self.get_header(0, 8) - self.handler.wfile.write(header) - self.handler.wfile.flush() - except ValueError: - logger.exception("ValueError while writing close frame:") - except OSError: - logger.exception("OSError while writing close frame:") + header = self.get_header(0, OPCODE_CLOSE) + self._sendBytes(header) try: WebSocketConnection.connections.remove(self) @@ -197,6 +210,24 @@ def close(self): self.open = False self.interrupt() + def cancelPing(self): + if self.pingTimer: + self.pingTimer.cancel() + + def resetPing(self): + self.cancelPing() + self.pingTimer = threading.Timer(30, self.sendPing) + self.pingTimer.start() + + def sendPing(self): + header = self.get_header(0, OPCODE_PING) + self._sendBytes(header) + self.resetPing() + + def sendPong(self): + header = self.get_header(0, OPCODE_PONG) + self._sendBytes(header) + class WebSocketException(Exception): pass From 5f703a043b78bf3f3c887fc535ade32a861b5f71 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 27 Sep 2019 23:28:43 +0200 Subject: [PATCH 0423/2616] fix ping race condition --- owrx/websocket.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/owrx/websocket.py b/owrx/websocket.py index 4e58b4f0c..d0e4993a9 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -216,6 +216,9 @@ def cancelPing(self): def resetPing(self): self.cancelPing() + if not self.open: + logger.debug("resetPing() while closed. passing...") + return self.pingTimer = threading.Timer(30, self.sendPing) self.pingTimer.start() From 42c59a3aa0d8a4c27296fdcd8f6a3bd719e19930 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 27 Sep 2019 23:29:22 +0200 Subject: [PATCH 0424/2616] fft needs the multiprocessing send, too --- owrx/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/connection.py b/owrx/connection.py index e2e23d3fa..b667a5338 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -172,7 +172,7 @@ def setDspProperties(self, params): self.dsp.setProperty(key, value) def write_spectrum_data(self, data): - self.send(bytes([0x01]) + data) + self.mp_send(bytes([0x01]) + data) def write_dsp_data(self, data): self.send(bytes([0x02]) + data) From cbc7b73b1d1d9d40c6265651afbf23e22185e6f3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 00:25:36 +0200 Subject: [PATCH 0425/2616] hand over message handling after initial handshake instead of delegating --- owrx/connection.py | 84 ++++++++++++++++++++++++---------------------- owrx/websocket.py | 5 ++- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/owrx/connection.py b/owrx/connection.py index b667a5338..b2df9f958 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -39,6 +39,12 @@ def close(self): def mp_send(self, data): self.multiprocessingPipe.put(data, block=False) + def handleBinaryMessage(self, conn, data): + logger.error("unsupported binary message, discarding") + + def handleClose(self): + self.close() + class OpenWebRxReceiverClient(Client): config_keys = [ @@ -100,6 +106,35 @@ def __init__(self, conn): CpuUsageThread.getSharedInstance().add_client(self) + def handleTextMessage(self, conn, message): + try: + message = json.loads(message) + if "type" in message: + if message["type"] == "dspcontrol": + if "action" in message and message["action"] == "start": + self.startDsp() + + if "params" in message: + params = message["params"] + self.setDspProperties(params) + + if message["type"] == "config": + if "params" in message: + self.setParams(message["params"]) + if message["type"] == "setsdr": + if "params" in message: + self.setSdr(message["params"]["sdr"]) + if message["type"] == "selectprofile": + if "params" in message and "profile" in message["params"]: + profile = message["params"]["profile"].split("|") + self.setSdr(profile[0]) + self.sdr.activateProfile(profile[1]) + else: + logger.warning("received message without type: {0}".format(message)) + + except json.JSONDecodeError: + logger.warning("message is not json: {0}".format(message)) + def setSdr(self, id=None): next = SdrService.getSource(id) if next == self.sdr: @@ -229,6 +264,9 @@ def __init__(self, conn): Map.getSharedInstance().addClient(self) + def handleTextMessage(self, conn, message): + pass + def close(self): Map.getSharedInstance().removeClient(self) super().close() @@ -243,8 +281,6 @@ def write_update(self, update): class WebSocketMessageHandler(object): def __init__(self): self.handshake = None - self.client = None - self.dsp = None def handleTextMessage(self, conn, message): if message[:16] == "SERVER DE CLIENT": @@ -256,50 +292,18 @@ def handleTextMessage(self, conn, message): if "type" in self.handshake: if self.handshake["type"] == "receiver": - self.client = OpenWebRxReceiverClient(conn) + client = OpenWebRxReceiverClient(conn) if self.handshake["type"] == "map": - self.client = MapConnection(conn) + client = MapConnection(conn) # backwards compatibility else: - self.client = OpenWebRxReceiverClient(conn) + client = OpenWebRxReceiverClient(conn) + + # hand off all further communication to the correspondig connection + conn.setMessageHandler(client) return if not self.handshake: logger.warning("not answering client request since handshake is not complete") return - - try: - message = json.loads(message) - if "type" in message: - if message["type"] == "dspcontrol": - if "action" in message and message["action"] == "start": - self.client.startDsp() - - if "params" in message: - params = message["params"] - self.client.setDspProperties(params) - - if message["type"] == "config": - if "params" in message: - self.client.setParams(message["params"]) - if message["type"] == "setsdr": - if "params" in message: - self.client.setSdr(message["params"]["sdr"]) - if message["type"] == "selectprofile": - if "params" in message and "profile" in message["params"]: - profile = message["params"]["profile"].split("|") - self.client.setSdr(profile[0]) - self.client.sdr.activateProfile(profile[1]) - else: - logger.warning("received message without type: {0}".format(message)) - - except json.JSONDecodeError: - logger.warning("message is not json: {0}".format(message)) - - def handleBinaryMessage(self, conn, data): - logger.error("unsupported binary message, discarding") - - def handleClose(self): - if self.client: - self.client.close() diff --git a/owrx/websocket.py b/owrx/websocket.py index d0e4993a9..020cc23c2 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -38,7 +38,7 @@ def closeAll(): def __init__(self, handler, messageHandler): self.handler = handler self.handler.connection.setblocking(0) - self.messageHandler = messageHandler + self.setMessageHandler(messageHandler) (self.interruptPipeRecv, self.interruptPipeSend) = Pipe(duplex=False) self.open = True self.sendLock = threading.Lock() @@ -64,6 +64,9 @@ def __init__(self, handler, messageHandler): self.pingTimer = None self.resetPing() + def setMessageHandler(self, messageHandler): + self.messageHandler = messageHandler + def get_header(self, size, opcode): ws_first_byte = 0b10000000 | (opcode & 0x0F) if size > 2 ** 16 - 1: From 00febdf2556e5b20b0cae376294d4fdb30415557 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 00:27:42 +0200 Subject: [PATCH 0426/2616] implement all methods for consistency --- owrx/connection.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/owrx/connection.py b/owrx/connection.py index b2df9f958..39eb71245 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -39,6 +39,9 @@ def close(self): def mp_send(self, data): self.multiprocessingPipe.put(data, block=False) + def handleTextMessage(self, conn, message): + pass + def handleBinaryMessage(self, conn, data): logger.error("unsupported binary message, discarding") @@ -307,3 +310,9 @@ def handleTextMessage(self, conn, message): if not self.handshake: logger.warning("not answering client request since handshake is not complete") return + + def handleBinaryMessage(self, conn, data): + pass + + def handleClose(self): + pass From cc98c94b2b272858e316b6e4defe54e861b618fd Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 00:53:58 +0200 Subject: [PATCH 0427/2616] send bookmarks to client --- bookmarks.json | 21 +++++++++++++++++++++ owrx/bookmarks.py | 43 +++++++++++++++++++++++++++++++++++++++++++ owrx/connection.py | 6 ++++++ 3 files changed, 70 insertions(+) create mode 100644 bookmarks.json create mode 100644 owrx/bookmarks.py diff --git a/bookmarks.json b/bookmarks.json new file mode 100644 index 000000000..b1043692e --- /dev/null +++ b/bookmarks.json @@ -0,0 +1,21 @@ +[{ + "name": "DB0ZU", + "frequency": 145725000, + "modulation": "nfm" +},{ + "name": "DB0ZM", + "frequency": 145750000, + "modulation": "nfm" +},{ + "name": "DB0EL", + "frequency": 439275000, + "modulation": "nfm" +},{ + "name": "DB0NJ", + "frequency": 438775000, + "modulation": "nfm" +},{ + "name": "DB0NJ", + "frequency": 439437500, + "modulation": "dmr" +}] \ No newline at end of file diff --git a/owrx/bookmarks.py b/owrx/bookmarks.py new file mode 100644 index 000000000..90740883d --- /dev/null +++ b/owrx/bookmarks.py @@ -0,0 +1,43 @@ +import json + + +class Bookmark(object): + def __init__(self, j): + self.name = j["name"] + self.frequency = j["frequency"] + self.modulation = j["modulation"] + + def getName(self): + return self.name + + def getFrequency(self): + return self.frequency + + def getModulation(self): + return self.modulation + + def __dict__(self): + return { + "name": self.getName(), + "frequency": self.getFrequency(), + "modulation": self.getModulation(), + } + + +class Bookmarks(object): + sharedInstance = None + @staticmethod + def getSharedInstance(): + if Bookmarks.sharedInstance is None: + Bookmarks.sharedInstance = Bookmarks() + return Bookmarks.sharedInstance + + def __init__(self): + f = open("bookmarks.json", "r") + bookmarks_json = json.load(f) + f.close() + self.bookmarks = [Bookmark(d) for d in bookmarks_json] + + def getBookmarks(self, range): + (lo, hi) = range + return [b for b in self.bookmarks if lo <= b.getFrequency() <= hi] diff --git a/owrx/connection.py b/owrx/connection.py index 39eb71245..55b10ed09 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -3,6 +3,7 @@ from owrx.feature import FeatureDetector from owrx.version import openwebrx_version from owrx.bands import Bandplan +from owrx.bookmarks import Bookmarks from owrx.map import Map from multiprocessing import Queue import json @@ -168,6 +169,8 @@ def sendConfig(key, value): srh = configProps["samp_rate"] / 2 frequencyRange = (cf - srh, cf + srh) self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencies(frequencyRange)) + bookmarks = [b.__dict__() for b in Bookmarks.getSharedInstance().getBookmarks(frequencyRange)] + self.write_bookmarks(bookmarks) self.configSub = configProps.wire(sendConfig) sendConfig(None, None) @@ -254,6 +257,9 @@ def write_wsjt_message(self, message): def write_dial_frequendies(self, frequencies): self.send({"type": "dial_frequencies", "value": frequencies}) + def write_bookmarks(self, bookmarks): + self.send({"type": "bookmarks", "value": bookmarks}) + def write_aprs_data(self, data): self.send({"type": "aprs_data", "value": data}) From 020445743cdcd5626d366d58b72629bac8a64523 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 01:57:34 +0200 Subject: [PATCH 0428/2616] add bookmarks display --- htdocs/css/openwebrx.css | 45 ++++++ htdocs/index.html | 309 ++++++++++++++++++++------------------- htdocs/openwebrx.js | 25 ++++ 3 files changed, 225 insertions(+), 154 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 26941f028..b269a287a 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -160,6 +160,51 @@ input[type=range]:focus::-ms-fill-upper position: relative; } +#openwebrx-bookmarks-container +{ + height: 25px; + background-color: #444; + overflow: hidden; + position: relative; +} + +#openwebrx-bookmarks-container .bookmark-locator { + display: inline-block; + position: absolute; + bottom: 0; +} + +#openwebrx-bookmarks-container .bookmark { + font-size: 10pt; + background-color: #FFFF00; + border: 1px solid #000; + border-radius: 5px; + padding: 2px 5px; + cursor: pointer; + + position: absolute; + bottom: 5px; + transform: translate(-50%, 0); +} + +#openwebrx-bookmarks-container .bookmark:hover { + z-index: 11; +} + +#openwebrx-bookmarks-container .bookmark:after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 0; + border: 5px solid transparent; + border-top-color: #FFFF00; + border-bottom: 0; + margin-left: -5px; + margin-bottom: -5px; +} + #webrx-canvas-container { /*background-image:url('../gfx/openwebrx-blank-background-1.jpg');*/ diff --git a/htdocs/index.html b/htdocs/index.html index 57247e0e0..d44c6db31 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -35,173 +35,174 @@
    ${header}
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    ---.--- MHz
    -
    ---.--- MHz
    -
    - -
    -
    -
    FM
    -
    AM
    -
    LSB
    -
    USB
    -
    CW
    - - - - -
    -
    -
    DIG
    - -
    - - - - - -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    0 dB
    -
    -
    -
    -
    -
    -
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    ---.--- MHz
    +
    ---.--- MHz
    +
    + +
    +
    +
    FM
    +
    AM
    +
    LSB
    +
    USB
    +
    CW
    + + + +
    -
    -
    -
    -
    OpenWebRX client log
    - Author: András Retzler, HA7ILM
    You can support OpenWebRX development via PayPal!
    -
    -
    +
    +
    DIG
    + +
    + + + + +
    -
    -
    Audio buffer [0 ms]
    -
    Audio output [0 sps]
    -
    Audio stream [0 kbps]
    -
    Network usage [0 kbps]
    -
    Server CPU [0%]
    -
    Clients [1]
    +
    +
    + +
    + +
    +
    +
    + +
    +
    -
    - Under construction -
    We're working on the code right now, so the application might fail. +
    +
    +
    +
    +
    +
    +
    0 dB
    -
    -
    -
    +
    +
    +
    -
    -
    -
    - -
    +
    +
    +
    +
    +
    +
    OpenWebRX client log
    + Author: András Retzler, HA7ILM
    You can support OpenWebRX development via PayPal!
    +
    +
    +
    +
    +
    +
    Audio buffer [0 ms]
    +
    Audio output [0 sps]
    +
    Audio stream [0 kbps]
    +
    Network usage [0 kbps]
    +
    Server CPU [0%]
    +
    Clients [1]
    +
    +
    + Under construction +
    We're working on the code right now, so the application might fail. +
    +
    +
    +
    +
    +
    +
    +
    +
    - - - - - - - - - -
    UTCdBDTFreqMessage
    - - - - - - - - -
    UTCCallsignCoordComment
    -
    -
    -
    -
    -
    -
    -
    -
    -
    +
    + + + + + + + + + +
    UTCdBDTFreqMessage
    + + + + + + + + +
    UTCCallsignCoordComment
    +
    +
    +
    +
    +
    +
    +
    +
    -
    -
    -
    -
    Timeslot 1
    -
    -
    -
    -
    -
    -
    -
    Timeslot 2
    -
    -
    -
    -
    -
    +
    +
    +
    +
    +
    Timeslot 1
    +
    +
    +
    +
    +
    +
    +
    Timeslot 2
    +
    +
    +
    +
    +
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 5508d0e54..44ab1ce38 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -904,6 +904,7 @@ function resize_scale() scale_ctx.canvas.width = window.innerWidth; scale_ctx.canvas.height = 47; mkscale(); + position_bookmarks(); } function canvas_mouseover(evt) @@ -992,6 +993,7 @@ function canvas_mousemove(evt) canvas_drag_last_x=evt.pageX; canvas_drag_last_y=evt.pageY; mkscale(); + position_bookmarks(); } } else e("webrx-mouse-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4); @@ -1092,6 +1094,7 @@ function zoom_step(out, where, onscreen) //console.log(zoom_center_where, zoom_center_rel, where); resize_canvases(true); mkscale(); + position_bookmarks(); } function zoom_set(level) @@ -1105,6 +1108,7 @@ function zoom_set(level) console.log(zoom_center_where, zoom_center_rel, -canvases[0].offsetLeft+canvas_container.clientWidth/2); resize_canvases(true); mkscale(); + position_bookmarks(); } function zoom_calc() @@ -1256,6 +1260,9 @@ function on_ws_recv(evt) case "aprs_data": update_packet_panel(json.value); break; + case "bookmarks": + update_bookmarks(json.value); + break; default: console.warn('received message of unknown type: ' + json.type); } @@ -1321,6 +1328,24 @@ function on_ws_recv(evt) } } +function update_bookmarks(bookmarks) { + $container = $('#openwebrx-bookmarks-container'); + $container.empty(); + bookmarks.forEach(function(b){ + $bookmark = $('
    ' + b.name + '
    '); + $bookmark.data(b); + $container.append($bookmark); + }); + position_bookmarks(); +} + +function position_bookmarks() { + range = get_visible_freq_range(); + $('#openwebrx-bookmarks-container .bookmark-locator').each(function(){ + $(this).css('left', scale_px_from_freq($(this).data('frequency'), range)); + }); +} + var dial_frequencies = []; function find_dial_frequencies() { From 12a341e607f553962093129cbdef69d0eae226b0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 02:21:29 +0200 Subject: [PATCH 0429/2616] click handling and tuning --- htdocs/css/openwebrx.css | 4 ++++ htdocs/openwebrx.js | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index b269a287a..c33ed2632 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -187,6 +187,10 @@ input[type=range]:focus::-ms-fill-upper transform: translate(-50%, 0); } +#openwebrx-bookmarks-container .bookmark.selected { + z-index: 10; +} + #openwebrx-bookmarks-container .bookmark:hover { z-index: 11; } diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 44ab1ce38..aa55f0bad 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -630,6 +630,7 @@ function demodulator_set_offset_frequency(which,to_what) demodulators[0].offset_frequency=Math.round(to_what); demodulators[0].set(); mkenvelopes(get_visible_freq_range()); + $("#webrx-actual-freq").html(format_frequency("{x} MHz", center_freq + to_what, 1e6, 4)); } @@ -1016,7 +1017,6 @@ function canvas_mouseup(evt) { //ws.send("SET offset_freq="+canvas_get_freq_offset(relativeX).toString()); demodulator_set_offset_frequency(0, canvas_get_freq_offset(relativeX)); - e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4); } else { @@ -1346,6 +1346,19 @@ function position_bookmarks() { }); } +function init_bookmarks() { + $container = $("#openwebrx-bookmarks-container") + $container.click(function(e){ + $container.find('.bookmark').removeClass('selected'); + $bookmark = $(e.target); + b = $bookmark.closest('.bookmark-locator').data(); + if (!b || !b.frequency || !b.modulation) return; + demodulator_set_offset_frequency(0, b.frequency - center_freq); + demodulator_analog_replace(b.modulation); + $bookmark.addClass('selected'); + }); +} + var dial_frequencies = []; function find_dial_frequencies() { @@ -1366,7 +1379,6 @@ function dial_button_click() { var frequency = available[0].frequency; console.info(frequency); demodulator_set_offset_frequency(0, frequency - center_freq); - $("#webrx-actual-freq").html(format_frequency("{x} MHz", frequency, 1e6, 4)); } function update_metadata(meta) { @@ -2533,6 +2545,7 @@ function openwebrx_init() window.addEventListener("resize",openwebrx_resize); check_top_bar_congestion(); init_header(); + init_bookmarks(); //Synchronise volume with slider updateVolume(); From d0c0ee29813882df856607df52a09d9766d818c8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 02:34:58 +0200 Subject: [PATCH 0430/2616] prevent line-wraps (not enough space) --- htdocs/css/openwebrx.css | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index c33ed2632..61af5caaa 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -181,6 +181,7 @@ input[type=range]:focus::-ms-fill-upper border-radius: 5px; padding: 2px 5px; cursor: pointer; + white-space: nowrap; position: absolute; bottom: 5px; From 9669b4e365be7620fb258a9c322f5dd40d41e407 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 02:35:12 +0200 Subject: [PATCH 0431/2616] moar bookmarks --- bookmarks.json | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/bookmarks.json b/bookmarks.json index b1043692e..96262f357 100644 --- a/bookmarks.json +++ b/bookmarks.json @@ -6,6 +6,10 @@ "name": "DB0ZM", "frequency": 145750000, "modulation": "nfm" +},{ + "name": "DM0ULR", + "frequency": 145787500, + "modulation": "nfm" },{ "name": "DB0EL", "frequency": 439275000, @@ -18,4 +22,36 @@ "name": "DB0NJ", "frequency": 439437500, "modulation": "dmr" +},{ + "name": "DB0UFO", + "frequency": 438312500, + "modulation": "dmr" +},{ + "name": "DB0PV", + "frequency": 438525000, + "modulation": "ysf" +},{ + "name": "DB0BZA", + "frequency": 438412500, + "modulation": "ysf" +},{ + "name": "DB0OSH", + "frequency": 438250000, + "modulation": "ysf" +},{ + "name": "DB0ULR", + "frequency": 439325000, + "modulation": "nfm" +},{ + "name": "DB0ZU", + "frequency": 43885000, + "modulation": "nfm" +},{ + "name": "DB0ISW", + "frequency": 43865000, + "modulation": "nfm" +},{ + "name": "Radio DARC", + "frequency": 6070000, + "modulation": "am" }] \ No newline at end of file From 31881ce47264e0089254dec08c21f07e23148a62 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 03:03:41 +0200 Subject: [PATCH 0432/2616] standard font size --- htdocs/css/openwebrx.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 61af5caaa..d8499e77d 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -175,7 +175,7 @@ input[type=range]:focus::-ms-fill-upper } #openwebrx-bookmarks-container .bookmark { - font-size: 10pt; + font-size: 12px; background-color: #FFFF00; border: 1px solid #000; border-radius: 5px; From 455001a75916b7fdba4fa0da999810f85746377b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 03:06:34 +0200 Subject: [PATCH 0433/2616] protect pskreporter upload loop --- owrx/pskreporter.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index 958d2f136..cb9797b92 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -57,12 +57,15 @@ def spot(self, spot): self.spots.append(spot) def upload(self): - with self.spotLock: - spots = self.spots - self.spots = [] - - if spots: - self.uploader.upload(spots) + try: + with self.spotLock: + spots = self.spots + self.spots = [] + + if spots: + self.uploader.upload(spots) + except Exception: + logger.exception("Failed to upload spots") self.scheduleNextUpload() From 46162dadbed33805a4c068240c03ba68be003453 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 03:06:34 +0200 Subject: [PATCH 0434/2616] protect pskreporter upload loop --- owrx/pskreporter.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index 958d2f136..cb9797b92 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -57,12 +57,15 @@ def spot(self, spot): self.spots.append(spot) def upload(self): - with self.spotLock: - spots = self.spots - self.spots = [] - - if spots: - self.uploader.upload(spots) + try: + with self.spotLock: + spots = self.spots + self.spots = [] + + if spots: + self.uploader.upload(spots) + except Exception: + logger.exception("Failed to upload spots") self.scheduleNextUpload() From 1b95807ac6975b56eb641d4a2c8b9147178b3d64 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 07:36:28 +0200 Subject: [PATCH 0435/2616] beautiful 2x scale for retina displays --- htdocs/openwebrx.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 5508d0e54..9c0b81983 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -901,8 +901,16 @@ function mkscale() function resize_scale() { - scale_ctx.canvas.width = window.innerWidth; - scale_ctx.canvas.height = 47; + ratio = window.devicePixelRatio || 1; + var w = window.innerWidth; + var h = 47; + scale_canvas.style.width = w + "px"; + scale_canvas.style.height = h + "px"; + w *= ratio; + h *= ratio; + scale_canvas.width = w; + scale_canvas.height = h; + scale_ctx.scale(ratio, ratio); mkscale(); } @@ -1339,7 +1347,6 @@ function dial_button_click() { var available = find_dial_frequencies(); if (!available.length) return; var frequency = available[0].frequency; - console.info(frequency); demodulator_set_offset_frequency(0, frequency - center_freq); $("#webrx-actual-freq").html(format_frequency("{x} MHz", frequency, 1e6, 4)); } From 4407146962a5d7dc22a7602ce266040bc4d32e15 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 16:50:21 +0200 Subject: [PATCH 0436/2616] add bookmark button --- htdocs/css/openwebrx.css | 20 ++++++++++++++++++++ htdocs/gfx/openwebrx-bookmark.png | Bin 0 -> 970 bytes htdocs/index.html | 11 +++++++++-- htdocs/openwebrx.js | 7 +++++++ 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 htdocs/gfx/openwebrx-bookmark.png diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index d8499e77d..10ad9d84a 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -537,6 +537,22 @@ img.openwebrx-mirror-img width:110px; } + +#openwebrx-panel-receiver .frequencies-container { + display: flex; + flex-direction: row; +} + +#openwebrx-panel-receiver .frequencies { + flex-grow: 1; +} + +#openwebrx-panel-receiver .openwebrx-bookmark-button { + width: 27px; + height: 27px; + text-align: center; +} + #openwebrx-mute-on { color: lime; @@ -566,6 +582,10 @@ img.openwebrx-mirror-img padding-top: 5px; } +.openwebrx-panel-line:first-child { + padding-top: 0; +} + #openwebrx-smeter-outer { border-color: #888; diff --git a/htdocs/gfx/openwebrx-bookmark.png b/htdocs/gfx/openwebrx-bookmark.png new file mode 100644 index 0000000000000000000000000000000000000000..bf1164bfcbfee058f8c53eb11cd505f516c310c8 GIT binary patch literal 970 zcmV;*12z1KP)EX>4Tx04R}tkv&MmKpe$iTcs*h2Rn##$WV2$AS&XhRVYG*P%E_RU~=gfG-*gu zTpR`0f`cE6RRbo!zg?bGxa%9Ou}<~-NVP%yBN>%KKJM7RdOZ+d;;+-(+!JwgLrz= z(mC%Fhgnflh|h_~47wokBiCh@-#8Z>_VdiJkxtDMhlzzk8_R9XiiS!&MI2RBjq?2& zmle)ioYiubHSft^7|LlY%Uq`!K>~|df(QXJswklh3o%+XQcR?1Kknfla{MB>WO9|j z$gzM5R7j2={11M2Yvv~>+@w$(=zOv5k6|FV3p8rB{e5iPjT0d73|wg~f29u0e3D*k zY0)E~e;c^CZfWu!aJd5vKIxJnIZ}Y8Kc5HQ&*+=7z`!lgv*z~J+{ftykfyGZH^9Lm zFjAoGb&q#9r0knIzmUdVF)A?ng9OO^!WY`lI&4Du7RxD(>jXCZH5DWR@ z=4W$4Tr?Y9D=H*KZUNmD#znKkEt;~CXTanyo{{R30 literal 0 HcmV?d00001 diff --git a/htdocs/index.html b/htdocs/index.html index d44c6db31..f1d37d4f2 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -46,8 +46,15 @@
    -
    ---.--- MHz
    -
    ---.--- MHz
    +
    +
    +
    ---.--- MHz
    +
    ---.--- MHz
    +
    + +
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 157bc73ee..c409555bf 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1365,6 +1365,13 @@ function init_bookmarks() { demodulator_analog_replace(b.modulation); $bookmark.addClass('selected'); }); + + $bookmarkButton = $('#openwebrx-panel-receiver .openwebrx-bookmark-button'); + if (typeof(Storage) !== 'undefined') { + $bookmarkButton.show(); + } else { + $bookmarkButton.hide(); + } } var dial_frequencies = []; From 4c2979d2425e3750d64a6d8f6f7eb3b2183b8a27 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 16:51:34 +0200 Subject: [PATCH 0437/2616] add z-index to prevent other content shining through --- htdocs/css/openwebrx.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 10ad9d84a..75d953574 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -166,6 +166,7 @@ input[type=range]:focus::-ms-fill-upper background-color: #444; overflow: hidden; position: relative; + z-index: 1000; } #openwebrx-bookmarks-container .bookmark-locator { @@ -189,11 +190,11 @@ input[type=range]:focus::-ms-fill-upper } #openwebrx-bookmarks-container .bookmark.selected { - z-index: 10; + z-index: 1010; } #openwebrx-bookmarks-container .bookmark:hover { - z-index: 11; + z-index: 1011; } #openwebrx-bookmarks-container .bookmark:after { From 39a4366eabcdc8382aa7db8e9bd5c2c8e12d4250 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 16:54:24 +0200 Subject: [PATCH 0438/2616] locator wrappers aren't even needed --- htdocs/css/openwebrx.css | 6 ------ htdocs/openwebrx.js | 6 +++--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 75d953574..8eb6efef0 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -169,12 +169,6 @@ input[type=range]:focus::-ms-fill-upper z-index: 1000; } -#openwebrx-bookmarks-container .bookmark-locator { - display: inline-block; - position: absolute; - bottom: 0; -} - #openwebrx-bookmarks-container .bookmark { font-size: 12px; background-color: #FFFF00; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index c409555bf..d0c3de1f5 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1340,7 +1340,7 @@ function update_bookmarks(bookmarks) { $container = $('#openwebrx-bookmarks-container'); $container.empty(); bookmarks.forEach(function(b){ - $bookmark = $('
    ' + b.name + '
    '); + $bookmark = $('
    ' + b.name + '
    '); $bookmark.data(b); $container.append($bookmark); }); @@ -1349,7 +1349,7 @@ function update_bookmarks(bookmarks) { function position_bookmarks() { range = get_visible_freq_range(); - $('#openwebrx-bookmarks-container .bookmark-locator').each(function(){ + $('#openwebrx-bookmarks-container .bookmark').each(function(){ $(this).css('left', scale_px_from_freq($(this).data('frequency'), range)); }); } @@ -1359,7 +1359,7 @@ function init_bookmarks() { $container.click(function(e){ $container.find('.bookmark').removeClass('selected'); $bookmark = $(e.target); - b = $bookmark.closest('.bookmark-locator').data(); + b = $bookmark.closest('.bookmark').data(); if (!b || !b.frequency || !b.modulation) return; demodulator_set_offset_frequency(0, b.frequency - center_freq); demodulator_analog_replace(b.modulation); From b29d3c575dcaaee2af2ca360792ac0aedd22b626 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 19:19:55 +0200 Subject: [PATCH 0439/2616] even moar bookmarks --- bookmarks.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bookmarks.json b/bookmarks.json index 96262f357..ded220b32 100644 --- a/bookmarks.json +++ b/bookmarks.json @@ -54,4 +54,12 @@ "name": "Radio DARC", "frequency": 6070000, "modulation": "am" +},{ + "name": "DB0TVM", + "frequency": 439575000, + "modulation": "dstar" +},{ + "name": "DB0TVM", + "frequency": 439800000, + "modulation": "dmr" }] \ No newline at end of file From be21d4c9ac62cdee6a98a80589f8f6b9dd6fe694 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 19:20:21 +0200 Subject: [PATCH 0440/2616] show dialog and load values into it --- htdocs/css/openwebrx.css | 46 ++++++++++++++++++++++++++++++++++++++-- htdocs/index.html | 26 +++++++++++++++++++++++ htdocs/openwebrx.js | 12 +++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 8eb6efef0..2bae7813b 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -659,7 +659,9 @@ img.openwebrx-mirror-img transition: width 500ms, left 500ms; } -.openwebrx-panel select { +.openwebrx-panel select, +.openwebrx-dialog select, +.openwebrx-dialog input { border-radius: 5px; background-color: #373737; color: White; @@ -673,7 +675,8 @@ img.openwebrx-mirror-img -moz-appearance: none; } -.openwebrx-panel select option { +.openwebrx-panel select option, +.openwebrx-dialog select option { border-width: 0px; background-color: #373737; color: White; @@ -954,6 +957,45 @@ img.openwebrx-mirror-img background-image: url(../../aprs-symbols/aprs-symbols-24-2.png) } +.openwebrx-dialog { + background-color: #575757; + padding: 10px; + color: white; + position: fixed; + font-size: 10pt; + border-radius: 15px; + -moz-border-radius: 15px; + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, 0); +} + +.openwebrx-dialog .form-field { + padding: 5px; + display: flex; + flex-direction: row; +} + +.openwebrx-dialog label { + display: inline-block; + flex: 1 0 20px; + padding-right: 20px; + margin-top: auto; + margin-bottom: auto; +} + +.openwebrx-dialog .form-field input, +.openwebrx-dialog .form-field select { + flex: 2 0 20px; + height: 27px; +} + +.openwebrx-dialog .form-field input { + padding: 0 5px; +} + + #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container, diff --git a/htdocs/index.html b/htdocs/index.html index f1d37d4f2..4f5a4d23e 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -218,5 +218,31 @@

    Start OpenWebRX
    + diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index d0c3de1f5..d9943a964 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1372,6 +1372,18 @@ function init_bookmarks() { } else { $bookmarkButton.hide(); } + $bookmarkButton.click(function(){ + showBookmarkEditDialog(); + }); +} + +function showBookmarkEditDialog() { + $dialog = $("#openwebrx-dialog-bookmark"); + $form = $dialog.find("form"); + $form.find("#name").val(""); + $form.find("#frequency").val(center_freq + demodulators[0].offset_frequency); + $form.find("#modulation").val(demodulators[0].subtype); + $dialog.show(); } var dial_frequencies = []; From bd9cdc1cba937df8be64017267e2b8e8168d6f0f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 20:15:47 +0200 Subject: [PATCH 0441/2616] buttons and storage action --- htdocs/css/openwebrx.css | 16 ++++++++++++++ htdocs/index.html | 7 +++++- htdocs/openwebrx.js | 47 +++++++++++++++++++++++++++++++++++----- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 2bae7813b..f91a59270 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -977,6 +977,10 @@ img.openwebrx-mirror-img flex-direction: row; } +.openwebrx-dialog .form-field:first-child { + padding-top: 0; +} + .openwebrx-dialog label { display: inline-block; flex: 1 0 20px; @@ -995,6 +999,18 @@ img.openwebrx-mirror-img padding: 0 5px; } +.openwebrx-dialog .buttons { + text-align: right; + padding: 5px 5px 0; + border-top: 1px solid #666; +} + +.openwebrx-dialog .buttons .openwebrx-button { + font-size: 12pt; + min-width: 50px; + text-align: center; + padding: 5px 10px; +} #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container, diff --git a/htdocs/index.html b/htdocs/index.html index 4f5a4d23e..11bb2b18a 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -226,7 +226,7 @@
    - +
    @@ -242,6 +242,11 @@
    +
    +
    Cancel
    +
    Ok
    +
    +
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index d9943a964..0aca0585e 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1337,7 +1337,7 @@ function on_ws_recv(evt) } function update_bookmarks(bookmarks) { - $container = $('#openwebrx-bookmarks-container'); + var $container = $('#openwebrx-bookmarks-container'); $container.empty(); bookmarks.forEach(function(b){ $bookmark = $('
    ' + b.name + '
    '); @@ -1348,14 +1348,14 @@ function update_bookmarks(bookmarks) { } function position_bookmarks() { - range = get_visible_freq_range(); + var range = get_visible_freq_range(); $('#openwebrx-bookmarks-container .bookmark').each(function(){ $(this).css('left', scale_px_from_freq($(this).data('frequency'), range)); }); } function init_bookmarks() { - $container = $("#openwebrx-bookmarks-container") + var $container = $("#openwebrx-bookmarks-container") $container.click(function(e){ $container.find('.bookmark').removeClass('selected'); $bookmark = $(e.target); @@ -1366,7 +1366,7 @@ function init_bookmarks() { $bookmark.addClass('selected'); }); - $bookmarkButton = $('#openwebrx-panel-receiver .openwebrx-bookmark-button'); + var $bookmarkButton = $('#openwebrx-panel-receiver .openwebrx-bookmark-button'); if (typeof(Storage) !== 'undefined') { $bookmarkButton.show(); } else { @@ -1375,17 +1375,52 @@ function init_bookmarks() { $bookmarkButton.click(function(){ showBookmarkEditDialog(); }); + + var $dialog = $("#openwebrx-dialog-bookmark"); + $dialog.find('.openwebrx-button[data-action=cancel]').click(function(){ + $dialog.hide(); + }); + $dialog.find('.openwebrx-button[data-action=submit]').click(function(){ + storeNewBookmark(); + }); } function showBookmarkEditDialog() { - $dialog = $("#openwebrx-dialog-bookmark"); - $form = $dialog.find("form"); + var $dialog = $("#openwebrx-dialog-bookmark"); + var $form = $dialog.find("form"); $form.find("#name").val(""); $form.find("#frequency").val(center_freq + demodulators[0].offset_frequency); $form.find("#modulation").val(demodulators[0].subtype); $dialog.show(); } +function storeNewBookmark() { + var $dialog = $("#openwebrx-dialog-bookmark"); + var bookmark = {}; + var valid = true; + ['name', 'frequency', 'modulation'].forEach(function(key){ + var $input = $dialog.find('#' + key); + valid = valid && $input[0].checkValidity(); + bookmark[key] = $input.val(); + }); + if (!valid) { + $dialog.find("form :submit").click(); + return; + } + var bookmarks = getLocalBookmarks(); + bookmarks.push(bookmark); + setLocalBookmarks(bookmarks); + $dialog.hide(); +} + +function getLocalBookmarks(){ + return JSON.parse(window.localStorage.getItem("bookmarks")) || []; +} + +function setLocalBookmarks(bookmarks){ + window.localStorage.setItem("bookmarks", JSON.stringify(bookmarks)); +} + var dial_frequencies = []; function find_dial_frequencies() { From af9fcbc38d30813e70ef4e855bb6f0ed4305c1b4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 20:28:25 +0200 Subject: [PATCH 0442/2616] complete storage and display --- htdocs/css/openwebrx.css | 8 ++++++++ htdocs/openwebrx.js | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index f91a59270..283cabcb9 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -205,6 +205,14 @@ input[type=range]:focus::-ms-fill-upper margin-bottom: -5px; } +#openwebrx-bookmarks-container .bookmark[data-source=local] { + background-color: #0FF; +} + +#openwebrx-bookmarks-container .bookmark[data-source=local]:after { + border-top-color: #0FF; +} + #webrx-canvas-container { /*background-image:url('../gfx/openwebrx-blank-background-1.jpg');*/ diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 0aca0585e..6b2d648e5 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1215,6 +1215,7 @@ function on_ws_recv(evt) waterfall_init(); audio_preinit(); + loadLocalBookmarks(); if (audio_allowed && !audio_initialized) audio_init(); waterfall_clear(); @@ -1269,7 +1270,7 @@ function on_ws_recv(evt) update_packet_panel(json.value); break; case "bookmarks": - update_bookmarks(json.value); + update_bookmarks(json.value, "server"); break; default: console.warn('received message of unknown type: ' + json.type); @@ -1336,17 +1337,25 @@ function on_ws_recv(evt) } } -function update_bookmarks(bookmarks) { +function update_bookmarks(bookmarks, source) { var $container = $('#openwebrx-bookmarks-container'); - $container.empty(); + $container.find('.bookmark[data-source=' + source + ']').remove(); bookmarks.forEach(function(b){ - $bookmark = $('
    ' + b.name + '
    '); + $bookmark = $('
    ' + b.name + '
    '); $bookmark.data(b); $container.append($bookmark); }); position_bookmarks(); } +function loadLocalBookmarks() { + var range = get_visible_freq_range(); + var bookmarks = getLocalBookmarks().filter(function(b){ + return b.frequency >= range.start && b.frequency <= range.end; + }); + update_bookmarks(bookmarks, 'local'); +} + function position_bookmarks() { var range = get_visible_freq_range(); $('#openwebrx-bookmarks-container .bookmark').each(function(){ @@ -1407,9 +1416,11 @@ function storeNewBookmark() { $dialog.find("form :submit").click(); return; } + bookmark.frequency = Number(bookmark.frequency); var bookmarks = getLocalBookmarks(); bookmarks.push(bookmark); setLocalBookmarks(bookmarks); + loadLocalBookmarks(); $dialog.hide(); } From fef6f3bbd1cf1c707f94b8232516b0987e91ece9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 20:42:17 +0200 Subject: [PATCH 0443/2616] fix bookmark frequencies --- bookmarks.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookmarks.json b/bookmarks.json index ded220b32..96f220d8e 100644 --- a/bookmarks.json +++ b/bookmarks.json @@ -44,11 +44,11 @@ "modulation": "nfm" },{ "name": "DB0ZU", - "frequency": 43885000, + "frequency": 438850000, "modulation": "nfm" },{ "name": "DB0ISW", - "frequency": 43865000, + "frequency": 438650000, "modulation": "nfm" },{ "name": "Radio DARC", From f292ba55c12ff8f497e90a97e5a098b1357a61fb Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 28 Sep 2019 20:52:37 +0200 Subject: [PATCH 0444/2616] use actual, not visible, frequency --- htdocs/openwebrx.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 6b2d648e5..80e218e9e 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1349,9 +1349,11 @@ function update_bookmarks(bookmarks, source) { } function loadLocalBookmarks() { - var range = get_visible_freq_range(); + var bwh = bandwidth / 2; + var start = center_freq - bwh; + var end = center_freq + bwh; var bookmarks = getLocalBookmarks().filter(function(b){ - return b.frequency >= range.start && b.frequency <= range.end; + return b.frequency >= start && b.frequency <= end; }); update_bookmarks(bookmarks, 'local'); } From 4a7b42202eef1236c3a29c4a76d7bd2d1132e4ab Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 29 Sep 2019 14:48:36 +0200 Subject: [PATCH 0445/2616] add edit and delete button --- htdocs/css/openwebrx.css | 28 +++++++++++++++++++++++++++- htdocs/gfx/openwebrx-edit.png | Bin 0 -> 1325 bytes htdocs/gfx/openwebrx-trashcan.png | Bin 0 -> 797 bytes htdocs/openwebrx.js | 15 ++++++++++++--- 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 htdocs/gfx/openwebrx-edit.png create mode 100644 htdocs/gfx/openwebrx-trashcan.png diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 283cabcb9..619ebb30d 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -164,7 +164,6 @@ input[type=range]:focus::-ms-fill-upper { height: 25px; background-color: #444; - overflow: hidden; position: relative; z-index: 1000; } @@ -177,18 +176,45 @@ input[type=range]:focus::-ms-fill-upper padding: 2px 5px; cursor: pointer; white-space: nowrap; + max-height: 14px; + max-width: 50px; position: absolute; bottom: 5px; transform: translate(-50%, 0); } +#openwebrx-bookmarks-container .bookmark .bookmark-content { + overflow: hidden; + text-overflow: ellipsis; +} + +#openwebrx-bookmarks-container .bookmark .bookmark-actions { + display: none; + text-align: right; +} + +.bookmark-actions .action { + line-height: 0; +} + +.bookmark-actions .action img { + width: 14px; +} + #openwebrx-bookmarks-container .bookmark.selected { z-index: 1010; } #openwebrx-bookmarks-container .bookmark:hover { z-index: 1011; + max-height: none; + max-width: none; +} + +#openwebrx-bookmarks-container .bookmark[editable]:hover .bookmark-actions { + display: block; + margin-bottom: 5px; } #openwebrx-bookmarks-container .bookmark:after { diff --git a/htdocs/gfx/openwebrx-edit.png b/htdocs/gfx/openwebrx-edit.png new file mode 100644 index 0000000000000000000000000000000000000000..ab403a53316d388fea4dd78f122e32685afcc53a GIT binary patch literal 1325 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 bG%;KNQNqnyh|#~t00000NkvXXu0mjf=jUA$ literal 0 HcmV?d00001 diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 80e218e9e..76949d630 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1337,11 +1337,20 @@ function on_ws_recv(evt) } } -function update_bookmarks(bookmarks, source) { +function update_bookmarks(bookmarks, source, editable) { + editable = !!editable; var $container = $('#openwebrx-bookmarks-container'); $container.find('.bookmark[data-source=' + source + ']').remove(); bookmarks.forEach(function(b){ - $bookmark = $('
    ' + b.name + '
    '); + $bookmark = $( + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + b.name + '
    ' + + '
    ' + ); $bookmark.data(b); $container.append($bookmark); }); @@ -1355,7 +1364,7 @@ function loadLocalBookmarks() { var bookmarks = getLocalBookmarks().filter(function(b){ return b.frequency >= start && b.frequency <= end; }); - update_bookmarks(bookmarks, 'local'); + update_bookmarks(bookmarks, 'local', true); } function position_bookmarks() { From eed520daacb16a3e24ed25036a9b94bda4c27b12 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 29 Sep 2019 15:29:53 +0200 Subject: [PATCH 0446/2616] implement edit and delete --- htdocs/openwebrx.js | 59 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 76949d630..f5bd86757 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1376,16 +1376,28 @@ function position_bookmarks() { function init_bookmarks() { var $container = $("#openwebrx-bookmarks-container") - $container.click(function(e){ + $container.on('click', '.bookmark', function(e){ + var $bookmark = $(e.target).closest('.bookmark'); $container.find('.bookmark').removeClass('selected'); - $bookmark = $(e.target); - b = $bookmark.closest('.bookmark').data(); + var b = $bookmark.data(); if (!b || !b.frequency || !b.modulation) return; demodulator_set_offset_frequency(0, b.frequency - center_freq); demodulator_analog_replace(b.modulation); $bookmark.addClass('selected'); }); + $container.on('click', '.action[data-action=edit]', function(e){ + e.stopPropagation(); + var $bookmark = $(e.target).closest('.bookmark'); + showBookmarkEditDialog($bookmark.data()); + }); + + $container.on('click', '.action[data-action=delete]', function(e){ + e.stopPropagation(); + var $bookmark = $(e.target).closest('.bookmark'); + deleteBookmark($bookmark.data()); + }); + var $bookmarkButton = $('#openwebrx-panel-receiver .openwebrx-bookmark-button'); if (typeof(Storage) !== 'undefined') { $bookmarkButton.show(); @@ -1401,20 +1413,28 @@ function init_bookmarks() { $dialog.hide(); }); $dialog.find('.openwebrx-button[data-action=submit]').click(function(){ - storeNewBookmark(); + storeBookmark(); }); } -function showBookmarkEditDialog() { +function showBookmarkEditDialog(bookmark) { var $dialog = $("#openwebrx-dialog-bookmark"); var $form = $dialog.find("form"); - $form.find("#name").val(""); - $form.find("#frequency").val(center_freq + demodulators[0].offset_frequency); - $form.find("#modulation").val(demodulators[0].subtype); + if (!bookmark) { + bookmark = { + name: "", + frequency: center_freq + demodulators[0].offset_frequency, + modulation: demodulators[0].subtype + } + } + ['name', 'frequency', 'modulation'].forEach(function(key){ + $form.find('#' + key).val(bookmark[key]); + }); + $dialog.data('id', bookmark.id); $dialog.show(); } -function storeNewBookmark() { +function storeBookmark() { var $dialog = $("#openwebrx-dialog-bookmark"); var bookmark = {}; var valid = true; @@ -1428,13 +1448,34 @@ function storeNewBookmark() { return; } bookmark.frequency = Number(bookmark.frequency); + var bookmarks = getLocalBookmarks(); + + bookmark.id = $dialog.data('id'); + if (!bookmark.id) { + if (bookmarks.length) { + bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; })); + } else { + bookmark.id = 1; + } + } + + bookmarks = bookmarks.filter(function(b) { return b.id != bookmark.id; }); bookmarks.push(bookmark); + setLocalBookmarks(bookmarks); loadLocalBookmarks(); $dialog.hide(); } +function deleteBookmark(data) { + if (data.id) data = data.id; + var bookmarks = getLocalBookmarks(); + bookmarks = bookmarks.filter(function(b) { return b.id != data; }); + setLocalBookmarks(bookmarks); + loadLocalBookmarks(); +} + function getLocalBookmarks(){ return JSON.parse(window.localStorage.getItem("bookmarks")) || []; } From 5a3e2a25758a39a1f3cc9ce2e1e5e170c5b4660e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 29 Sep 2019 15:38:50 +0200 Subject: [PATCH 0447/2616] auto-focus; submit on enter; --- htdocs/openwebrx.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index f5bd86757..9309c6b79 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1415,6 +1415,10 @@ function init_bookmarks() { $dialog.find('.openwebrx-button[data-action=submit]').click(function(){ storeBookmark(); }); + $dialog.find('form').on('submit', function(e){ + e.preventDefault(); + storeBookmark(); + }); } function showBookmarkEditDialog(bookmark) { @@ -1432,6 +1436,7 @@ function showBookmarkEditDialog(bookmark) { }); $dialog.data('id', bookmark.id); $dialog.show(); + $dialog.find('#name').focus(); } function storeBookmark() { From fac19e09cd0f9a7ab9bdb0d2a52b3b9a5105ccbb Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 29 Sep 2019 15:48:24 +0200 Subject: [PATCH 0448/2616] scale background (it's stretching now, doesn't look too bad though) --- htdocs/css/openwebrx.css | 10 +++++++--- htdocs/index.html | 8 +++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 619ebb30d..127e8f4ce 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -153,17 +153,21 @@ input[type=range]:focus::-ms-fill-upper #openwebrx-scale-container { height: 47px; - background-image: url("../gfx/openwebrx-scale-background.png"); - background-repeat: repeat-x; overflow: hidden; z-index:1000; position: relative; } +#openwebrx-frequency-container { + background-image: url("../gfx/openwebrx-scale-background.png"); + background-repeat: repeat-x; + background-size: cover; + background-color: #444; +} + #openwebrx-bookmarks-container { height: 25px; - background-color: #444; position: relative; z-index: 1000; } diff --git a/htdocs/index.html b/htdocs/index.html index 11bb2b18a..7b39ad6ac 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -35,9 +35,11 @@
    ${header}
    -
    -
    - +
    +
    +
    + +
    From b662c547f33a37e03686b2c7da1aa12c228409e3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 29 Sep 2019 16:02:37 +0200 Subject: [PATCH 0449/2616] update readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 463fb4581..6efaf597e 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,12 @@ It has the following features: - [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN) - [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9) +**News (2019-09-29 by DD5FJK)** +- One of the most-requested features is finally coming to OpenWebRX: Bookmarks (sometimes also referred to as labels). There's two kinds of bookmarks available: + - Serverside bookmarks that are set up by the receiver administrator. Check the file `bookmarks.json` for examples! + - Clientside bookmarks which every user can store for themselves. They are stored in the browser's localStorage. +- Some more bugs in the websocket handling have been fixed. + **News (2019-09-25 by DD5JFK)** - Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is now possible. Please have a look at the configuration on how to set it up. - Websocket communication has been overhauled in large parts. It should now be more reliable, and failing connections should now have no impact on other users. From 1c72e9ac505d287f6f56cb0db700631b987b1051 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 29 Sep 2019 16:21:42 +0200 Subject: [PATCH 0450/2616] switch rf_gain to 0 for sdrplay (4 is not supported on RSP1) --- config_webrx.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index b0316cb52..56a748813 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -138,7 +138,7 @@ "20m": { "name": "20m", "center_freq": 14150000, - "rf_gain": 4, + "rf_gain": 0, "samp_rate": 500000, "start_freq": 14070000, "start_mod": "usb", @@ -147,7 +147,7 @@ "30m": { "name": "30m", "center_freq": 10125000, - "rf_gain": 4, + "rf_gain": 0, "samp_rate": 250000, "start_freq": 10142000, "start_mod": "usb", @@ -155,7 +155,7 @@ "40m": { "name": "40m", "center_freq": 7100000, - "rf_gain": 4, + "rf_gain": 0, "samp_rate": 500000, "start_freq": 7070000, "start_mod": "usb", @@ -164,7 +164,7 @@ "80m": { "name": "80m", "center_freq": 3650000, - "rf_gain": 4, + "rf_gain": 0, "samp_rate": 500000, "start_freq": 3570000, "start_mod": "usb", @@ -173,7 +173,7 @@ "49m": { "name": "49m Broadcast", "center_freq": 6000000, - "rf_gain": 4, + "rf_gain": 0, "samp_rate": 500000, "start_freq": 6070000, "start_mod": "am", From 5903ae1603d6a63840d118bcb95c6000874fdb80 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 29 Sep 2019 17:16:08 +0200 Subject: [PATCH 0451/2616] prevent the meta panel from disappearing --- htdocs/openwebrx.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 9309c6b79..ac319d2b7 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -620,8 +620,7 @@ function demodulator_analog_replace(subtype, for_digital) } demodulator_add(new demodulator_default_analog(temp_offset,subtype)); demodulator_buttons_update(); - hide_digitalvoice_panels(); - toggle_panel("openwebrx-panel-metadata-" + subtype, true); + update_digitalvoice_panels("openwebrx-panel-metadata-" + subtype); } function demodulator_set_offset_frequency(which,to_what) @@ -1689,9 +1688,9 @@ function update_packet_panel(msg) { $b.scrollTop($b[0].scrollHeight); } -function hide_digitalvoice_panels() { +function update_digitalvoice_panels(showing) { $(".openwebrx-meta-panel").each(function(_, p){ - toggle_panel(p.id, false); + toggle_panel(p.id, p.id == showing); }); clear_metadata(); } @@ -2977,7 +2976,6 @@ function demodulator_digital_replace(subtype) case "ft4": secondary_demod_start(subtype); demodulator_analog_replace('usb', true); - demodulator_buttons_update(); break; case "wspr": secondary_demod_start(subtype); @@ -2986,14 +2984,13 @@ function demodulator_digital_replace(subtype) demodulators[0].low_cut = 1350; demodulators[0].high_cut = 1650; demodulators[0].set(); - demodulator_buttons_update(); break; case "packet": secondary_demod_start(subtype); demodulator_analog_replace('nfm', true); - demodulator_buttons_update(); break; } + demodulator_buttons_update(); $('#openwebrx-panel-digimodes').attr('data-mode', subtype); toggle_panel("openwebrx-panel-digimodes", true); toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(subtype) >= 0); From 774b71f8f09a10fbea3c2b80c7ba6454954f5a65 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 29 Sep 2019 20:42:31 +0200 Subject: [PATCH 0452/2616] update latest image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6efaf597e..483a626ee 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ It has the following features: ### Raspberry Pi SD Card Images -Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-09-26-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+. +Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-09-29-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+. This is based off the Raspbian Lite distribution, so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply. From 630a542ed69099c325198d457988b3ad95a9b99e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 2 Oct 2019 11:28:41 +0200 Subject: [PATCH 0453/2616] better websocket header handling --- owrx/websocket.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/owrx/websocket.py b/owrx/websocket.py index 020cc23c2..ba6f47a09 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -42,17 +42,16 @@ def __init__(self, handler, messageHandler): (self.interruptPipeRecv, self.interruptPipeSend) = Pipe(duplex=False) self.open = True self.sendLock = threading.Lock() - my_headers = self.handler.headers.items() - my_header_keys = list(map(lambda x: x[0], my_headers)) - h_key_exists = lambda x: my_header_keys.count(x) - h_value = lambda x: my_headers[my_header_keys.index(x)][1] - if ( - (not h_key_exists("Upgrade")) - or not (h_value("Upgrade") == "websocket") - or (not h_key_exists("Sec-WebSocket-Key")) - ): - raise WebSocketException - ws_key = h_value("Sec-WebSocket-Key") + + headers = {key.lower(): value for key, value in self.handler.headers.items()} + if not "upgrade" in headers: + raise WebSocketException("Upgrade header not found") + if headers["upgrade"].lower() != "websocket": + raise WebSocketException("Upgrade header does not contain expected value") + if not "sec-websocket-key" in headers: + raise WebSocketException("Websocket key not provided") + + ws_key = headers["sec-websocket-key"] shakey = hashlib.sha1() shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key=ws_key).encode()) ws_key_toreturn = base64.b64encode(shakey.digest()) From 08e95200198611cb75f11c8aade77966ef0a206a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 2 Oct 2019 18:13:33 +0200 Subject: [PATCH 0454/2616] reduce png size by using indexed colors --- htdocs/gfx/openwebrx-avatar.png | Bin 12935 -> 2763 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/htdocs/gfx/openwebrx-avatar.png b/htdocs/gfx/openwebrx-avatar.png index 2ae083fc25a91d78dfa5d64e9123fdae530b6306..fc2052912003871386319738a1a8975788444c8c 100644 GIT binary patch literal 2763 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 JDYx>V{|mNJ(~JNB literal 12935 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 From eb0f54e79d8ab14704d8a4e866d6ceb244f0be6a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 2 Oct 2019 23:48:13 +0200 Subject: [PATCH 0455/2616] reset status values properly on reconnect --- htdocs/openwebrx.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index ac319d2b7..4eea348fd 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1170,6 +1170,7 @@ function audio_calculate_resampling(targetRate) debug_ws_data_received=0; +debug_ws_time_start = 0; max_clients_num=0; client_num = 0; @@ -1749,6 +1750,8 @@ function on_ws_opened() { ws.send("SERVER DE CLIENT client=openwebrx.js type=receiver"); divlog("WebSocket opened to "+ws_url); + debug_ws_data_received = 0; + debug_ws_time_start = new Date().getTime(); reconnect_timeout = false; } @@ -2087,11 +2090,11 @@ function audio_init() audio_debug_time_start=(new Date()).getTime(); audio_debug_time_last_start=audio_debug_time_start; + audio_buffer_current_count_debug = 0; //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js audio_initialized=1; // only tell on_ws_recv() not to call it again - //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor createjsnode_function = (audio_context.createJavaScriptNode == undefined)?audio_context.createScriptProcessor.bind(audio_context):audio_context.createJavaScriptNode.bind(audio_context); audio_node = createjsnode_function(audio_buffer_size, 0, 1); @@ -2749,11 +2752,14 @@ function debug_audio() progressbar_set(e("openwebrx-bar-audio-speed"),audio_speed_value/500000,"Audio stream ["+(audio_speed_value/1000).toFixed(0)+" kbps]",false); var audio_output_value=(audio_buffer_current_count_debug*audio_buffer_size)/audio_debug_time_taken; - progressbar_set(e("openwebrx-bar-audio-output"),audio_output_value/55000,"Audio output ["+(audio_output_value/1000).toFixed(1)+" ksps]",audio_output_value>55000||audio_output_value<10000); + var audio_max_rate = audio_context.sampleRate * 1.25; + var audio_min_rate = audio_context.sampleRate * .25; + progressbar_set(e("openwebrx-bar-audio-output"),audio_output_value/audio_max_rate,"Audio output ["+(audio_output_value/1000).toFixed(1)+" ksps]",audio_output_value>audio_max_rate||audio_output_value Date: Thu, 3 Oct 2019 00:14:05 +0200 Subject: [PATCH 0456/2616] restart dsp chain on output_rate change, fixes #8 --- csdr.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/csdr.py b/csdr.py index 90232693d..36f96d06b 100644 --- a/csdr.py +++ b/csdr.py @@ -455,8 +455,11 @@ def isPacket(self, demodulator=None): return demodulator == "packet" def set_output_rate(self, output_rate): + if self.output_rate == output_rate: + return self.output_rate = output_rate self.calculate_decimation() + self.restart() def set_demodulator(self, demodulator): if self.demodulator == demodulator: From 6ae934e461137cb60bb0b39d3f501a219eee4949 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 3 Oct 2019 00:36:26 +0200 Subject: [PATCH 0457/2616] initialize demodulator with configured start values, fixes #9 --- htdocs/openwebrx.js | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 4eea348fd..6534d5a81 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1197,7 +1197,7 @@ function on_ws_recv(evt) waterfallColorsDefault(); window.starting_mod = config.start_mod - window.starting_offset_frequency = config.start_offset_frequency; + window.starting_offset_frequency = config.start_offset_freq; window.audio_buffering_fill_to = config.client_audio_buffer_size; bandwidth = config.samp_rate; center_freq = config.center_freq + config.lfo_offset; @@ -1217,7 +1217,13 @@ function on_ws_recv(evt) audio_preinit(); loadLocalBookmarks(); - if (audio_allowed && !audio_initialized) audio_init(); + if (audio_allowed) { + if (audio_initialized) { + initialize_demodulator(); + } else { + audio_init(); + } + } waterfall_clear(); break; case "secondary_config": @@ -2108,18 +2114,7 @@ function audio_init() window.setInterval(audio_flush,audio_flush_interval_ms); divlog('Web Audio API succesfully initialized, sample rate: '+audio_context.sampleRate.toString()+ " sps"); - /*audio_source=audio_context.createBufferSource(); - audio_buffer = audio_context.createBuffer(xhr.response, false); - audio_source.buffer = buffer; - audio_source.noteOn(0);*/ - demodulator_analog_replace(starting_mod); - if(starting_offset_frequency) - { - demodulators[0].offset_frequency = starting_offset_frequency; - e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",center_freq+starting_offset_frequency,1e6,4); - demodulators[0].set(); - mkscale(); - } + initialize_demodulator(); //hide log panel in a second (if user has not hidden it yet) window.setTimeout(function(){ @@ -2133,6 +2128,17 @@ function audio_init() } +function initialize_demodulator() { + demodulator_analog_replace(starting_mod); + if(starting_offset_frequency) + { + demodulators[0].offset_frequency = starting_offset_frequency; + e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",center_freq+starting_offset_frequency,1e6,4); + demodulators[0].set(); + mkscale(); + } +} + var reconnect_timeout = false; function on_ws_closed() From 2025ccb3662fabeb915c072d51eb93b246c23351 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 3 Oct 2019 00:58:27 +0200 Subject: [PATCH 0458/2616] catch more generic OSError --- owrx/websocket.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/owrx/websocket.py b/owrx/websocket.py index ba6f47a09..e80ebdf70 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -189,8 +189,8 @@ def protected_read(num): except IncompleteRead: logger.warning("incomplete read on websocket; closing connection") self.open = False - except TimeoutError: - logger.warning("websocket timed out; closing connection") + except OSError: + logger.exception("OSError while reading data; closing connection") self.open = False logger.debug("websocket loop ended; shutting down") From 3e8e2182a82ab372090a7634c4c4111b9ae92863 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 3 Oct 2019 17:24:28 +0200 Subject: [PATCH 0459/2616] fix many, many problems with the frontend frequency displays, scroll and drag handling, closes #13 --- htdocs/css/openwebrx.css | 20 +------- htdocs/index.html | 1 - htdocs/openwebrx.js | 101 ++++++++++++--------------------------- 3 files changed, 32 insertions(+), 90 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 127e8f4ce..a18e63218 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -245,13 +245,9 @@ input[type=range]:focus::-ms-fill-upper #webrx-canvas-container { - /*background-image:url('../gfx/openwebrx-blank-background-1.jpg');*/ position: relative; height: 2000px; - overflow-y: scroll; - overflow-x: hidden; - /*background-color: #646464;*/ - /*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/ + overflow: hidden; background-image: url('../gfx/openwebrx-background-cool-blue.png'); background-repeat: no-repeat; background-color: #1e5f7f; @@ -273,20 +269,6 @@ input[type=range]:focus::-ms-fill-upper display: none; } -#openwebrx-phantom-canvas -{ - position: absolute; - width: 0px; - height: 0px; -} - -/*#openwebrx-canvas-gradient-background -{ - overflow: hidden; - width: 100%; - height: 396px; -}*/ - #openwebrx-log-scroll { /*overflow-y:auto;*/ diff --git a/htdocs/index.html b/htdocs/index.html index 7b39ad6ac..e098ec17e 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -43,7 +43,6 @@
    -
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 6534d5a81..539ce6fb3 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -497,6 +497,8 @@ function demodulator_default_analog(offset_frequency,subtype) this.color,center_freq+this.parent.offset_frequency); }; + this.envelope.dragged_range = demodulator.draggable_ranges.none; + // event handlers this.envelope.drag_start=function(x, key_modifiers) { @@ -649,6 +651,8 @@ function scale_setup() scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false); scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false); resize_scale(); + var frequency_container = e("openwebrx-frequency-container"); + frequency_container.addEventListener("mousemove", frequency_container_mousemove, false); } var scale_canvas_drag_params={ @@ -698,14 +702,18 @@ function scale_canvas_mousemove(evt) } +function frequency_container_mousemove(evt) { + var frequency = center_freq + scale_offset_freq_from_px(evt.pageX); + e("webrx-mouse-freq").innerHTML = format_frequency("{x} MHz", frequency, 1e6, 4); +} + function scale_canvas_end_drag(x) { - canvas_container.style.cursor="default"; + scale_canvas.style.cursor="default"; scale_canvas_drag_params.drag=false; scale_canvas_drag_params.mouse_down=false; var event_handled=false; for (var i=0;imaxX) realX=maxX; - if(realX<0) realX=0; - element.style.left=realX.toString()+"px";*/ + relativeX = get_relative_x(evt); if(canvas_mouse_down) { if(!canvas_drag&&Math.abs(evt.pageX-canvas_drag_start_x)>canvas_drag_min_delta) @@ -990,13 +980,11 @@ function canvas_mousemove(evt) { var deltaX=canvas_drag_last_x-evt.pageX; var deltaY=canvas_drag_last_y-evt.pageY; - //zoom_center_where=zoom_center_where_calc(evt.pageX); var dpx=range.hps*deltaX; if( !(zoom_center_rel+dpx>(bandwidth/2-canvas_container.clientWidth*(1-zoom_center_where)*range.hps)) && !(zoom_center_rel+dpx<-bandwidth/2+canvas_container.clientWidth*zoom_center_where*range.hps) ) { zoom_center_rel+=dpx; } -// -((canvases_new_width*(0.5+zoom_center_rel/bandwidth))-(winsize*zoom_center_where)); resize_canvases(false); canvas_drag_last_x=evt.pageX; canvas_drag_last_y=evt.pageY; @@ -1007,22 +995,18 @@ function canvas_mousemove(evt) else e("webrx-mouse-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4); } -function canvas_container_mouseout(evt) +function canvas_container_mouseleave(evt) { canvas_end_drag(); } -//function body_mouseup() { canvas_end_drag(); console.log("body_mouseup"); } -//function window_mouseout() { canvas_end_drag(); console.log("document_mouseout"); } - function canvas_mouseup(evt) { if(!waterfall_setup_done) return; - relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; + relativeX = get_relative_x(evt); if(!canvas_drag) { - //ws.send("SET offset_freq="+canvas_get_freq_offset(relativeX).toString()); demodulator_set_offset_frequency(0, canvas_get_freq_offset(relativeX)); } else @@ -1044,19 +1028,20 @@ function zoom_center_where_calc(screenposX) return screenposX/canvas_container.clientWidth; } +function get_relative_x(evt) { + var relativeX = (evt.offsetX)?evt.offsetX:evt.layerX; + if ($(evt.target).closest(canvas_container).length) return relativeX; + // compensate for the frequency scale, since that is not resized by the browser. + return relativeX - zoom_offset_px; +} + function canvas_mousewheel(evt) { if(!waterfall_setup_done) return; - //var i=Math.abs(evt.wheelDelta); - //var dir=(i/evt.wheelDelta)<0; - //console.log(evt); - var relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; + var relativeX = get_relative_x(evt); var dir=(evt.deltaY/Math.abs(evt.deltaY))>0; - //console.log(dir); - //i/=120; - /*while (i--)*/ zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX)); + zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX)); evt.preventDefault(); - //evt.returnValue = false; //disable scrollbar move } @@ -2235,12 +2220,6 @@ function add_canvas() new_canvas.style.top=new_canvas.openwebrx_top.toString()+"px"; canvas_context = new_canvas.getContext("2d"); canvas_container.appendChild(new_canvas); - new_canvas.addEventListener("mouseover", canvas_mouseover, false); - new_canvas.addEventListener("mouseout", canvas_mouseout, false); - new_canvas.addEventListener("mousemove", canvas_mousemove, false); - new_canvas.addEventListener("mouseup", canvas_mouseup, false); - new_canvas.addEventListener("mousedown", canvas_mousedown, false); - new_canvas.addEventListener("wheel",canvas_mousewheel, false); canvases.push(new_canvas); while (canvas_container && canvas_container.clientHeight + canvas_default_height * 2 < canvases.length * canvas_default_height) { var c = canvases.shift(); @@ -2254,17 +2233,13 @@ function init_canvas_container() { canvas_container=e("webrx-canvas-container"); mathbox_container=e("openwebrx-mathbox-container"); - canvas_container.addEventListener("mouseout",canvas_container_mouseout, false); - //window.addEventListener("mouseout",window_mouseout,false); - //document.body.addEventListener("mouseup",body_mouseup,false); - canvas_phantom=e("openwebrx-phantom-canvas"); - canvas_phantom.addEventListener("mouseover", canvas_mouseover, false); - canvas_phantom.addEventListener("mouseout", canvas_mouseout, false); - canvas_phantom.addEventListener("mousemove", canvas_mousemove, false); - canvas_phantom.addEventListener("mouseup", canvas_mouseup, false); - canvas_phantom.addEventListener("mousedown", canvas_mousedown, false); - canvas_phantom.addEventListener("wheel",canvas_mousewheel, false); - canvas_phantom.style.width=canvas_container.clientWidth+"px"; + canvas_container.addEventListener("mouseleave",canvas_container_mouseleave, false); + canvas_container.addEventListener("mousemove", canvas_mousemove, false); + canvas_container.addEventListener("mouseup", canvas_mouseup, false); + canvas_container.addEventListener("mousedown", canvas_mousedown, false); + canvas_container.addEventListener("wheel",canvas_mousewheel, false); + var frequency_container = e("openwebrx-frequency-container"); + frequency_container.addEventListener("wheel",canvas_mousewheel, false); add_canvas(); } @@ -2277,18 +2252,6 @@ function shift_canvases() p.style.top=(p.openwebrx_top++).toString()+"px"; }); canvas_maxshift++; - if(canvas_container.clientHeight>canvas_maxshift) - { - canvas_phantom.style.top=canvas_maxshift.toString()+"px"; - canvas_phantom.style.height=(canvas_container.clientHeight-canvas_maxshift).toString()+"px"; - canvas_phantom.style.display="block"; - } - else - canvas_phantom.style.display="none"; - - - //canvas_container.style.height=(((canvases.length-1)*canvas_default_height)+(canvas_default_height-canvas_actual_line)).toString()+"px"; - //canvas_container.style.height="100%"; } function resize_canvases(zoom) @@ -2303,8 +2266,6 @@ function resize_canvases(zoom) p.style.width=new_width; p.style.left=zoom_value; }); - canvas_phantom.style.width=new_width; - canvas_phantom.style.left=zoom_value; } function waterfall_init() @@ -3069,7 +3030,7 @@ function secondary_demod_init() .mouseup(secondary_demod_canvas_container_mouseup) .mousedown(secondary_demod_canvas_container_mousedown) .mouseenter(secondary_demod_canvas_container_mousein) - .mouseleave(secondary_demod_canvas_container_mouseout); + .mouseleave(secondary_demod_canvas_container_mouseleave); init_digital_removal_timer(); } @@ -3227,7 +3188,7 @@ function secondary_demod_canvas_container_mousein() $("#openwebrx-digimode-select-channel").css("opacity","0.7"); //.css("border-width", "1px"); } -function secondary_demod_canvas_container_mouseout() +function secondary_demod_canvas_container_mouseleave() { $("#openwebrx-digimode-select-channel").css("opacity","0"); } From e9f9bbb9c0538c08ae97603d6b0a3aec267234d8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 3 Oct 2019 18:10:46 +0200 Subject: [PATCH 0460/2616] replace receiver_qra setting with locator calculation --- htdocs/openwebrx.js | 2 +- owrx/connection.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 539ce6fb3..240374bf5 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1220,7 +1220,7 @@ function on_ws_recv(evt) case "receiver_details": var r = json.value; e('webrx-rx-title').innerHTML = r.receiver_name; - e('webrx-rx-desc').innerHTML = r.receiver_location + ' | Loc: ' + r.receiver_qra + ', ASL: ' + r.receiver_asl + ' m, [maps]'; + e('webrx-rx-desc').innerHTML = r.receiver_location + ' | Loc: ' + r.locator + ', ASL: ' + r.receiver_asl + ' m, [maps]'; e('webrx-rx-photo-title').innerHTML = r.photo_title; e('webrx-rx-photo-desc').innerHTML = r.photo_desc; break; diff --git a/owrx/connection.py b/owrx/connection.py index 55b10ed09..f7f8dc547 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -5,6 +5,7 @@ from owrx.bands import Bandplan from owrx.bookmarks import Bookmarks from owrx.map import Map +from owrx.locator import Locator from multiprocessing import Queue import json import threading @@ -89,13 +90,13 @@ def __init__(self, conn): receiver_keys = [ "receiver_name", "receiver_location", - "receiver_qra", "receiver_asl", "receiver_gps", "photo_title", "photo_desc", ] receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys) + receiver_details["locator"] = Locator.fromCoordinates(receiver_details["receiver_gps"]) self.write_receiver_details(receiver_details) profiles = [ From a16813610276d726a193d8589a660c8709648184 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 3 Oct 2019 18:11:25 +0200 Subject: [PATCH 0461/2616] remove from config, too --- config_webrx.py | 1 - 1 file changed, 1 deletion(-) diff --git a/config_webrx.py b/config_webrx.py index 56a748813..882dfe32a 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -41,7 +41,6 @@ # ==== Web GUI configuration ==== receiver_name = "[Callsign]" receiver_location = "Budapest, Hungary" -receiver_qra = "JN97ML" receiver_asl = 200 receiver_ant = "Longwire" receiver_device = "RTL-SDR" From 71d815cf084b57fca6d922154b0840ae7765c169 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 3 Oct 2019 23:35:36 +0200 Subject: [PATCH 0462/2616] trim config --- config_webrx.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index 882dfe32a..5613d39a7 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -42,11 +42,8 @@ receiver_name = "[Callsign]" receiver_location = "Budapest, Hungary" receiver_asl = 200 -receiver_ant = "Longwire" -receiver_device = "RTL-SDR" receiver_admin = "example@example.com" receiver_gps = (47.000000, 19.000000) -photo_height = 350 photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory" photo_desc = """ You can add your own background photo and receiver information.
    @@ -180,8 +177,6 @@ }, }, }, - # this one is just here to test feature detection - "test": {"type": "test"}, } # ==== Misc settings ==== From 9f90d01dc663795caeaba8640436d4c5bf7b26f0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 3 Oct 2019 23:55:04 +0200 Subject: [PATCH 0463/2616] simplify icon display --- htdocs/css/openwebrx-header.css | 16 ++++------------ htdocs/gfx/openwebrx-avatar-background.png | Bin 459 -> 0 bytes htdocs/include/header.include.html | 4 +--- 3 files changed, 5 insertions(+), 15 deletions(-) delete mode 100644 htdocs/gfx/openwebrx-avatar-background.png diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css index ef0a129bb..34802f6e2 100644 --- a/htdocs/css/openwebrx-header.css +++ b/htdocs/css/openwebrx-header.css @@ -53,21 +53,13 @@ padding: 15px; } -#webrx-rx-avatar-background +#webrx-rx-avatar { - cursor:pointer; - background-image: url(../gfx/openwebrx-avatar-background.png); - background-origin: content-box; - background-repeat: no-repeat; + background-color: rgba(154, 154, 154, .5); + border-radius: 7px; float: left; - width: 54px; - height: 54px; - padding: 7px; - box-sizing: content-box; -} + margin: 7px; -#webrx-rx-avatar -{ cursor:pointer; width: 46px; height: 46px; diff --git a/htdocs/gfx/openwebrx-avatar-background.png b/htdocs/gfx/openwebrx-avatar-background.png deleted file mode 100644 index e52cb0b951a27ae1e759d229c802e98b16ae5067..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 459 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SL1|)l2v+e>ZmSQK*5Dp-y;YjHK@;M7UB8wRq z)DD6$W6@l0KcJvwiEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$QVa}?9-c0a zAsLNtZ*9yv6d-Z*V{zV&*h_g$&8)R8PA{!3%KgPpPI+Efva&k;{z8QxY+QOx-kOn8 zhh8Qv*v+|pevSQ(;Ax-LU%Pw0SabdLo^`LqC-qM+Z?E^c)W2l$lH_^M=eq`8xocjQ zxcX|=t!;0$PqwFcuUB|!eR@;>(WF8Ky~NW`i=>ikluuq^Ua#osdwEZIXekGSMDm>I zekO)VJ%;BP4QBhO`K!zi?tjI7^7q@a-=ZpKgZf|2UJ}V*;Afk{@R%WCO4U&Yx5kud zRj&6v(iFdTEUcJeDwo#V9{D)&8gWeD30IcMT{QR1Wheld`JCYa$V|V+Cw5=E_D#Ig zzDcKiQr@%mo>o&+cXBgaF*)m2pP$jYOC~9g;l(!pbnERUZ~k!Sg-mo8@qWJ!81M|9 Lu6{1-oD!M -
    - -
    +
    From 351f63f0b8c972228dd586bd7bf8cb20ca11a2ca Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 4 Oct 2019 00:17:40 +0200 Subject: [PATCH 0464/2616] improve receiver button alignment --- htdocs/css/openwebrx.css | 12 ++++++++++++ htdocs/index.html | 6 ++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index a18e63218..76f3e324b 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -421,12 +421,18 @@ input[type=range]:focus::-ms-fill-upper color: #FFFF50; } +.openwebrx-button:last-child { + margin-right: 0; +} + .openwebrx-demodulator-button { width: 38px; height: 19px; font-size: 12pt; text-align: center; + flex: 1; + margin-right: 5px; } .openwebrx-dial-button svg { @@ -597,6 +603,11 @@ img.openwebrx-mirror-img padding-top: 5px; } +.openwebrx-panel-flex-line { + display: flex; + flex-direction: row; +} + .openwebrx-panel-line:first-child { padding-top: 0; } @@ -707,6 +718,7 @@ img.openwebrx-mirror-img width: 173px; height: 27px; padding-left:3px; + margin-right: 5px; } #openwebrx-sdr-profiles-listbox { diff --git a/htdocs/index.html b/htdocs/index.html index e098ec17e..222fad036 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -60,7 +60,7 @@
    -
    +
    FM
    USB
    CW
    +
    +
    @@ -84,7 +86,7 @@ style="display:none;" data-feature="digital_voice_digiham" onclick="demodulator_analog_replace('ysf');">YSF
    -
    +
    DIG
    -
    - - - - - -
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 9ac102b38..600602123 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1259,9 +1259,7 @@ function on_ws_recv(evt) update_wsjt_panel(json.value); break; case "dial_frequencies": - dial_frequencies = json.value; - update_dial_button(); - var as_bookmarks = dial_frequencies.map(function(d){ + var as_bookmarks = json.value.map(function(d){ return { name: d.mode.toUpperCase(), digital_modulation: d.mode, @@ -1341,27 +1339,6 @@ function on_ws_recv(evt) } } -var dial_frequencies = []; - -function find_dial_frequencies() { - var sdm = $("#openwebrx-secondary-demod-listbox")[0].value; - return dial_frequencies.filter(function(d){ - return d.mode == sdm; - }); -} - -function update_dial_button() { - var available = find_dial_frequencies(); - $("#openwebrx-secondary-demod-dial-button")[available.length ? "addClass" : "removeClass"]("available"); -} - -function dial_button_click() { - var available = find_dial_frequencies(); - if (!available.length) return; - var frequency = available[0].frequency; - demodulator_set_offset_frequency(0, frequency - center_freq); -} - function update_metadata(meta) { if (meta.protocol) switch (meta.protocol) { case 'DMR': From fce8c294d3ea6a3ff511b64c23fa7a07b9b75a75 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 12 Oct 2019 20:19:34 +0200 Subject: [PATCH 0479/2616] first work at detecting failed sdr devices --- owrx/service.py | 14 ++++++++++++-- owrx/source.py | 47 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/owrx/service.py b/owrx/service.py index 752d1b3f2..6ac7dcfb5 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -119,11 +119,14 @@ def scheduleSelection(self, time=None): if time is not None: delta = time - datetime.utcnow() seconds = delta.total_seconds() - if self.selectionTimer: - self.selectionTimer.cancel() + self.cancelTimer() self.selectionTimer = threading.Timer(seconds, self.selectProfile) self.selectionTimer.start() + def cancelTimer(self): + if self.selectionTimer: + self.selectionTimer.cancel() + def isActive(self): return self.active @@ -133,6 +136,9 @@ def onSdrAvailable(self): def onSdrUnavailable(self): self.scheduleSelection() + def onSdrFailed(self): + self.cancelTimer() + def selectProfile(self): self.active = False if self.source.hasActiveClients(): @@ -183,6 +189,10 @@ def onSdrUnavailable(self): logger.debug("sdr source becoming unavailable; stopping services.") self.stopServices() + def onSdrFailed(self): + logger.debug("sdr source failed; stopping services.") + self.stopServices() + def isSupported(self, mode): return mode in PropertyManager.getSharedInstance()["services_decoders"] diff --git a/owrx/source.py b/owrx/source.py index 4b7fe5c28..f97823f22 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -75,10 +75,11 @@ def sdrTypeAvailable(value): @staticmethod def getSource(id=None): SdrService.loadProps() + sources = SdrService.getSources() if id is None: # TODO: configure default sdr in config? right now it will pick the first one off the list. - id = list(SdrService.sdrProps.keys())[0] - sources = SdrService.getSources() + id = list(sources.keys())[0] + return sources[id] @staticmethod @@ -90,11 +91,7 @@ def getSources(): className = "".join(x for x in props["type"].title() if x.isalnum()) + "Source" cls = getattr(sys.modules[__name__], className) SdrService.sources[id] = cls(id, props, SdrService.getNextPort()) - return SdrService.sources - - -class SdrSourceException(Exception): - pass + return {key: s for key, s in SdrService.sources.items() if not s.isFailed()} class SdrSource(object): @@ -120,6 +117,7 @@ def restart(name, value): self.spectrumThread = None self.process = None self.modificationLock = threading.Lock() + self.failed = False # override this in subclasses def getCommand(self): @@ -224,17 +222,23 @@ def wait_for_process_to_end(): except: time.sleep(0.1) - self.modificationLock.release() - if not available: - raise SdrSourceException("rtl source failed to start up") + self.failed = True + + self.modificationLock.release() for c in self.clients: - c.onSdrAvailable() + if self.failed: + c.onSdrFailed() + else: + c.onSdrAvailable() def isAvailable(self): return self.monitor is not None + def isFailed(self): + return self.failed + def stop(self): for c in self.clients: c.onSdrUnavailable() @@ -305,6 +309,9 @@ def __init__(self, props, port, sdr): super().__init__(None, props, port) def start(self): + if self.isFailed(): + return + self.modificationLock.acquire() if self.monitor: self.modificationLock.release() @@ -364,13 +371,16 @@ def wait_for_process_to_end(): except: time.sleep(0.1) - self.modificationLock.release() - if not available: - raise SdrSourceException("resampler source failed to start up") + self.failed = True + + self.modificationLock.release() for c in self.clients: - c.onSdrAvailable() + if self.failed: + c.onSdrFailed() + else: + c.onSdrAvailable() def activateProfile(self, profile_id=None): pass @@ -504,6 +514,9 @@ def onSdrAvailable(self): def onSdrUnavailable(self): self.dsp.stop() + def onSdrFailed(self): + self.dsp.stop() + class DspManager(csdr.output): def __init__(self, handler, sdrSource): @@ -638,6 +651,10 @@ def onSdrUnavailable(self): logger.debug("received onSdrUnavailable, shutting down DspSource") self.dsp.stop() + def onSdrFailed(self): + logger.debug("received onSdrFailed, shutting down DspSource") + self.dsp.stop() + class CpuUsageThread(threading.Thread): sharedInstance = None From 70d8fe82b340929c31dab378a1d1c4b4a36894d1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 12 Oct 2019 20:46:32 +0200 Subject: [PATCH 0480/2616] send failure message to client --- owrx/connection.py | 3 +++ owrx/source.py | 1 + 2 files changed, 4 insertions(+) diff --git a/owrx/connection.py b/owrx/connection.py index cea86ce7d..da6365d1a 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -266,6 +266,9 @@ def write_bookmarks(self, bookmarks): def write_aprs_data(self, data): self.send({"type": "aprs_data", "value": data}) + def write_sdr_error(self, message): + self.send({"type": "sdr_error", "value": message}) + class MapConnection(Client): def __init__(self, conn): diff --git a/owrx/source.py b/owrx/source.py index f97823f22..80e125c69 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -654,6 +654,7 @@ def onSdrUnavailable(self): def onSdrFailed(self): logger.debug("received onSdrFailed, shutting down DspSource") self.dsp.stop() + self.handler.write_sdr_error("sdr failed") class CpuUsageThread(threading.Thread): From 5b61f8c7a35406651483b633bb482c1cb4f52f8e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 12 Oct 2019 20:48:36 +0200 Subject: [PATCH 0481/2616] show message in log --- htdocs/openwebrx.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 600602123..a0f91297b 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1274,6 +1274,9 @@ function on_ws_recv(evt) case "bookmarks": bookmarks.replace_bookmarks(json.value, "server"); break; + case "sdr_error": + divlog(json.value, true); + break; default: console.warn('received message of unknown type: ' + json.type); } From ea67340cabbf2316b6e99dfd0c91d1ebf4bbdc3a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 13 Oct 2019 14:17:32 +0200 Subject: [PATCH 0482/2616] display message when sdr unavailable --- owrx/connection.py | 8 ++++++++ owrx/source.py | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/owrx/connection.py b/owrx/connection.py index da6365d1a..ab0cabb8b 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -142,6 +142,11 @@ def handleTextMessage(self, conn, message): def setSdr(self, id=None): next = SdrService.getSource(id) + + if next is None: + self.handleSdrFailure("sdr device failed") + return + if next == self.sdr: return @@ -180,6 +185,9 @@ def sendConfig(key, value): self.sdr.addSpectrumClient(self) + def handleSdrFailure(self, message): + self.write_sdr_error(message) + def startDsp(self): if self.dsp is None: self.dsp = DspManager(self, self.sdr) diff --git a/owrx/source.py b/owrx/source.py index 80e125c69..2d576ce18 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -76,10 +76,14 @@ def sdrTypeAvailable(value): def getSource(id=None): SdrService.loadProps() sources = SdrService.getSources() + if not sources: + return None if id is None: # TODO: configure default sdr in config? right now it will pick the first one off the list. id = list(sources.keys())[0] + if not id in sources: + return None return sources[id] @staticmethod @@ -654,7 +658,7 @@ def onSdrUnavailable(self): def onSdrFailed(self): logger.debug("received onSdrFailed, shutting down DspSource") self.dsp.stop() - self.handler.write_sdr_error("sdr failed") + self.handler.handleSdrFailure("sdr device failed") class CpuUsageThread(threading.Thread): From eda556ef0308ebb602241f15c5029e70008a4d77 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 13 Oct 2019 17:51:00 +0200 Subject: [PATCH 0483/2616] prevent start-up of services if requirements are not fulfilled. closes #4 --- owrx/service.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/owrx/service.py b/owrx/service.py index 6ac7dcfb5..7b4e0e09b 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -8,6 +8,7 @@ from owrx.aprs import AprsParser from owrx.config import PropertyManager from owrx.source import Resampler +from owrx.feature import FeatureDetector import logging @@ -194,7 +195,24 @@ def onSdrFailed(self): self.stopServices() def isSupported(self, mode): - return mode in PropertyManager.getSharedInstance()["services_decoders"] + # TODO this should be in a more central place (the frontend also needs this) + requirements = { + 'ft8': 'wsjt-x', + 'ft4': 'wsjt-x', + 'jt65': 'wsjt-x', + 'jt9': 'wsjt-x', + 'wspr': 'wsjt-x', + 'packet': 'packet', + } + fd = FeatureDetector() + + # this looks overly complicated... but i'd like modes with no requirements to be always available without + # being listed in the hash above + unavailable = [mode for mode, req in requirements.items() if not fd.is_available(req)] + configured = PropertyManager.getSharedInstance()["services_decoders"] + available = [mode for mode in configured if mode not in unavailable] + + return mode in available def stopServices(self): with self.lock: From f45857f79b67190deea554e74f5c562ee0f52e20 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 13 Oct 2019 18:25:32 +0200 Subject: [PATCH 0484/2616] don't use the resampler if the optimization says so --- owrx/service.py | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/owrx/service.py b/owrx/service.py index 7b4e0e09b..92db9c145 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -266,23 +266,28 @@ def updateServices(self): with self.lock: self.services = [] - for group in self.optimizeResampling(dials, sr): - frequencies = sorted([f["frequency"] for f in group]) - min = frequencies[0] - max = frequencies[-1] - cf = (min + max) / 2 - bw = max - min - logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw)) - resampler_props = PropertyManager() - resampler_props["center_freq"] = cf - # TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths - resampler_props["samp_rate"] = bw + 24000 - resampler = Resampler(resampler_props, self.getAvailablePort(), self.source) - resampler.start() - self.services.append(resampler) - - for dial in group: - self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler)) + groups = self.optimizeResampling(dials, sr) + if groups is None: + for dial in dials: + self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source)) + else: + for group in groups: + frequencies = sorted([f["frequency"] for f in group]) + min = frequencies[0] + max = frequencies[-1] + cf = (min + max) / 2 + bw = max - min + logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw)) + resampler_props = PropertyManager() + resampler_props["center_freq"] = cf + # TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths + resampler_props["samp_rate"] = bw + 24000 + resampler = Resampler(resampler_props, self.getAvailablePort(), self.source) + resampler.start() + self.services.append(resampler) + + for dial in group: + self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler)) def optimizeResampling(self, freqs, bandwidth): freqs = sorted(freqs, key=lambda f: f["frequency"]) @@ -320,7 +325,10 @@ def get_bandwitdh(group): for r in results: logger.debug("splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"])) - return results[0]["groups"] + best = results[0] + if best["num_splits"] is None: + return None + return best["groups"] def setupService(self, mode, frequency, source): logger.debug("setting up service {0} on frequency {1}".format(mode, frequency)) From de179d070df5509f601601c68bd10f5cee7b8041 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 13 Oct 2019 18:28:58 +0200 Subject: [PATCH 0485/2616] this is not theoretical any more --- owrx/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/service.py b/owrx/service.py index 92db9c145..ec86ad890 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -318,7 +318,7 @@ def get_bandwitdh(group): return {"num_splits": num_splits, "total_bandwidth": total_bandwidth, "groups": groups} usages = [calculate_usage(i) for i in range(0, len(freqs))] - # this is simulating no resampling. i haven't seen this as the best result yet + # another possible outcome might be that it's best not to resample at all. this is a special case. usages += [{"num_splits": None, "total_bandwidth": bandwidth * len(freqs), "groups": [freqs]}] results = sorted(usages, key=lambda f: f["total_bandwidth"]) From de90219406aa498f360b2636640740ed5b97e972 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 15 Oct 2019 19:50:24 +0200 Subject: [PATCH 0486/2616] dynamically calculate audio block size (improving latency) --- csdr.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/csdr.py b/csdr.py index 36f96d06b..fd458d50b 100644 --- a/csdr.py +++ b/csdr.py @@ -391,6 +391,15 @@ def set_secondary_fft_size(self, secondary_fft_size): def set_audio_compression(self, what): self.audio_compression = what + def get_audio_bytes_to_read(self): + # desired latency: 5ms + # uncompressed audio has 16 bits = 2 bytes per sample + base = self.output_rate * 0.005 * 2 + # adpcm compresses the bitstream by 4 + if self.audio_compression == "adpcm": + base = base / 4 + return int(base) + def set_fft_compression(self, what): self.fft_compression = what @@ -398,7 +407,7 @@ def get_fft_bytes_to_read(self): if self.fft_compression == "none": return self.fft_size * 4 if self.fft_compression == "adpcm": - return (self.fft_size / 2) + (10 / 2) + return int((self.fft_size / 2) + (10 / 2)) def get_secondary_fft_bytes_to_read(self): if self.fft_compression == "none": @@ -650,7 +659,8 @@ def watch_thread(): self.output.send_output( "audio", partial( - self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256 + self.process.stdout.read, + self.get_fft_bytes_to_read() if self.demodulator == "fft" else self.get_audio_bytes_to_read(), ), ) From 72062c8570a24f8db0ccc03cc5c232f2bd68d74e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 16 Oct 2019 13:17:47 +0200 Subject: [PATCH 0487/2616] let's apply some formatting --- htdocs/openwebrx.js | 4232 +++++++++++++++++++++---------------------- 1 file changed, 2020 insertions(+), 2212 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index a0f91297b..a45b2e5d2 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -21,34 +21,25 @@ */ -is_firefox=navigator.userAgent.indexOf("Firefox")!=-1; +is_firefox = navigator.userAgent.indexOf("Firefox") >= 0; function arrayBufferToString(buf) { - //http://stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers - return String.fromCharCode.apply(null, new Uint8Array(buf)); -} - -function getFirstChars(buf, num) -{ - var u8buf=new Uint8Array(buf); - var output=String(); - num=Math.min(num,u8buf.length); - for(i=0;i=parseInt(wfmax.value)) - { - if(!which) wfmin.value=(parseInt(wfmax.value)-1).toString(); - else wfmax.value=(parseInt(wfmin.value)+1).toString(); - } - waterfall_min_level=parseInt(wfmin.value); - waterfall_max_level=parseInt(wfmax.value); -} -function waterfallColorsDefault() -{ - waterfall_min_level=waterfall_min_level_default; - waterfall_max_level=waterfall_max_level_default; - e("openwebrx-waterfall-color-min").value=waterfall_min_level.toString(); - e("openwebrx-waterfall-color-max").value=waterfall_max_level.toString(); +function zoomInOneStep() { + zoom_set(zoom_level + 1); } -function waterfallColorsAuto() -{ - e("openwebrx-waterfall-color-min").value=(waterfall_measure_minmax_min-waterfall_auto_level_margin[0]).toString(); - e("openwebrx-waterfall-color-max").value=(waterfall_measure_minmax_max+waterfall_auto_level_margin[1]).toString(); - updateWaterfallColors(0); +function zoomOutOneStep() { + zoom_set(zoom_level - 1); } -function setSmeterRelativeValue(value) -{ - if(value<0) value=0; - if(value>1.0) value=1.0; - var bar=e("openwebrx-smeter-bar"); - var outer=e("openwebrx-smeter-outer"); - bar.style.width=(outer.offsetWidth*value).toString()+"px"; - bgRed="linear-gradient(to top, #ff5939 , #961700)"; - bgGreen="linear-gradient(to top, #22ff2f , #008908)"; - bgYellow="linear-gradient(to top, #fff720 , #a49f00)"; - bar.style.background=(value>0.9)?bgRed:((value>0.7)?bgYellow:bgGreen); - //bar.style.backgroundColor=(value>0.9)?"#ff5939":((value>0.7)?"#fff720":"#22ff2f"); -} - -function getLogSmeterValue(value) -{ - return 10*Math.log10(value); +function zoomInTotal() { + zoom_set(zoom_levels.length - 1); } -function getLinearSmeterValue(db_value) -{ - return Math.pow(10,db_value/10); +function zoomOutTotal() { + zoom_set(0); } -function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch_and_smeter_cc` -{ - var logValue=getLogSmeterValue(value); - var lowLevel=waterfall_min_level-20; - var highLevel=waterfall_max_level+20; - var percent=(logValue-lowLevel)/(highLevel-lowLevel); - setSmeterRelativeValue(percent); - e("openwebrx-smeter-db").innerHTML=logValue.toFixed(1)+" dB"; +function setSquelchToAuto() { + e("openwebrx-panel-squelch").value = (getLogSmeterValue(smeter_level) + 10).toString(); + updateSquelch(); } -function typeInAnimation(element,timeout,what,onFinish) +function updateSquelch() { + var sliderValue = parseInt(e("openwebrx-panel-squelch").value); + var outputValue = (sliderValue === parseInt(e("openwebrx-panel-squelch").min)) ? 0 : getLinearSmeterValue(sliderValue); + ws.send(JSON.stringify({"type": "dspcontrol", "params": {"squelch_level": outputValue}})); +} + +function updateWaterfallColors(which) { + wfmax = e("openwebrx-waterfall-color-max"); + wfmin = e("openwebrx-waterfall-color-min"); + if (parseInt(wfmin.value) >= parseInt(wfmax.value)) { + if (!which) wfmin.value = (parseInt(wfmax.value) - 1).toString(); + else wfmax.value = (parseInt(wfmin.value) + 1).toString(); + } + waterfall_min_level = parseInt(wfmin.value); + waterfall_max_level = parseInt(wfmax.value); +} + +function waterfallColorsDefault() { + waterfall_min_level = waterfall_min_level_default; + waterfall_max_level = waterfall_max_level_default; + e("openwebrx-waterfall-color-min").value = waterfall_min_level.toString(); + e("openwebrx-waterfall-color-max").value = waterfall_max_level.toString(); +} + +function waterfallColorsAuto() { + e("openwebrx-waterfall-color-min").value = (waterfall_measure_minmax_min - waterfall_auto_level_margin[0]).toString(); + e("openwebrx-waterfall-color-max").value = (waterfall_measure_minmax_max + waterfall_auto_level_margin[1]).toString(); + updateWaterfallColors(0); +} + +function setSmeterRelativeValue(value) { + if (value < 0) value = 0; + if (value > 1.0) value = 1.0; + var bar = e("openwebrx-smeter-bar"); + var outer = e("openwebrx-smeter-outer"); + bar.style.width = (outer.offsetWidth * value).toString() + "px"; + bgRed = "linear-gradient(to top, #ff5939 , #961700)"; + bgGreen = "linear-gradient(to top, #22ff2f , #008908)"; + bgYellow = "linear-gradient(to top, #fff720 , #a49f00)"; + bar.style.background = (value > 0.9) ? bgRed : ((value > 0.7) ? bgYellow : bgGreen); +} + +function getLogSmeterValue(value) { + return 10 * Math.log10(value); +} + +function getLinearSmeterValue(db_value) { + return Math.pow(10, db_value / 10); +} + +function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch_and_smeter_cc` { - if(!what) { onFinish(); return; } - element.innerHTML+=what[0]; - window.setTimeout( function(){typeInAnimation(element,timeout,what.substring(1),onFinish);}, timeout ); + var logValue = getLogSmeterValue(value); + var lowLevel = waterfall_min_level - 20; + var highLevel = waterfall_max_level + 20; + var percent = (logValue - lowLevel) / (highLevel - lowLevel); + setSmeterRelativeValue(percent); + e("openwebrx-smeter-db").innerHTML = logValue.toFixed(1) + " dB"; } +function typeInAnimation(element, timeout, what, onFinish) { + if (!what) { + onFinish(); + return; + } + element.innerHTML += what[0]; + window.setTimeout(function () { + typeInAnimation(element, timeout, what.substring(1), onFinish); + }, timeout); +} // ======================================================== // ================= ANIMATION ROUTINES ================= // ======================================================== -function animate(object,style_name,unit,from,to,accel,time_ms,fps,to_exec) -{ - //console.log(object.className); - if(typeof to_exec=="undefined") to_exec=0; - object.style[style_name]=from.toString()+unit; - object.anim_i=0; - n_of_iters=time_ms/(1000/fps); - change=(to-from)/(n_of_iters); - if(typeof object.anim_timer!="undefined") { window.clearInterval(object.anim_timer); } - object.anim_timer=window.setInterval( - function(){ - if(object.anim_i++9||unit!="px") new_val=(to+accel*remain); - else {if(Math.abs(remain)<2) new_val=to; - else new_val=to+remain-(remain/Math.abs(remain));} - object.style[style_name]=new_val.toString()+unit; - } - } - else - {object.style[style_name]=to.toString()+unit; window.clearInterval(object.anim_timer); delete object.anim_timer; } - if(to_exec!=0) to_exec(); - },1000/fps); +function animate(object, style_name, unit, from, to, accel, time_ms, fps, to_exec) { + //console.log(object.className); + if (typeof to_exec === "undefined") to_exec = 0; + object.style[style_name] = from.toString() + unit; + object.anim_i = 0; + n_of_iters = time_ms / (1000 / fps); + change = (to - from) / (n_of_iters); + if (typeof object.anim_timer !== "undefined") { + window.clearInterval(object.anim_timer); + } + object.anim_timer = window.setInterval( + function () { + if (object.anim_i++ < n_of_iters) { + if (accel === 1) object.style[style_name] = (parseFloat(object.style[style_name]) + change).toString() + unit; + else { + remain = parseFloat(object.style[style_name]) - to; + if (Math.abs(remain) > 9 || unit !== "px") new_val = (to + accel * remain); + else { + if (Math.abs(remain) < 2) new_val = to; + else new_val = to + remain - (remain / Math.abs(remain)); + } + object.style[style_name] = new_val.toString() + unit; + } + } + else { + object.style[style_name] = to.toString() + unit; + window.clearInterval(object.anim_timer); + delete object.anim_timer; + } + if (to_exec !== 0) to_exec(); + }, 1000 / fps); } -function animate_to(object,style_name,unit,to,accel,time_ms,fps,to_exec) -{ - from=parseFloat(style_value(object,style_name)); - animate(object,style_name,unit,from,to,accel,time_ms,fps,to_exec); +function animate_to(object, style_name, unit, to, accel, time_ms, fps, to_exec) { + from = parseFloat(style_value(object, style_name)); + animate(object, style_name, unit, from, to, accel, time_ms, fps, to_exec); } @@ -279,245 +291,239 @@ function animate_to(object,style_name,unit,to,accel,time_ms,fps,to_exec) // ================ DEMODULATOR ROUTINES ================ // ======================================================== -demodulators=[] +demodulators = []; + +demodulator_color_index = 0; +demodulator_colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"] + +function demodulators_get_next_color() { + if (demodulator_color_index >= demodulator_colors.length) demodulator_color_index = 0; + return (demodulator_colors[demodulator_color_index++]); +} + +function demod_envelope_draw(range, from, to, color, line) { // ____ + // Draws a standard filter envelope like this: _/ \_ + // Parameters are given in offset frequency (Hz). + // Envelope is drawn on the scale canvas. + // A "drag range" object is returned, containing information about the draggable areas of the envelope + // (beginning, ending and the line showing the offset frequency). + if (typeof color === "undefined") color = "#ffff00"; //yellow + env_bounding_line_w = 5; // + env_att_w = 5; // _______ ___env_h2 in px ___|_____ + env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_ + env_h2 = 5; // |||env_att_line_w |_env_lineplus + env_lineplus = 1; // ||env_bounding_line_w + env_line_click_area = 6; + //range=get_visible_freq_range(); + from_px = scale_px_from_freq(from, range); + to_px = scale_px_from_freq(to, range); + if (to_px < from_px) /* swap'em */ { + temp_px = to_px; + to_px = from_px; + from_px = temp_px; + } -demodulator_color_index=0; -demodulator_colors=["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"] -function demodulators_get_next_color() -{ - if(demodulator_color_index>=demodulator_colors.length) demodulator_color_index=0; - return(demodulator_colors[demodulator_color_index++]); -} - -function demod_envelope_draw(range, from, to, color, line) -{ // ____ - // Draws a standard filter envelope like this: _/ \_ - // Parameters are given in offset frequency (Hz). - // Envelope is drawn on the scale canvas. - // A "drag range" object is returned, containing information about the draggable areas of the envelope - // (beginning, ending and the line showing the offset frequency). - if(typeof color == "undefined") color="#ffff00"; //yellow - env_bounding_line_w=5; // - env_att_w=5; // _______ ___env_h2 in px ___|_____ - env_h1=17; // _/| \_ ___env_h1 in px _/ |_ \_ - env_h2=5; // |||env_att_line_w |_env_lineplus - env_lineplus=1; // ||env_bounding_line_w - env_line_click_area=6; - //range=get_visible_freq_range(); - from_px=scale_px_from_freq(from,range); - to_px=scale_px_from_freq(to,range); - if(to_pxwindow.innerWidth)) // out of screen? - { - drag_ranges.beginning={x1:from_px, x2: from_px+env_bounding_line_w+env_att_w}; - drag_ranges.ending={x1:to_px-env_bounding_line_w-env_att_w, x2: to_px}; - drag_ranges.whole_envelope={x1:from_px, x2: to_px}; - drag_ranges.envelope_on_screen=true; - scale_ctx.beginPath(); - scale_ctx.moveTo(from_px,env_h1); - scale_ctx.lineTo(from_px+env_bounding_line_w, env_h1); - scale_ctx.lineTo(from_px+env_bounding_line_w+env_att_w, env_h2); - scale_ctx.lineTo(to_px-env_bounding_line_w-env_att_w, env_h2); - scale_ctx.lineTo(to_px-env_bounding_line_w, env_h1); - scale_ctx.lineTo(to_px, env_h1); - scale_ctx.globalAlpha = 0.3; - scale_ctx.fill(); - scale_ctx.globalAlpha = 1; - scale_ctx.stroke(); - } - if(typeof line != "undefined") // out of screen? - { - line_px=scale_px_from_freq(line,range); - if(!(line_px<0||line_px>window.innerWidth)) - { - drag_ranges.line={x1:line_px-env_line_click_area/2, x2: line_px+env_line_click_area/2}; - drag_ranges.line_on_screen=true; - scale_ctx.moveTo(line_px,env_h1+env_lineplus); - scale_ctx.lineTo(line_px,env_h2-env_lineplus); - scale_ctx.stroke(); - } - } - return drag_ranges; -} - -function demod_envelope_where_clicked(x, drag_ranges, key_modifiers) -{ // Check exactly what the user has clicked based on ranges returned by demod_envelope_draw(). - in_range=function(x,range) { return range.x1<=x&&range.x2>=x; } - dr=demodulator.draggable_ranges; - - if(key_modifiers.shiftKey) - { - //Check first: shift + center drag emulates BFO knob - if(drag_ranges.line_on_screen&&in_range(x,drag_ranges.line)) return dr.bfo; - //Check second: shift + envelope drag emulates PBF knob - if(drag_ranges.envelope_on_screen&&in_range(x,drag_ranges.whole_envelope)) return dr.pbs; - } - if(drag_ranges.envelope_on_screen) - { - // For low and high cut: - if(in_range(x,drag_ranges.beginning)) return dr.beginning; - if(in_range(x,drag_ranges.ending)) return dr.ending; - // Last priority: having clicked anything else on the envelope, without holding the shift key - if(in_range(x,drag_ranges.whole_envelope)) return dr.anything_else; - } - return dr.none; //User doesn't drag the envelope for this demodulator + from_px -= (env_att_w + env_bounding_line_w); + to_px += (env_att_w + env_bounding_line_w); + // do drawing: + scale_ctx.lineWidth = 3; + scale_ctx.strokeStyle = color; + scale_ctx.fillStyle = color; + var drag_ranges = {envelope_on_screen: false, line_on_screen: false}; + if (!(to_px < 0 || from_px > window.innerWidth)) // out of screen? + { + drag_ranges.beginning = {x1: from_px, x2: from_px + env_bounding_line_w + env_att_w}; + drag_ranges.ending = {x1: to_px - env_bounding_line_w - env_att_w, x2: to_px}; + drag_ranges.whole_envelope = {x1: from_px, x2: to_px}; + drag_ranges.envelope_on_screen = true; + scale_ctx.beginPath(); + scale_ctx.moveTo(from_px, env_h1); + scale_ctx.lineTo(from_px + env_bounding_line_w, env_h1); + scale_ctx.lineTo(from_px + env_bounding_line_w + env_att_w, env_h2); + scale_ctx.lineTo(to_px - env_bounding_line_w - env_att_w, env_h2); + scale_ctx.lineTo(to_px - env_bounding_line_w, env_h1); + scale_ctx.lineTo(to_px, env_h1); + scale_ctx.globalAlpha = 0.3; + scale_ctx.fill(); + scale_ctx.globalAlpha = 1; + scale_ctx.stroke(); + } + if (typeof line !== "undefined") // out of screen? + { + line_px = scale_px_from_freq(line, range); + if (!(line_px < 0 || line_px > window.innerWidth)) { + drag_ranges.line = {x1: line_px - env_line_click_area / 2, x2: line_px + env_line_click_area / 2}; + drag_ranges.line_on_screen = true; + scale_ctx.moveTo(line_px, env_h1 + env_lineplus); + scale_ctx.lineTo(line_px, env_h2 - env_lineplus); + scale_ctx.stroke(); + } + } + return drag_ranges; +} + +function demod_envelope_where_clicked(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by demod_envelope_draw(). + in_range = function (x, range) { + return range.x1 <= x && range.x2 >= x; + }; + dr = demodulator.draggable_ranges; + + if (key_modifiers.shiftKey) { + //Check first: shift + center drag emulates BFO knob + if (drag_ranges.line_on_screen && in_range(x, drag_ranges.line)) return dr.bfo; + //Check second: shift + envelope drag emulates PBF knob + if (drag_ranges.envelope_on_screen && in_range(x, drag_ranges.whole_envelope)) return dr.pbs; + } + if (drag_ranges.envelope_on_screen) { + // For low and high cut: + if (in_range(x, drag_ranges.beginning)) return dr.beginning; + if (in_range(x, drag_ranges.ending)) return dr.ending; + // Last priority: having clicked anything else on the envelope, without holding the shift key + if (in_range(x, drag_ranges.whole_envelope)) return dr.anything_else; + } + return dr.none; //User doesn't drag the envelope for this demodulator } //******* class demodulator ******* // this can be used as a base class for ANY demodulator -demodulator=function(offset_frequency) -{ - //console.log("this too"); - this.offset_frequency=offset_frequency; - this.has_audio_output=true; - this.has_text_output=false; - this.envelope={}; - this.color=demodulators_get_next_color(); - this.stop=function(){}; +demodulator = function (offset_frequency) { + //console.log("this too"); + this.offset_frequency = offset_frequency; + this.has_audio_output = true; + this.has_text_output = false; + this.envelope = {}; + this.color = demodulators_get_next_color(); + this.stop = function () { + }; } //ranges on filter envelope that can be dragged: -demodulator.draggable_ranges={none: 0, beginning:1 /*from*/, ending: 2 /*to*/, anything_else: 3, bfo: 4 /*line (while holding shift)*/, pbs: 5 } //to which parameter these correspond in demod_envelope_draw() +demodulator.draggable_ranges = { + none: 0, + beginning: 1 /*from*/, + ending: 2 /*to*/, + anything_else: 3, + bfo: 4 /*line (while holding shift)*/, + pbs: 5 +} //to which parameter these correspond in demod_envelope_draw() //******* class demodulator_default_analog ******* // This can be used as a base for basic audio demodulators. // It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB -demodulator_response_time=50; +demodulator_response_time = 50; + //in ms; if we don't limit the number of SETs sent to the server, audio will underrun (possibly output buffer is cleared on SETs in GNU Radio -function demodulator_default_analog(offset_frequency,subtype) -{ - //console.log("hopefully this happens"); - //http://stackoverflow.com/questions/4152931/javascript-inheritance-call-super-constructor-or-use-prototype-chain - demodulator.call(this,offset_frequency); - this.subtype=subtype; - this.filter={ - min_passband: 100, - high_cut_limit: (audio_server_output_rate/2)-1, //audio_context.sampleRate/2, - low_cut_limit: (-audio_server_output_rate/2)+1 //-audio_context.sampleRate/2 - }; - //Subtypes only define some filter parameters and the mod string sent to server, - //so you may set these parameters in your custom child class. - //Why? As of demodulation is done on the server, difference is mainly on the server side. - this.server_mod=subtype; - if(subtype=="lsb") - { - this.low_cut=-3000; - this.high_cut=-300; - this.server_mod="ssb"; - } - else if(subtype=="usb") - { - this.low_cut=300; - this.high_cut=3000; - this.server_mod="ssb"; - } - else if(subtype=="cw") - { - this.low_cut=700; - this.high_cut=900; - this.server_mod="ssb"; - } - else if(subtype=="nfm") - { - this.low_cut=-4000; - this.high_cut=4000; - } - else if(subtype=="dmr" || subtype=="ysf") - { - this.low_cut=-4000; - this.high_cut=4000; - } - else if(subtype=="dstar" || subtype=="nxdn") - { - this.low_cut=-3250; - this.high_cut=3250; - } - else if(subtype=="am") - { - this.low_cut=-4000; - this.high_cut=4000; - } - - this.wait_for_timer=false; - this.set_after=false; - this.set=function() - { //set() is a wrapper to call doset(), but it ensures that doset won't execute more frequently than demodulator_response_time. - if(!this.wait_for_timer) - { - this.doset(false); - this.set_after=false; - this.wait_for_timer=true; - timeout_this=this; //http://stackoverflow.com/a/2130411 - window.setTimeout(function() { - timeout_this.wait_for_timer=false; - if(timeout_this.set_after) timeout_this.set(); - },demodulator_response_time); - } - else - { - this.set_after=true; - } - } - - this.doset=function(first_time) - { //this function sends demodulator parameters to the server - params = { - "low_cut": this.low_cut, - "high_cut": this.high_cut, - "offset_freq": this.offset_frequency - }; - if (first_time) params.mod = this.server_mod; - ws.send(JSON.stringify({"type":"dspcontrol","params":params})); - } - this.doset(true); //we set parameters on object creation - - //******* envelope object ******* - // for drawing the filter envelope above scale - this.envelope.parent=this; - - this.envelope.draw=function(visible_range) - { - this.visible_range=visible_range; - this.drag_ranges=demod_envelope_draw(range, - center_freq+this.parent.offset_frequency+this.parent.low_cut, - center_freq+this.parent.offset_frequency+this.parent.high_cut, - this.color,center_freq+this.parent.offset_frequency); - }; - - this.envelope.dragged_range = demodulator.draggable_ranges.none; - - // event handlers - this.envelope.drag_start=function(x, key_modifiers) - { - this.key_modifiers=key_modifiers; - this.dragged_range=demod_envelope_where_clicked(x,this.drag_ranges, key_modifiers); - //console.log("dragged_range: "+this.dragged_range.toString()); - this.drag_origin={ - x: x, - low_cut: this.parent.low_cut, - high_cut: this.parent.high_cut, - offset_frequency: this.parent.offset_frequency - }; - return this.dragged_range!=demodulator.draggable_ranges.none; - }; - - this.envelope.drag_move=function(x) - { - dr=demodulator.draggable_ranges; - if(this.dragged_range==dr.none) return false; // we return if user is not dragging (us) at all - freq_change=Math.round(this.visible_range.hps*(x-this.drag_origin.x)); - /*if(this.dragged_range==dr.beginning||this.dragged_range==dr.ending) +function demodulator_default_analog(offset_frequency, subtype) { + //console.log("hopefully this happens"); + //http://stackoverflow.com/questions/4152931/javascript-inheritance-call-super-constructor-or-use-prototype-chain + demodulator.call(this, offset_frequency); + this.subtype = subtype; + this.filter = { + min_passband: 100, + high_cut_limit: (audio_server_output_rate / 2) - 1, //audio_context.sampleRate/2, + low_cut_limit: (-audio_server_output_rate / 2) + 1 //-audio_context.sampleRate/2 + }; + //Subtypes only define some filter parameters and the mod string sent to server, + //so you may set these parameters in your custom child class. + //Why? As of demodulation is done on the server, difference is mainly on the server side. + this.server_mod = subtype; + if (subtype === "lsb") { + this.low_cut = -3000; + this.high_cut = -300; + this.server_mod = "ssb"; + } + else if (subtype === "usb") { + this.low_cut = 300; + this.high_cut = 3000; + this.server_mod = "ssb"; + } + else if (subtype === "cw") { + this.low_cut = 700; + this.high_cut = 900; + this.server_mod = "ssb"; + } + else if (subtype === "nfm") { + this.low_cut = -4000; + this.high_cut = 4000; + } + else if (subtype === "dmr" || subtype === "ysf") { + this.low_cut = -4000; + this.high_cut = 4000; + } + else if (subtype === "dstar" || subtype === "nxdn") { + this.low_cut = -3250; + this.high_cut = 3250; + } + else if (subtype === "am") { + this.low_cut = -4000; + this.high_cut = 4000; + } + + this.wait_for_timer = false; + this.set_after = false; + this.set = function () { //set() is a wrapper to call doset(), but it ensures that doset won't execute more frequently than demodulator_response_time. + if (!this.wait_for_timer) { + this.doset(false); + this.set_after = false; + this.wait_for_timer = true; + timeout_this = this; //http://stackoverflow.com/a/2130411 + window.setTimeout(function () { + timeout_this.wait_for_timer = false; + if (timeout_this.set_after) timeout_this.set(); + }, demodulator_response_time); + } + else { + this.set_after = true; + } + }; + + this.doset = function (first_time) { //this function sends demodulator parameters to the server + params = { + "low_cut": this.low_cut, + "high_cut": this.high_cut, + "offset_freq": this.offset_frequency + }; + if (first_time) params.mod = this.server_mod; + ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); + }; + this.doset(true); //we set parameters on object creation + + //******* envelope object ******* + // for drawing the filter envelope above scale + this.envelope.parent = this; + + this.envelope.draw = function (visible_range) { + this.visible_range = visible_range; + this.drag_ranges = demod_envelope_draw(range, + center_freq + this.parent.offset_frequency + this.parent.low_cut, + center_freq + this.parent.offset_frequency + this.parent.high_cut, + this.color, center_freq + this.parent.offset_frequency); + }; + + this.envelope.dragged_range = demodulator.draggable_ranges.none; + + // event handlers + this.envelope.drag_start = function (x, key_modifiers) { + this.key_modifiers = key_modifiers; + this.dragged_range = demod_envelope_where_clicked(x, this.drag_ranges, key_modifiers); + //console.log("dragged_range: "+this.dragged_range.toString()); + this.drag_origin = { + x: x, + low_cut: this.parent.low_cut, + high_cut: this.parent.high_cut, + offset_frequency: this.parent.offset_frequency + }; + return this.dragged_range !== demodulator.draggable_ranges.none; + }; + + this.envelope.drag_move = function (x) { + dr = demodulator.draggable_ranges; + if (this.dragged_range === dr.none) return false; // we return if user is not dragging (us) at all + freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x)); + /*if(this.dragged_range==dr.beginning||this.dragged_range==dr.ending) { //we don't let the passband be too small if(this.parent.low_cut+new_freq_change<=this.parent.high_cut-this.parent.filter.min_passband) this.freq_change=new_freq_change; @@ -525,110 +531,99 @@ function demodulator_default_analog(offset_frequency,subtype) } var new_value;*/ - //dragging the line in the middle of the filter envelope while holding Shift does emulate - //the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged - //Filter passband moves in the opposite direction than dragged, hence the minus below. - minus=(this.dragged_range==dr.bfo)?-1:1; - //dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob - //(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset - //frequency. - if(this.dragged_range==dr.beginning||this.dragged_range==dr.bfo||this.dragged_range==dr.pbs) - { - //we don't let low_cut go beyond its limits - if((new_value=this.drag_origin.low_cut+minus*freq_change)=this.parent.high_cut) return true; - this.parent.low_cut=new_value; - } - if(this.dragged_range==dr.ending||this.dragged_range==dr.bfo||this.dragged_range==dr.pbs) - { - //we don't let high_cut go beyond its limits - if((new_value=this.drag_origin.high_cut+minus*freq_change)>this.parent.filter.high_cut_limit) return true; - //nor the filter passband be too small - if(new_value-this.parent.low_cutbandwidth/2||new_value<-bandwidth/2) return true; //we don't allow tuning above Nyquist frequency :-) - this.parent.offset_frequency=new_value; - } - //now do the actual modifications: - mkenvelopes(this.visible_range); - this.parent.set(); - //will have to change this when changing to multi-demodulator mode: - e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",center_freq+this.parent.offset_frequency,1e6,4); - return true; - }; + //dragging the line in the middle of the filter envelope while holding Shift does emulate + //the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged + //Filter passband moves in the opposite direction than dragged, hence the minus below. + minus = (this.dragged_range === dr.bfo) ? -1 : 1; + //dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob + //(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset + //frequency. + if (this.dragged_range === dr.beginning || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { + //we don't let low_cut go beyond its limits + if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.parent.filter.low_cut_limit) return true; + //nor the filter passband be too small + if (this.parent.high_cut - new_value < this.parent.filter.min_passband) return true; + //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" + if (new_value >= this.parent.high_cut) return true; + this.parent.low_cut = new_value; + } + if (this.dragged_range === dr.ending || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { + //we don't let high_cut go beyond its limits + if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.parent.filter.high_cut_limit) return true; + //nor the filter passband be too small + if (new_value - this.parent.low_cut < this.parent.filter.min_passband) return true; + //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" + if (new_value <= this.parent.low_cut) return true; + this.parent.high_cut = new_value; + } + if (this.dragged_range === dr.anything_else || this.dragged_range === dr.bfo) { + //when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it) + new_value = this.drag_origin.offset_frequency + freq_change; + if (new_value > bandwidth / 2 || new_value < -bandwidth / 2) return true; //we don't allow tuning above Nyquist frequency :-) + this.parent.offset_frequency = new_value; + } + //now do the actual modifications: + mkenvelopes(this.visible_range); + this.parent.set(); + //will have to change this when changing to multi-demodulator mode: + e("webrx-actual-freq").innerHTML = format_frequency("{x} MHz", center_freq + this.parent.offset_frequency, 1e6, 4); + return true; + }; - this.envelope.drag_end=function(x) - { //in this demodulator we've already changed values in the drag_move() function so we shouldn't do too much here. - demodulator_buttons_update(); - to_return=this.dragged_range!=demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset - this.dragged_range=demodulator.draggable_ranges.none; - return to_return; - }; + this.envelope.drag_end = function (x) { //in this demodulator we've already changed values in the drag_move() function so we shouldn't do too much here. + demodulator_buttons_update(); + to_return = this.dragged_range !== demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset + this.dragged_range = demodulator.draggable_ranges.none; + return to_return; + }; } -demodulator_default_analog.prototype=new demodulator(); +demodulator_default_analog.prototype = new demodulator(); function mkenvelopes(visible_range) //called from mkscale { - scale_ctx.clearRect(0,0,scale_ctx.canvas.width,22); //clear the upper part of the canvas (where filter envelopes reside) - for (var i=0;ibandwidth/2||to_what<-bandwidth/2) return; - demodulators[0].offset_frequency=Math.round(to_what); - demodulators[0].set(); - mkenvelopes(get_visible_freq_range()); + var temp_offset = 0; + if (demodulators.length) { + temp_offset = demodulators[0].offset_frequency; + demodulator_remove(0); + } + demodulator_add(new demodulator_default_analog(temp_offset, subtype)); + demodulator_buttons_update(); + update_digitalvoice_panels("openwebrx-panel-metadata-" + subtype); +} + +function demodulator_set_offset_frequency(which, to_what) { + if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return; + demodulators[0].offset_frequency = Math.round(to_what); + demodulators[0].set(); + mkenvelopes(get_visible_freq_range()); $("#webrx-actual-freq").html(format_frequency("{x} MHz", center_freq + to_what, 1e6, 4)); } @@ -640,273 +635,260 @@ function demodulator_set_offset_frequency(which,to_what) var scale_ctx; var scale_canvas; -function scale_setup() -{ - e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(window.innerWidth/2),1e6,4); - scale_canvas=e("openwebrx-scale-canvas"); - scale_ctx=scale_canvas.getContext("2d"); - scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false); - scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false); - scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false); - resize_scale(); - var frequency_container = e("openwebrx-frequency-container"); - frequency_container.addEventListener("mousemove", frequency_container_mousemove, false); -} - -var scale_canvas_drag_params={ - mouse_down: false, - drag: false, - start_x: 0, - key_modifiers: {shiftKey:false, altKey: false, ctrlKey: false} +function scale_setup() { + e("webrx-actual-freq").innerHTML = format_frequency("{x} MHz", canvas_get_frequency(window.innerWidth / 2), 1e6, 4); + scale_canvas = e("openwebrx-scale-canvas"); + scale_ctx = scale_canvas.getContext("2d"); + scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false); + scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false); + scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false); + resize_scale(); + var frequency_container = e("openwebrx-frequency-container"); + frequency_container.addEventListener("mousemove", frequency_container_mousemove, false); +} + +var scale_canvas_drag_params = { + mouse_down: false, + drag: false, + start_x: 0, + key_modifiers: {shiftKey: false, altKey: false, ctrlKey: false} }; -function scale_canvas_mousedown(evt) -{ - with(scale_canvas_drag_params) - { - mouse_down=true; - drag=false; - start_x=evt.pageX; - key_modifiers.shiftKey=evt.shiftKey; - key_modifiers.altKey=evt.altKey; - key_modifiers.ctrlKey=evt.ctrlKey; - } - evt.preventDefault(); -} - -function scale_offset_freq_from_px(x, visible_range) -{ - if(typeof visible_range === "undefined") visible_range=get_visible_freq_range(); - return (visible_range.start+visible_range.bw*(x/canvas_container.clientWidth))-center_freq; +function scale_canvas_mousedown(evt) { + with (scale_canvas_drag_params) { + mouse_down = true; + drag = false; + start_x = evt.pageX; + key_modifiers.shiftKey = evt.shiftKey; + key_modifiers.altKey = evt.altKey; + key_modifiers.ctrlKey = evt.ctrlKey; + } + evt.preventDefault(); } -function scale_canvas_mousemove(evt) -{ - var event_handled; - if(scale_canvas_drag_params.mouse_down&&!scale_canvas_drag_params.drag&&Math.abs(evt.pageX-scale_canvas_drag_params.start_x)>canvas_drag_min_delta) - //we can use the main drag_min_delta thing of the main canvas - { - scale_canvas_drag_params.drag=true; - //call the drag_start for all demodulators (and they will decide if they're dragged, based on X coordinate) - for (var i=0;i canvas_drag_min_delta) + //we can use the main drag_min_delta thing of the main canvas + { + scale_canvas_drag_params.drag = true; + //call the drag_start for all demodulators (and they will decide if they're dragged, based on X coordinate) + for (var i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_start(evt.pageX, scale_canvas_drag_params.key_modifiers); + scale_canvas.style.cursor = "move"; + } + else if (scale_canvas_drag_params.drag) { + //call the drag_move for all demodulators (and they will decide if they're dragged) + for (var i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_move(evt.pageX); + if (!event_handled) demodulator_set_offset_frequency(0, scale_offset_freq_from_px(evt.pageX)); + } } function frequency_container_mousemove(evt) { - var frequency = center_freq + scale_offset_freq_from_px(evt.pageX); + var frequency = center_freq + scale_offset_freq_from_px(evt.pageX); e("webrx-mouse-freq").innerHTML = format_frequency("{x} MHz", frequency, 1e6, 4); } -function scale_canvas_end_drag(x) -{ - scale_canvas.style.cursor="default"; - scale_canvas_drag_params.drag=false; - scale_canvas_drag_params.mouse_down=false; - var event_handled=false; - for (var i=0;i= scale_min_space_bw_small_markers && freq.toString()[0] !== "5") { + out.small /= 2; + out.ratio *= 2; + } + out.smallbw = freq / out.ratio; + return true; + }; + for (i = scale_markers_levels.length - 1; i >= 0; i--) { + mp = scale_markers_levels[i]; + if (!fcalc(mp.large_marker_per_hz)) continue; + //console.log(mp.large_marker_per_hz); + //console.log(out); + if (out.large - mp.estimated_text_width > scale_min_space_bw_texts) break; + } + out.params = mp; + return out; +} + +function mkscale() { + //clear the lower part of the canvas (where frequency scale resides; the upper part is used by filter envelopes): + range = get_visible_freq_range(); + mkenvelopes(range); //when scale changes we will always have to redraw filter envelopes, too + scale_ctx.clearRect(0, 22, scale_ctx.canvas.width, scale_ctx.canvas.height - 22); + scale_ctx.strokeStyle = "#fff"; + scale_ctx.font = "bold 11px sans-serif"; + scale_ctx.textBaseline = "top"; + scale_ctx.fillStyle = "#fff"; + spacing = get_scale_mark_spacing(range); + //console.log(spacing); + marker_hz = Math.ceil(range.start / spacing.smallbw) * spacing.smallbw; + text_h_pos = 22 + 10 + ((is_firefox) ? 3 : 0); + var text_to_draw; + var ftext = function (f) { + text_to_draw = format_frequency(spacing.params.format, f, spacing.params.pre_divide, spacing.params.decimals); + }; + var last_large; + for (; ;) { + var x = scale_px_from_freq(marker_hz, range); + if (x > window.innerWidth) break; + scale_ctx.beginPath(); + scale_ctx.moveTo(x, 22); + if (marker_hz % spacing.params.large_marker_per_hz === 0) { //large marker + if (typeof first_large === "undefined") var first_large = marker_hz; + last_large = marker_hz; + scale_ctx.lineWidth = 3.5; + scale_ctx.lineTo(x, 22 + 11); + ftext(marker_hz); + var text_measured = scale_ctx.measureText(text_to_draw); + scale_ctx.textAlign = "center"; + //advanced text drawing begins + if (zoom_level === 0 && (range.start + spacing.smallbw * spacing.ratio > marker_hz) && (x < text_measured.width / 2)) { //if this is the first overall marker when zoomed out... and if it would be clipped off the screen... + if (scale_px_from_freq(marker_hz + spacing.smallbw * spacing.ratio, range) - text_measured.width >= scale_min_space_bw_texts) { //and if we have enough space to draw it correctly without clipping + scale_ctx.textAlign = "left"; + scale_ctx.fillText(text_to_draw, 0, text_h_pos); + } + } + else if (zoom_level === 0 && (range.end - spacing.smallbw * spacing.ratio < marker_hz) && (x > window.innerWidth - text_measured.width / 2)) { // if this is the last overall marker when zoomed out... and if it would be clipped off the screen... + if (window.innerWidth - text_measured.width - scale_px_from_freq(marker_hz - spacing.smallbw * spacing.ratio, range) >= scale_min_space_bw_texts) { //and if we have enough space to draw it correctly without clipping + scale_ctx.textAlign = "right"; + scale_ctx.fillText(text_to_draw, window.innerWidth, text_h_pos); + } + } + else scale_ctx.fillText(text_to_draw, x, text_h_pos); //draw text normally + } + else { //small marker + scale_ctx.lineWidth = 2; + scale_ctx.lineTo(x, 22 + 8); + } + marker_hz += spacing.smallbw; + scale_ctx.stroke(); + } + if (zoom_level !== 0) { // if zoomed, we don't want the texts to disappear because their markers can't be seen + // on the left side + scale_ctx.textAlign = "center"; + var f = first_large - spacing.smallbw * spacing.ratio; + var x = scale_px_from_freq(f, range); + ftext(f); + var w = scale_ctx.measureText(text_to_draw).width; + if (x + w / 2 > 0) scale_ctx.fillText(text_to_draw, x, 22 + 10); + // on the right side + f = last_large + spacing.smallbw * spacing.ratio; + x = scale_px_from_freq(f, range); + ftext(f); + w = scale_ctx.measureText(text_to_draw).width; + if (x - w / 2 < window.innerWidth) scale_ctx.fillText(text_to_draw, x, 22 + 10); + } +} -function get_scale_mark_spacing(range) -{ - out={}; - fcalc=function(freq) - { - out.numlarge=(range.bw/freq); - out.large=canvas_container.clientWidth/out.numlarge; //distance between large markers (these have text) - out.ratio=5; //(ratio-1) small markers exist per large marker - out.small=out.large/out.ratio; //distance between small markers - if(out.small=scale_min_space_bw_small_markers&&freq.toString()[0]!="5") {out.small/=2; out.ratio*=2; } - out.smallbw=freq/out.ratio; - return true; - } - for(i=scale_markers_levels.length-1;i>=0;i--) - { - mp=scale_markers_levels[i]; - if (!fcalc(mp.large_marker_per_hz)) continue; - //console.log(mp.large_marker_per_hz); - //console.log(out); - if (out.large-mp.estimated_text_width>scale_min_space_bw_texts) break; - } - out.params=mp; - return out; -} - -function mkscale() -{ - //clear the lower part of the canvas (where frequency scale resides; the upper part is used by filter envelopes): - range=get_visible_freq_range(); - mkenvelopes(range); //when scale changes we will always have to redraw filter envelopes, too - scale_ctx.clearRect(0,22,scale_ctx.canvas.width,scale_ctx.canvas.height-22); - scale_ctx.strokeStyle = "#fff"; - scale_ctx.font = "bold 11px sans-serif"; - scale_ctx.textBaseline = "top"; - scale_ctx.fillStyle = "#fff"; - spacing=get_scale_mark_spacing(range); - //console.log(spacing); - marker_hz=Math.ceil(range.start/spacing.smallbw)*spacing.smallbw; - text_h_pos=22+10+((is_firefox)?3:0); - var text_to_draw; - var ftext=function(f) {text_to_draw=format_frequency(spacing.params.format,f,spacing.params.pre_divide,spacing.params.decimals);} - var last_large; - for(;;) - { - var x=scale_px_from_freq(marker_hz,range); - if(x>window.innerWidth) break; - scale_ctx.beginPath(); - scale_ctx.moveTo(x, 22); - if(marker_hz%spacing.params.large_marker_per_hz==0) - { //large marker - if(typeof first_large == "undefined") var first_large=marker_hz; - last_large=marker_hz; - scale_ctx.lineWidth=3.5; - scale_ctx.lineTo(x,22+11); - ftext(marker_hz); - var text_measured=scale_ctx.measureText(text_to_draw); - scale_ctx.textAlign = "center"; - //advanced text drawing begins - if( zoom_level==0 && (range.start+spacing.smallbw*spacing.ratio>marker_hz) && (x=scale_min_space_bw_texts) - { //and if we have enough space to draw it correctly without clipping - scale_ctx.textAlign = "left"; - scale_ctx.fillText(text_to_draw, 0, text_h_pos); - } - } - else if( zoom_level==0 && (range.end-spacing.smallbw*spacing.ratiowindow.innerWidth-text_measured.width/2) ) - { // if this is the last overall marker when zoomed out... and if it would be clipped off the screen... - if(window.innerWidth-text_measured.width-scale_px_from_freq(marker_hz-spacing.smallbw*spacing.ratio,range)>=scale_min_space_bw_texts) - { //and if we have enough space to draw it correctly without clipping - scale_ctx.textAlign = "right"; - scale_ctx.fillText(text_to_draw, window.innerWidth, text_h_pos); - } - } - else scale_ctx.fillText(text_to_draw, x, text_h_pos); //draw text normally - } - else - { //small marker - scale_ctx.lineWidth=2; - scale_ctx.lineTo(x,22+8); - } - marker_hz+=spacing.smallbw; - scale_ctx.stroke(); - } - if(zoom_level!=0) - { // if zoomed, we don't want the texts to disappear because their markers can't be seen - // on the left side - scale_ctx.textAlign = "center"; - var f=first_large-spacing.smallbw*spacing.ratio; - var x=scale_px_from_freq(f,range); - ftext(f); - var w=scale_ctx.measureText(text_to_draw).width; - if(x+w/2>0) scale_ctx.fillText(text_to_draw, x, 22+10); - // on the right side - f=last_large+spacing.smallbw*spacing.ratio; - x=scale_px_from_freq(f,range); - ftext(f); - w=scale_ctx.measureText(text_to_draw).width; - if(x-w/23) - { - out=out.substr(0,at)+","+out.substr(at); - at+=4; - decimals-=3; - } - return out; -} - -canvas_drag=false; -canvas_drag_min_delta=1; -canvas_mouse_down=false; - -function canvas_mousedown(evt) -{ - canvas_mouse_down=true; - canvas_drag=false; - canvas_drag_last_x=canvas_drag_start_x=evt.pageX; - canvas_drag_last_y=canvas_drag_start_y=evt.pageY; - evt.preventDefault(); //don't show text selection mouse pointer +function format_frequency(format, freq_hz, pre_divide, decimals) { + out = format.replace("{x}", (freq_hz / pre_divide).toFixed(decimals)); + at = out.indexOf(".") + 4; + while (decimals > 3) { + out = out.substr(0, at) + "," + out.substr(at); + at += 4; + decimals -= 3; + } + return out; } -function canvas_mousemove(evt) -{ - if(!waterfall_setup_done) return; - relativeX = get_relative_x(evt); - if(canvas_mouse_down) - { - if(!canvas_drag&&Math.abs(evt.pageX-canvas_drag_start_x)>canvas_drag_min_delta) - { - canvas_drag=true; - canvas_container.style.cursor="move"; - } - if(canvas_drag) - { - var deltaX=canvas_drag_last_x-evt.pageX; - var deltaY=canvas_drag_last_y-evt.pageY; - var dpx=range.hps*deltaX; - if( - !(zoom_center_rel+dpx>(bandwidth/2-canvas_container.clientWidth*(1-zoom_center_where)*range.hps)) && - !(zoom_center_rel+dpx<-bandwidth/2+canvas_container.clientWidth*zoom_center_where*range.hps) - ) { zoom_center_rel+=dpx; } - resize_canvases(false); - canvas_drag_last_x=evt.pageX; - canvas_drag_last_y=evt.pageY; - mkscale(); - bookmarks.position(); - } - } - else e("webrx-mouse-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4); +canvas_drag = false; +canvas_drag_min_delta = 1; +canvas_mouse_down = false; + +function canvas_mousedown(evt) { + canvas_mouse_down = true; + canvas_drag = false; + canvas_drag_last_x = canvas_drag_start_x = evt.pageX; + canvas_drag_last_y = canvas_drag_start_y = evt.pageY; + evt.preventDefault(); //don't show text selection mouse pointer } -function canvas_container_mouseleave(evt) -{ - canvas_end_drag(); +function canvas_mousemove(evt) { + if (!waterfall_setup_done) return; + relativeX = get_relative_x(evt); + if (canvas_mouse_down) { + if (!canvas_drag && Math.abs(evt.pageX - canvas_drag_start_x) > canvas_drag_min_delta) { + canvas_drag = true; + canvas_container.style.cursor = "move"; + } + if (canvas_drag) { + var deltaX = canvas_drag_last_x - evt.pageX; + var deltaY = canvas_drag_last_y - evt.pageY; + var dpx = range.hps * deltaX; + if ( + !(zoom_center_rel + dpx > (bandwidth / 2 - canvas_container.clientWidth * (1 - zoom_center_where) * range.hps)) && + !(zoom_center_rel + dpx < -bandwidth / 2 + canvas_container.clientWidth * zoom_center_where * range.hps) + ) { + zoom_center_rel += dpx; + } + resize_canvases(false); + canvas_drag_last_x = evt.pageX; + canvas_drag_last_y = evt.pageY; + mkscale(); + bookmarks.position(); + } + } + else e("webrx-mouse-freq").innerHTML = format_frequency("{x} MHz", canvas_get_frequency(relativeX), 1e6, 4); } -function canvas_mouseup(evt) -{ - if(!waterfall_setup_done) return; - relativeX = get_relative_x(evt); +function canvas_container_mouseleave(evt) { + canvas_end_drag(); +} - if(!canvas_drag) - { - demodulator_set_offset_frequency(0, canvas_get_freq_offset(relativeX)); - } - else - { - canvas_end_drag(); - } - canvas_mouse_down=false; +function canvas_mouseup(evt) { + if (!waterfall_setup_done) return; + relativeX = get_relative_x(evt); + + if (!canvas_drag) { + demodulator_set_offset_frequency(0, canvas_get_freq_offset(relativeX)); + } + else { + canvas_end_drag(); + } + canvas_mouse_down = false; } -function canvas_end_drag() -{ - canvas_container.style.cursor="crosshair"; - canvas_mouse_down=false; +function canvas_end_drag() { + canvas_container.style.cursor = "crosshair"; + canvas_mouse_down = false; } -function zoom_center_where_calc(screenposX) -{ - //return (screenposX-(window.innerWidth-canvas_container.clientWidth))/canvas_container.clientWidth; - return screenposX/canvas_container.clientWidth; +function zoom_center_where_calc(screenposX) { + //return (screenposX-(window.innerWidth-canvas_container.clientWidth))/canvas_container.clientWidth; + return screenposX / canvas_container.clientWidth; } function get_relative_x(evt) { - var relativeX = (evt.offsetX)?evt.offsetX:evt.layerX; + var relativeX = (evt.offsetX) ? evt.offsetX : evt.layerX; if ($(evt.target).closest(canvas_container).length) return relativeX; - // compensate for the frequency scale, since that is not resized by the browser. + // compensate for the frequency scale, since that is not resized by the browser. return relativeX - zoom_offset_px; } -function canvas_mousewheel(evt) -{ - if(!waterfall_setup_done) return; - var relativeX = get_relative_x(evt); - var dir=(evt.deltaY/Math.abs(evt.deltaY))>0; - zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX)); - evt.preventDefault(); -} - - -zoom_max_level_hps=33; //Hz/pixel -zoom_levels_count=14; - -function get_zoom_coeff_from_hps(hps) -{ - var shown_bw=(window.innerWidth*hps); - return bandwidth/shown_bw; -} - -zoom_levels=[1]; -zoom_level=0; -zoom_freq=0; -zoom_offset_px=0; -zoom_center_rel=0; -zoom_center_where=0; - -smeter_level=0; +function canvas_mousewheel(evt) { + if (!waterfall_setup_done) return; + var relativeX = get_relative_x(evt); + var dir = (evt.deltaY / Math.abs(evt.deltaY)) > 0; + zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX)); + evt.preventDefault(); +} + + +zoom_max_level_hps = 33; //Hz/pixel +zoom_levels_count = 14; + +function get_zoom_coeff_from_hps(hps) { + var shown_bw = (window.innerWidth * hps); + return bandwidth / shown_bw; +} + +zoom_levels = [1]; +zoom_level = 0; +zoom_freq = 0; +zoom_offset_px = 0; +zoom_center_rel = 0; +zoom_center_where = 0; + +smeter_level = 0; + +function mkzoomlevels() { + zoom_levels = [1]; + maxc = get_zoom_coeff_from_hps(zoom_max_level_hps); + if (maxc < 1) return; + // logarithmic interpolation + zoom_ratio = Math.pow(maxc, 1 / zoom_levels_count); + for (i = 1; i < zoom_levels_count; i++) + zoom_levels.push(Math.pow(zoom_ratio, i)); +} + +function zoom_step(out, where, onscreen) { + if ((out && zoom_level === 0) || (!out && zoom_level >= zoom_levels_count - 1)) return; + if (out) --zoom_level; + else ++zoom_level; + + zoom_center_rel = canvas_get_freq_offset(where); + //console.log("zoom_step || zlevel: "+zoom_level.toString()+" zlevel_val: "+zoom_levels[zoom_level].toString()+" zoom_center_rel: "+zoom_center_rel.toString()); + zoom_center_where = onscreen; + //console.log(zoom_center_where, zoom_center_rel, where); + resize_canvases(true); + mkscale(); + bookmarks.position(); +} + +function zoom_set(level) { + if (!(level >= 0 && level <= zoom_levels.length - 1)) return; + level = parseInt(level); + zoom_level = level; + //zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+canvas_container.clientWidth/2); //zoom to screen center instead of demod envelope + zoom_center_rel = demodulators[0].offset_frequency; + zoom_center_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack + console.log(zoom_center_where, zoom_center_rel, -canvases[0].offsetLeft + canvas_container.clientWidth / 2); + resize_canvases(true); + mkscale(); + bookmarks.position(); +} + +function zoom_calc() { + winsize = canvas_container.clientWidth; + var canvases_new_width = winsize * zoom_levels[zoom_level]; + zoom_offset_px = -((canvases_new_width * (0.5 + zoom_center_rel / bandwidth)) - (winsize * zoom_center_where)); + if (zoom_offset_px > 0) zoom_offset_px = 0; + if (zoom_offset_px < winsize - canvases_new_width) + zoom_offset_px = winsize - canvases_new_width; + //console.log("zoom_calc || zopx:"+zoom_offset_px.toString()+ " maxoff:"+(winsize-canvases_new_width).toString()+" relval:"+(0.5+zoom_center_rel/bandwidth).toString() ); +} + +function resize_waterfall_container(check_init) { + if (check_init && !waterfall_setup_done) return; + var numHeight; + mathbox_container.style.height = canvas_container.style.height = (numHeight = window.innerHeight - e("webrx-top-container").clientHeight - e("openwebrx-scale-container").clientHeight).toString() + "px"; + if (mathbox) { + //mathbox.three.camera.aspect = document.body.offsetWidth / numHeight; + //mathbox.three.camera.updateProjectionMatrix(); + mathbox.three.renderer.setSize(document.body.offsetWidth, numHeight); + console.log(document.body.offsetWidth, numHeight); + } -function mkzoomlevels() -{ - zoom_levels=[1]; - maxc=get_zoom_coeff_from_hps(zoom_max_level_hps); - if(maxc<1) return; - // logarithmic interpolation - zoom_ratio = Math.pow(maxc, 1/zoom_levels_count); - for(i=1;i=zoom_levels_count-1)) return; - if(out) --zoom_level; - else ++zoom_level; - - zoom_center_rel=canvas_get_freq_offset(where); - //console.log("zoom_step || zlevel: "+zoom_level.toString()+" zlevel_val: "+zoom_levels[zoom_level].toString()+" zoom_center_rel: "+zoom_center_rel.toString()); - zoom_center_where=onscreen; - //console.log(zoom_center_where, zoom_center_rel, where); - resize_canvases(true); - mkscale(); - bookmarks.position(); -} +audio_server_output_rate = 11025; +audio_client_resampling_factor = 4; -function zoom_set(level) -{ - if(!(level>=0&&level<=zoom_levels.length-1)) return; - level=parseInt(level); - zoom_level = level; - //zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+canvas_container.clientWidth/2); //zoom to screen center instead of demod envelope - zoom_center_rel=demodulators[0].offset_frequency; - zoom_center_where=0.5+(zoom_center_rel/bandwidth); //this is a kind of hack - console.log(zoom_center_where, zoom_center_rel, -canvases[0].offsetLeft+canvas_container.clientWidth/2); - resize_canvases(true); - mkscale(); - bookmarks.position(); -} - -function zoom_calc() -{ - winsize=canvas_container.clientWidth; - var canvases_new_width=winsize*zoom_levels[zoom_level]; - zoom_offset_px=-((canvases_new_width*(0.5+zoom_center_rel/bandwidth))-(winsize*zoom_center_where)); - if(zoom_offset_px>0) zoom_offset_px=0; - if(zoom_offset_pxPlease change your operating system default settings in order to fix this.",1); - } - if(audio_server_output_rate >= output_range_min && audio_server_output_rate <= output_range_max) break; //okay, we're done - i++; - } - audio_client_resampling_factor=i; - console.log("audio_calculate_resampling() :: "+audio_client_resampling_factor.toString()+", "+audio_server_output_rate.toString()); +function audio_calculate_resampling(targetRate) { //both at the server and the client + output_range_max = 12000; + output_range_min = 8000; + i = 1; + while (true) { + audio_server_output_rate = Math.floor(targetRate / i); + if (audio_server_output_rate < output_range_min) { + audio_client_resampling_factor = audio_server_output_rate = 0; + divlog("Your audio card sampling rate (" + targetRate.toString() + ") is not supported.
    Please change your operating system default settings in order to fix this.", 1); + } + if (audio_server_output_rate >= output_range_min && audio_server_output_rate <= output_range_max) break; //okay, we're done + i++; + } + audio_client_resampling_factor = i; + console.log("audio_calculate_resampling() :: " + audio_client_resampling_factor.toString() + ", " + audio_server_output_rate.toString()); } -debug_ws_data_received=0; +debug_ws_data_received = 0; debug_ws_time_start = 0; -max_clients_num=0; +max_clients_num = 0; client_num = 0; var currentprofile; -var COMPRESS_FFT_PAD_N=10; //should be the same as in csdr.c +var COMPRESS_FFT_PAD_N = 10; //should be the same as in csdr.c -function on_ws_recv(evt) -{ - if (typeof evt.data == 'string') { +function on_ws_recv(evt) { + if (typeof evt.data === 'string') { // text messages debug_ws_data_received += evt.data.length / 1000; - if (evt.data.substr(0, 16) == "CLIENT DE SERVER") { - divlog("Server acknowledged WebSocket connection."); - } else { - try { - json = JSON.parse(evt.data) - switch (json.type) { + if (evt.data.substr(0, 16) === "CLIENT DE SERVER") { + divlog("Server acknowledged WebSocket connection."); + } else { + try { + json = JSON.parse(evt.data); + switch (json.type) { case "config": config = json.value; window.waterfall_colors = config.waterfall_colors; @@ -1180,136 +1137,136 @@ function on_ws_recv(evt) window.waterfall_auto_level_margin = config.waterfall_auto_level_margin; waterfallColorsDefault(); - window.starting_mod = config.start_mod + window.starting_mod = config.start_mod; window.starting_offset_frequency = config.start_offset_freq; window.audio_buffering_fill_to = config.client_audio_buffer_size; bandwidth = config.samp_rate; center_freq = config.center_freq + config.lfo_offset; fft_size = config.fft_size; - fft_fps = config.fft_fps; - audio_compression = config.audio_compression; - divlog( "Audio stream is "+ ((audio_compression=="adpcm")?"compressed":"uncompressed")+"." ) - fft_compression = config.fft_compression; - divlog( "FFT stream is "+ ((fft_compression=="adpcm")?"compressed":"uncompressed")+"." ) - max_clients_num = config.max_clients; - progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num*0.85); + fft_fps = config.fft_fps; + audio_compression = config.audio_compression; + divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + "."); + fft_compression = config.fft_compression; + divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + "."); + max_clients_num = config.max_clients; + progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num * 0.85); mathbox_waterfall_colors = config.mathbox_waterfall_colors; - mathbox_waterfall_frequency_resolution = config.mathbox_waterfall_frequency_resolution; - mathbox_waterfall_history_length = config.mathbox_waterfall_history_length; - - waterfall_init(); - audio_preinit(); - bookmarks.loadLocalBookmarks(); - - if (audio_allowed) { - if (audio_initialized) { - initialize_demodulator(); - } else { - audio_init(); - } - } - waterfall_clear(); + mathbox_waterfall_frequency_resolution = config.mathbox_waterfall_frequency_resolution; + mathbox_waterfall_history_length = config.mathbox_waterfall_history_length; + + waterfall_init(); + audio_preinit(); + bookmarks.loadLocalBookmarks(); + + if (audio_allowed) { + if (audio_initialized) { + initialize_demodulator(); + } else { + audio_init(); + } + } + waterfall_clear(); currentprofile = config.profile_id; - $('#openwebrx-sdr-profiles-listbox').val(currentprofile); - break; + $('#openwebrx-sdr-profiles-listbox').val(currentprofile); + break; case "secondary_config": window.secondary_fft_size = json.value.secondary_fft_size; window.secondary_bw = json.value.secondary_bw; window.if_samp_rate = json.value.if_samp_rate; secondary_demod_init_canvases(); - break; + break; case "receiver_details": var r = json.value; e('webrx-rx-title').innerHTML = r.receiver_name; e('webrx-rx-desc').innerHTML = r.receiver_location + ' | Loc: ' + r.locator + ', ASL: ' + r.receiver_asl + ' m, [maps]'; e('webrx-rx-photo-title').innerHTML = r.photo_title; e('webrx-rx-photo-desc').innerHTML = r.photo_desc; - break; + break; case "smeter": smeter_level = json.value; setSmeterAbsoluteValue(smeter_level); - break; + break; case "cpuusage": - var server_cpu_usage = json.value; - progressbar_set(e("openwebrx-bar-server-cpu"),server_cpu_usage,"Server CPU [" + Math.round(server_cpu_usage * 100) + "%]",server_cpu_usage>85); - break; + var server_cpu_usage = json.value; + progressbar_set(e("openwebrx-bar-server-cpu"), server_cpu_usage, "Server CPU [" + Math.round(server_cpu_usage * 100) + "%]", server_cpu_usage > 85); + break; case "clients": client_num = json.value; - progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num*0.85); - break; + progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num * 0.85); + break; case "profiles": var listbox = e("openwebrx-sdr-profiles-listbox"); - listbox.innerHTML = json.value.map(function(profile){ + listbox.innerHTML = json.value.map(function (profile) { return '"; }).join(""); if (currentprofile) { - $('#openwebrx-sdr-profiles-listbox').val(currentprofile); - } - break; - case "features": - for (var feature in json.value) { - $('[data-feature="' + feature + '"')[json.value[feature] ? "show" : "hide"](); - } - break; - case "metadata": - update_metadata(json.value); - break; - case "wsjt_message": - update_wsjt_panel(json.value); - break; - case "dial_frequencies": - var as_bookmarks = json.value.map(function(d){ - return { - name: d.mode.toUpperCase(), - digital_modulation: d.mode, - frequency: d.frequency - }; - }); - bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies'); - break; - case "aprs_data": - update_packet_panel(json.value); - break; - case "bookmarks": - bookmarks.replace_bookmarks(json.value, "server"); - break; - case "sdr_error": - divlog(json.value, true); - break; + $('#openwebrx-sdr-profiles-listbox').val(currentprofile); + } + break; + case "features": + for (var feature in json.value) { + $('[data-feature="' + feature + '"')[json.value[feature] ? "show" : "hide"](); + } + break; + case "metadata": + update_metadata(json.value); + break; + case "wsjt_message": + update_wsjt_panel(json.value); + break; + case "dial_frequencies": + var as_bookmarks = json.value.map(function (d) { + return { + name: d.mode.toUpperCase(), + digital_modulation: d.mode, + frequency: d.frequency + }; + }); + bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies'); + break; + case "aprs_data": + update_packet_panel(json.value); + break; + case "bookmarks": + bookmarks.replace_bookmarks(json.value, "server"); + break; + case "sdr_error": + divlog(json.value, true); + break; default: console.warn('received message of unknown type: ' + json.type); - } - } catch (e) { - // don't lose exception - console.error(e) - } - } + } + } catch (e) { + // don't lose exception + console.error(e) + } + } } else if (evt.data instanceof ArrayBuffer) { // binary messages debug_ws_data_received += evt.data.byteLength / 1000; - type = new Uint8Array(evt.data, 0, 1)[0] - data = evt.data.slice(1) + type = new Uint8Array(evt.data, 0, 1)[0]; + data = evt.data.slice(1); switch (type) { case 1: // FFT data - if (fft_compression=="none") { + if (fft_compression === "none") { waterfall_add(new Float32Array(data)); - } else if (fft_compression == "adpcm") { + } else if (fft_compression === "adpcm") { fft_codec.reset(); - var waterfall_i16=fft_codec.decode(new Uint8Array(data)); - var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N); - for(var i=0;iaudio_buffering_fill_to)) audio_init() - break; + if (!(ios || is_chrome) && (audio_initialized === 0 && audio_prepared_buffers.length > audio_buffering_fill_to)) audio_init() + break; case 3: // secondary FFT - if (fft_compression == "none") { + if (fft_compression === "none") { secondary_demod_waterfall_add(new Float32Array(data)); - } else if (fft_compression == "adpcm") { + } else if (fft_compression === "adpcm") { fft_codec.reset(); - var waterfall_i16=fft_codec.decode(new Uint8Array(data)); - var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N); - for(var i=0;i= 0) { var matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); - if (matches && matches[2] != 'RR73') { + if (matches && matches[2] !== 'RR73') { linkedmsg = html_escape(matches[1]) + '' + matches[2] + ''; } else { linkedmsg = html_escape(linkedmsg); } - } else if (msg['mode'] == 'WSPR') { + } else if (msg['mode'] === 'WSPR') { var matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/); if (matches) { linkedmsg = html_escape(matches[1]) + '' + matches[2] + '' + html_escape(matches[3]); @@ -1430,11 +1389,11 @@ function update_wsjt_panel(msg) { } $b.append($( '' + - '' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '' + - '' + msg['db'] + '' + - '' + msg['dt'] + '' + - '' + msg['freq'] + '' + - '' + linkedmsg + '' + + '' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '' + + '' + msg['db'] + '' + + '' + msg['dt'] + '' + + '' + msg['freq'] + '' + + '' + linkedmsg + '' + '' )); $b.scrollTop($b[0].scrollHeight); @@ -1445,8 +1404,8 @@ var digital_removal_interval; // remove old wsjt messages in fixed intervals function init_digital_removal_timer() { if (digital_removal_interval) clearInterval(digital_removal_interval); - digital_removal_interval = setInterval(function(){ - ['#openwebrx-panel-wsjt-message', '#openwebrx-panel-packet-message'].forEach(function(root){ + digital_removal_interval = setInterval(function () { + ['#openwebrx-panel-wsjt-message', '#openwebrx-panel-packet-message'].forEach(function (root) { var $elements = $(root + ' tbody tr'); // limit to 1000 entries in the list since browsers get laggy at some point var toRemove = $elements.length - 1000; @@ -1457,18 +1416,20 @@ function init_digital_removal_timer() { } function update_packet_panel(msg) { - var $b = $('#openwebrx-panel-packet-message tbody'); - var pad = function(i) { return ('' + i).padStart(2, "0"); } + var $b = $('#openwebrx-panel-packet-message').find('tbody'); + var pad = function (i) { + return ('' + i).padStart(2, "0"); + }; - if (msg.type && msg.type == 'thirdparty' && msg.data) { + if (msg.type && msg.type === 'thirdparty' && msg.data) { msg = msg.data; } var source = msg.source; if (msg.type) { - if (msg.type == 'item') { + if (msg.type === 'item') { source = msg.item; } - if (msg.type == 'object') { + if (msg.type === 'object') { source = msg.object; } } @@ -1483,26 +1444,28 @@ function update_packet_panel(msg) { var classes = []; var styles = {}; var overlay = ''; - var stylesToString = function(s) { - return $.map(s, function(value, key){ return key + ':' + value + ';'}).join('') - } + var stylesToString = function (s) { + return $.map(s, function (value, key) { + return key + ':' + value + ';' + }).join('') + }; if (msg.symbol) { classes.push('aprs-symbol'); - classes.push('aprs-symboltable-' + (msg.symbol.table == '/' ? 'normal' : 'alternate')); + classes.push('aprs-symboltable-' + (msg.symbol.table === '/' ? 'normal' : 'alternate')); styles['background-position-x'] = -(msg.symbol.index % 16) * 15 + 'px'; styles['background-position-y'] = -Math.floor(msg.symbol.index / 16) * 15 + 'px'; - if (msg.symbol.table != '/' && msg.symbol.table != '\\') { + if (msg.symbol.table !== '/' && msg.symbol.table !== '\\') { s = {} s['background-position-x'] = -(msg.symbol.tableindex % 16) * 15 + 'px'; s['background-position-y'] = -Math.floor(msg.symbol.tableindex / 16) * 15 + 'px'; - overlay='
    '; + overlay = '
    '; } } else if (msg.lat && msg.lon) { classes.push('openwebrx-maps-pin'); } var attrs = [ 'class="' + classes.join(' ') + '"', - 'style="' + stylesToString(styles) + '"', + 'style="' + stylesToString(styles) + '"' ].join(' '); if (msg.lat && msg.lon) { link = '' + overlay + ''; @@ -1512,18 +1475,18 @@ function update_packet_panel(msg) { $b.append($( '' + - '' + timestamp + '' + - '' + source + '' + - '' + link + '' + - '' + (msg.comment || msg.message || '') + '' + + '' + timestamp + '' + + '' + source + '' + + '' + link + '' + + '' + (msg.comment || msg.message || '') + '' + '' )); $b.scrollTop($b[0].scrollHeight); } function update_digitalvoice_panels(showing) { - $(".openwebrx-meta-panel").each(function(_, p){ - toggle_panel(p.id, p.id == showing); + $(".openwebrx-meta-panel").each(function (_, p) { + toggle_panel(p.id, p.id === showing); }); clear_metadata(); } @@ -1534,61 +1497,57 @@ function clear_metadata() { $(".openwebrx-dmr-timeslot-panel").removeClass("muted"); } -function add_problem(what) -{ - problems_span=e("openwebrx-problems"); - for(var i=0;i"+what+""; - if(e("openwebrx-panel-log").openwebrxHidden) toggle_panel("openwebrx-panel-log"); //show panel if any error is present - } - e("openwebrx-debugdiv").innerHTML+=what+"
    "; - //var wls=e("openwebrx-log-scroll"); - //wls.scrollTop=wls.scrollHeight; //scroll to bottom +function divlog(what, is_error) { + is_error = !!is_error; + was_error |= is_error; + if (is_error) { + what = "" + what + ""; + if (e("openwebrx-panel-log").openwebrxHidden) toggle_panel("openwebrx-panel-log"); //show panel if any error is present + } + e("openwebrx-debugdiv").innerHTML += what + "
    "; + //var wls=e("openwebrx-log-scroll"); + //wls.scrollTop=wls.scrollHeight; //scroll to bottom $(".nano").nanoScroller(); - $(".nano").nanoScroller({ scroll: 'bottom' }); + $(".nano").nanoScroller({scroll: 'bottom'}); } var audio_context; -var audio_initialized=0; +var audio_initialized = 0; var volume = 1.0; var volumeBeforeMute = 100.0; var mute = false; @@ -1596,17 +1555,16 @@ var mute = false; var audio_received = Array(); var audio_buffer_index = 0; var audio_resampler; -var audio_codec=new sdrjs.ImaAdpcm(); -var audio_compression="unknown"; +var audio_codec = new sdrjs.ImaAdpcm(); var audio_node; //var audio_received_sample_rate = 48000; var audio_input_buffer_size; // Optimalise these if audio lags or is choppy: var audio_buffer_size; -var audio_buffer_maximal_length_sec=3; //actual number of samples are calculated from sample rate -var audio_buffer_decrease_to_on_overrun_sec=2.2; -var audio_flush_interval_ms=500; //the interval in which audio_flush() is called +var audio_buffer_maximal_length_sec = 3; //actual number of samples are calculated from sample rate +var audio_buffer_decrease_to_on_overrun_sec = 2.2; +var audio_flush_interval_ms = 500; //the interval in which audio_flush() is called var audio_prepared_buffers = Array(); var audio_rebuffer; @@ -1614,413 +1572,397 @@ var audio_last_output_buffer; var audio_last_output_offset = 0; var audio_buffering = false; //var audio_buffering_fill_to=4; //on audio underrun we wait until this n*audio_buffer_size samples are present - //tnx to the hint from HA3FLT, now we have about half the response time! (original value: 10) +//tnx to the hint from HA3FLT, now we have about half the response time! (original value: 10) -function gain_ff(gain_value,data) //great! solved clicking! will have to move to sdr.js +function gain_ff(gain_value, data) //great! solved clicking! will have to move to sdr.js { - for(var i=0;iaudio_buffering_fill_to) { console.log("buffers now: "+audio_prepared_buffers.length.toString()); audio_buffering=false; } + //console.log("prepare",data.length,audio_rebuffer.remaining()); + while (audio_rebuffer.remaining()) { + audio_prepared_buffers.push(audio_rebuffer.take()); + audio_buffer_current_count_debug++; + } + if (audio_buffering && audio_prepared_buffers.length > audio_buffering_fill_to) { + console.log("buffers now: " + audio_prepared_buffers.length.toString()); + audio_buffering = false; + } } -function audio_prepare_without_resampler(data) -{ - audio_rebuffer.push(sdrjs.ConvertI16_F(data)); - console.log("prepare",data.length,audio_rebuffer.remaining()); - while(audio_rebuffer.remaining()) - { - audio_prepared_buffers.push(audio_rebuffer.take()); - audio_buffer_current_count_debug++; - } - if(audio_buffering && audio_prepared_buffers.length>audio_buffering_fill_to) audio_buffering=false; +function audio_prepare_without_resampler(data) { + audio_rebuffer.push(sdrjs.ConvertI16_F(data)); + console.log("prepare", data.length, audio_rebuffer.remaining()); + while (audio_rebuffer.remaining()) { + audio_prepared_buffers.push(audio_rebuffer.take()); + audio_buffer_current_count_debug++; + } + if (audio_buffering && audio_prepared_buffers.length > audio_buffering_fill_to) audio_buffering = false; } -function audio_prepare_old(data) -{ - //console.log("audio_prepare :: "+data.length.toString()); - //console.log("data.len = "+data.length.toString()); - var dopush=function() - { - console.log(audio_last_output_buffer); - audio_prepared_buffers.push(audio_last_output_buffer); - audio_last_output_offset=0; - audio_last_output_buffer=new Float32Array(audio_buffer_size); - audio_buffer_current_count_debug++; - }; - - var original_data_length=data.length; - var f32data=new Float32Array(data.length); - for(var i=0;iaudio_buffering_fill_to) audio_buffering=false; -} - -if (!AudioBuffer.prototype.copyToChannel) -{ //Chrome 36 does not have it, Firefox does - AudioBuffer.prototype.copyToChannel=function(input,channel) //input is Float32Array - { - var cd=this.getChannelData(channel); - for(var i=0;i audio_buffering_fill_to) audio_buffering = false; +} + +if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefox does + AudioBuffer.prototype.copyToChannel = function (input, channel) //input is Float32Array + { + var cd = this.getChannelData(channel); + for (var i = 0; i < input.length; i++) cd[i] = input[i]; + } +} + +var silence = new Float32Array(4096); + +function audio_onprocess(e) { + if (audio_buffering) { e.outputBuffer.copyToChannel(silence, 0); return; } - if(audio_prepared_buffers.length==0) { + if (audio_prepared_buffers.length === 0) { audio_buffer_progressbar_update(); /*add_problem("audio underrun");*/ - audio_buffering=true; - var silence = new Float32Array(4096); + audio_buffering = true; e.outputBuffer.copyToChannel(silence, 0); } else { var buf = audio_prepared_buffers.shift(); - e.outputBuffer.copyToChannel(buf,0); + e.outputBuffer.copyToChannel(buf, 0); } } -var audio_buffer_progressbar_update_disabled=false; +var audio_buffer_progressbar_update_disabled = false; -var audio_buffer_total_average_level=0; -var audio_buffer_total_average_level_length=0; +var audio_buffer_total_average_level = 0; +var audio_buffer_total_average_level_length = 0; var audio_overrun_cnt = 0; var audio_underrun_cnt = 0; -function audio_buffer_progressbar_update() -{ - if(audio_buffer_progressbar_update_disabled) return; - var audio_buffer_value=(audio_prepared_buffers.length*audio_buffer_size)/audio_context.sampleRate; - audio_buffer_total_average_level_length++; audio_buffer_total_average_level=(audio_buffer_total_average_level*((audio_buffer_total_average_level_length-1)/audio_buffer_total_average_level_length))+(audio_buffer_value/audio_buffer_total_average_level_length); - var overrun=audio_buffer_value>audio_buffer_maximal_length_sec; - var underrun=audio_prepared_buffers.length==0; - var text="buffer"; - if(overrun) { text="overrun"; console.log("audio overrun, "+(++audio_overrun_cnt).toString()); } - if(underrun) { text="underrun"; console.log("audio underrun, "+(++audio_underrun_cnt).toString()); } - if(overrun||underrun) - { - audio_buffer_progressbar_update_disabled=true; - window.setTimeout(function(){audio_buffer_progressbar_update_disabled=false; audio_buffer_progressbar_update();},1000); - } - progressbar_set(e("openwebrx-bar-audio-buffer"),(underrun)?1:audio_buffer_value/1.5,"Audio "+text+" ["+(audio_buffer_value).toFixed(1)+" s]",overrun||underrun||audio_buffer_value<0.25); +function audio_buffer_progressbar_update() { + if (audio_buffer_progressbar_update_disabled) return; + var audio_buffer_value = (audio_prepared_buffers.length * audio_buffer_size) / audio_context.sampleRate; + audio_buffer_total_average_level_length++; + audio_buffer_total_average_level = (audio_buffer_total_average_level * ((audio_buffer_total_average_level_length - 1) / audio_buffer_total_average_level_length)) + (audio_buffer_value / audio_buffer_total_average_level_length); + var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; + var underrun = audio_prepared_buffers.length === 0; + var text = "buffer"; + if (overrun) { + text = "overrun"; + console.log("audio overrun, " + (++audio_overrun_cnt).toString()); + } + if (underrun) { + text = "underrun"; + console.log("audio underrun, " + (++audio_underrun_cnt).toString()); + } + if (overrun || underrun) { + audio_buffer_progressbar_update_disabled = true; + window.setTimeout(function () { + audio_buffer_progressbar_update_disabled = false; + audio_buffer_progressbar_update(); + }, 1000); + } + progressbar_set(e("openwebrx-bar-audio-buffer"), (underrun) ? 1 : audio_buffer_value / 1.5, "Audio " + text + " [" + (audio_buffer_value).toFixed(1) + " s]", overrun || underrun || audio_buffer_value < 0.25); } - -function audio_flush() -{ - flushed=false; - we_have_more_than=function(sec){ return sec*audio_context.sampleRateread_remain) - { - for (i=audio_buffer_index; i"+read_remain.toString()+" obi="+obi.toString()+"\n"; - audio_buffer_index+=read_remain; - break; - } - else - { - for (i=audio_buffer_index; iaudio_buffer_maximal_length) +function audio_onprocess_notused(e) { + //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js + if (audio_received.length === 0) { + add_problem("audio underrun"); + return; + } + output = e.outputBuffer.getChannelData(0); + int_buffer = audio_received[0]; + read_remain = audio_buffer_size; + //audio_buffer_maximal_length=120; + + obi = 0; //output buffer index + debug_str = ""; + while (1) { + if (int_buffer.length - audio_buffer_index > read_remain) { + for (i = audio_buffer_index; i < audio_buffer_index + read_remain; i++) + output[obi++] = int_buffer[i] / 32768; + //debug_str+="added whole ibl="+int_buffer.length.toString()+" abi="+audio_buffer_index.toString()+" "+(int_buffer.length-audio_buffer_index).toString()+">"+read_remain.toString()+" obi="+obi.toString()+"\n"; + audio_buffer_index += read_remain; + break; + } + else { + for (i = audio_buffer_index; i < int_buffer.length; i++) + output[obi++] = int_buffer[i] / 32768; + read_remain -= (int_buffer.length - audio_buffer_index); + audio_buffer_current_size -= audio_received[0].length; + /*if (audio_received.length>audio_buffer_maximal_length) { add_problem("audio overrun"); audio_received.splice(0,audio_received.length-audio_buffer_maximal_length); } else*/ - audio_received.splice(0,1); - //debug_str+="added remain, remain="+read_remain.toString()+" abi="+audio_buffer_index.toString()+" alen="+int_buffer.length.toString()+" i="+i.toString()+" arecva="+audio_received.length.toString()+" obi="+obi.toString()+"\n"; - audio_buffer_index = 0; - if(audio_received.length == 0 || read_remain == 0) return; - int_buffer = audio_received[0]; - } - } - //debug_str+="obi="+obi.toString(); - //alert(debug_str); + audio_received.splice(0, 1); + //debug_str+="added remain, remain="+read_remain.toString()+" abi="+audio_buffer_index.toString()+" alen="+int_buffer.length.toString()+" i="+i.toString()+" arecva="+audio_received.length.toString()+" obi="+obi.toString()+"\n"; + audio_buffer_index = 0; + if (audio_received.length === 0 || read_remain === 0) return; + int_buffer = audio_received[0]; + } + } + //debug_str+="obi="+obi.toString(); + //alert(debug_str); } -function audio_flush_notused() -{ - if (audio_buffer_current_size>audio_buffer_maximal_length_sec*audio_context.sampleRate) - { - add_problem("audio overrun"); - console.log("audio_flush() :: size: "+audio_buffer_current_size.toString()+" allowed: "+(audio_buffer_maximal_length_sec*audio_context.sampleRate).toString()); - while (audio_buffer_current_size>audio_buffer_maximal_length_sec*audio_context.sampleRate*0.5) - { - audio_buffer_current_size-=audio_received[0].length; - audio_received.splice(0,1); - } - } +function audio_flush_notused() { + if (audio_buffer_current_size > audio_buffer_maximal_length_sec * audio_context.sampleRate) { + add_problem("audio overrun"); + console.log("audio_flush() :: size: " + audio_buffer_current_size.toString() + " allowed: " + (audio_buffer_maximal_length_sec * audio_context.sampleRate).toString()); + while (audio_buffer_current_size > audio_buffer_maximal_length_sec * audio_context.sampleRate * 0.5) { + audio_buffer_current_size -= audio_received[0].length; + audio_received.splice(0, 1); + } + } } -function webrx_set_param(what, value) -{ +function webrx_set_param(what, value) { params = {}; params[what] = value; - ws.send(JSON.stringify({"type":"dspcontrol","params":params})); + ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); } var starting_mute = false; -function parsehash() -{ - if(h=window.location.hash) - { - h.substring(1).split(",").forEach(function(x){ - harr=x.split("="); - //console.log(harr); - if(harr[0]=="mute") toggleMute(); - else if(harr[0]=="mod") starting_mod = harr[1]; - else if(harr[0]=="sql") - { - e("openwebrx-panel-squelch").value=harr[1]; - updateSquelch(); - } - else if(harr[0]=="freq") - { - console.log(parseInt(harr[1])); - console.log(center_freq); - starting_offset_frequency = parseInt(harr[1])-center_freq; - } - }); +function parsehash() { + if (h = window.location.hash) { + h.substring(1).split(",").forEach(function (x) { + harr = x.split("="); + //console.log(harr); + if (harr[0] === "mute") toggleMute(); + else if (harr[0] === "mod") starting_mod = harr[1]; + else if (harr[0] === "sql") { + e("openwebrx-panel-squelch").value = harr[1]; + updateSquelch(); + } + else if (harr[0] === "freq") { + console.log(parseInt(harr[1])); + console.log(center_freq); + starting_offset_frequency = parseInt(harr[1]) - center_freq; + } + }); - } + } } -function audio_preinit() -{ - try - { - window.AudioContext = window.AudioContext||window.webkitAudioContext; - audio_context = new AudioContext(); - } - catch(e) - { - divlog('Your browser does not support Web Audio API, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser.', 1); - return; - } - - if(audio_context.sampleRate<44100*2) - audio_buffer_size = 4096; - else if(audio_context.sampleRate>=44100*2 && audio_context.sampleRate<44100*4) - audio_buffer_size = 4096 * 2; - else if(audio_context.sampleRate>44100*4) - audio_buffer_size = 4096 * 4; +function audio_preinit() { + try { + window.AudioContext = window.AudioContext || window.webkitAudioContext; + audio_context = new AudioContext(); + } + catch (e) { + divlog('Your browser does not support Web Audio API, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser.', 1); + return; + } + + if (audio_context.sampleRate < 44100 * 2) + audio_buffer_size = 4096; + else if (audio_context.sampleRate >= 44100 * 2 && audio_context.sampleRate < 44100 * 4) + audio_buffer_size = 4096 * 2; + else if (audio_context.sampleRate > 44100 * 4) + audio_buffer_size = 4096 * 4; if (!audio_rebuffer) { - audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED); + audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size, sdrjs.REBUFFER_FIXED); audio_last_output_buffer = new Float32Array(audio_buffer_size); //we send our setup packet parsehash(); audio_calculate_resampling(audio_context.sampleRate); - audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1); + audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor, 1); } - ws.send(JSON.stringify({"type":"dspcontrol","action":"start","params":{"output_rate":audio_server_output_rate}})); -} - -function audio_init() -{ - if(is_chrome) audio_context.resume() - if(starting_mute) toggleMute(); - - if(audio_client_resampling_factor==0) return; //if failed to find a valid resampling factor... - - audio_debug_time_start=(new Date()).getTime(); - audio_debug_time_last_start=audio_debug_time_start; - audio_buffer_current_count_debug = 0; - - //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js - audio_initialized=1; // only tell on_ws_recv() not to call it again - - //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor - createjsnode_function = (audio_context.createJavaScriptNode == undefined)?audio_context.createScriptProcessor.bind(audio_context):audio_context.createJavaScriptNode.bind(audio_context); - audio_node = createjsnode_function(audio_buffer_size, 0, 1); - audio_node.onaudioprocess = audio_onprocess; - audio_node.connect(audio_context.destination); - // --- Resampling --- - //https://github.com/grantgalitz/XAudioJS/blob/master/XAudioServer.js - //audio_resampler = new Resampler(audio_received_sample_rate, audio_context.sampleRate, 1, audio_buffer_size, true); - //audio_input_buffer_size = audio_buffer_size*(audio_received_sample_rate/audio_context.sampleRate); - webrx_set_param("audio_rate",audio_context.sampleRate); //Don't try to resample //TODO remove this - - window.setInterval(audio_flush,audio_flush_interval_ms); - divlog('Web Audio API succesfully initialized, sample rate: '+audio_context.sampleRate.toString()+ " sps"); - initialize_demodulator(); - - //hide log panel in a second (if user has not hidden it yet) - window.setTimeout(function(){ - if(typeof e("openwebrx-panel-log").openwebrxHidden == "undefined" && !was_error) - { - toggle_panel("openwebrx-panel-log"); - //animate(e("openwebrx-panel-log"),"opacity","",1,0,0.9,1000,60); - //window.setTimeout(function(){toggle_panel("openwebrx-panel-log");e("openwebrx-panel-log").style.opacity="1";},1200) - } - },2000); + ws.send(JSON.stringify({ + "type": "dspcontrol", + "action": "start", + "params": {"output_rate": audio_server_output_rate} + })); +} + +function audio_init() { + if (is_chrome) audio_context.resume(); + if (starting_mute) toggleMute(); + + if (audio_client_resampling_factor === 0) return; //if failed to find a valid resampling factor... + + audio_debug_time_start = (new Date()).getTime(); + audio_debug_time_last_start = audio_debug_time_start; + audio_buffer_current_count_debug = 0; + + //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js + audio_initialized = 1; // only tell on_ws_recv() not to call it again + + //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor + createjsnode_function = (audio_context.createJavaScriptNode === undefined) ? audio_context.createScriptProcessor.bind(audio_context) : audio_context.createJavaScriptNode.bind(audio_context); + audio_node = createjsnode_function(audio_buffer_size, 0, 1); + audio_node.onaudioprocess = audio_onprocess; + audio_node.connect(audio_context.destination); + // --- Resampling --- + //https://github.com/grantgalitz/XAudioJS/blob/master/XAudioServer.js + //audio_resampler = new Resampler(audio_received_sample_rate, audio_context.sampleRate, 1, audio_buffer_size, true); + //audio_input_buffer_size = audio_buffer_size*(audio_received_sample_rate/audio_context.sampleRate); + webrx_set_param("audio_rate", audio_context.sampleRate); //Don't try to resample //TODO remove this + + window.setInterval(audio_flush, audio_flush_interval_ms); + divlog('Web Audio API succesfully initialized, sample rate: ' + audio_context.sampleRate.toString() + " sps"); + initialize_demodulator(); + + //hide log panel in a second (if user has not hidden it yet) + window.setTimeout(function () { + if (typeof e("openwebrx-panel-log").openwebrxHidden === "undefined" && !was_error) { + toggle_panel("openwebrx-panel-log"); + //animate(e("openwebrx-panel-log"),"opacity","",1,0,0.9,1000,60); + //window.setTimeout(function(){toggle_panel("openwebrx-panel-log");e("openwebrx-panel-log").style.opacity="1";},1200) + } + }, 2000); } function initialize_demodulator() { - demodulator_analog_replace(starting_mod); - if(starting_offset_frequency) - { - demodulators[0].offset_frequency = starting_offset_frequency; - e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",center_freq+starting_offset_frequency,1e6,4); - demodulators[0].set(); - mkscale(); - } + demodulator_analog_replace(starting_mod); + if (starting_offset_frequency) { + demodulators[0].offset_frequency = starting_offset_frequency; + e("webrx-actual-freq").innerHTML = format_frequency("{x} MHz", center_freq + starting_offset_frequency, 1e6, 4); + demodulators[0].set(); + mkscale(); + } } var reconnect_timeout = false; -function on_ws_closed() -{ - try - { - audio_node.disconnect(); - } - catch (dont_care) {} - audio_initialized = 0; - if (reconnect_timeout) { - // max value: roundabout 8 and a half minutes - reconnect_timeout = Math.min(reconnect_timeout * 2, 512000); - } else { - // initial value: 1s - reconnect_timeout = 1000; - } - divlog("WebSocket has closed unexpectedly. Attempting to reconnect in " + reconnect_timeout / 1000 + " seconds...", 1); - - setTimeout(open_websocket, reconnect_timeout); -} - -function on_ws_error(event) -{ - divlog("WebSocket error.",1); +function on_ws_closed() { + try { + audio_node.disconnect(); + } + catch (dont_care) { + } + audio_initialized = 0; + if (reconnect_timeout) { + // max value: roundabout 8 and a half minutes + reconnect_timeout = Math.min(reconnect_timeout * 2, 512000); + } else { + // initial value: 1s + reconnect_timeout = 1000; + } + divlog("WebSocket has closed unexpectedly. Attempting to reconnect in " + reconnect_timeout / 1000 + " seconds...", 1); + + setTimeout(open_websocket, reconnect_timeout); } -String.prototype.startswith=function(str){ return this.indexOf(str) == 0; }; //http://stackoverflow.com/questions/646628/how-to-check-if-a-string-startswith-another-string +function on_ws_error(event) { + divlog("WebSocket error.", 1); +} -function open_websocket() -{ - var protocol = 'ws'; - if (window.location.toString().startsWith('https://')) { - protocol = 'wss'; - } - - ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; //guess automatically -> now default behaviour - if (!("WebSocket" in window)) - divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser."); - ws = new WebSocket(ws_url); - ws.onopen = on_ws_opened; - ws.onmessage = on_ws_recv; - ws.onclose = on_ws_closed; - ws.binaryType = "arraybuffer"; - window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript - ws.onclose = function () {}; - ws.close(); - }; - ws.onerror = on_ws_error; -} - -function waterfall_mkcolor(db_value, waterfall_colors_arg) -{ - if(typeof waterfall_colors_arg === 'undefined') waterfall_colors_arg = waterfall_colors; - if(db_valuewaterfall_max_level) db_value=waterfall_max_level; - full_scale=waterfall_max_level-waterfall_min_level; - relative_value=db_value-waterfall_min_level; - value_percent=relative_value/full_scale; - percent_for_one_color=1/(waterfall_colors_arg.length-1); - index=Math.floor(value_percent/percent_for_one_color); - remain=(value_percent-percent_for_one_color*index)/percent_for_one_color; - return color_between(waterfall_colors_arg[index+1],waterfall_colors_arg[index],remain); -} - -function color_between(first, second, percent) -{ - output=0; - for(i=0;i<4;i++) - { - add = ((((first&(0xff<<(i*8)))>>>0)*percent) + (((second&(0xff<<(i*8)))>>>0)*(1-percent))) & (0xff<<(i*8)); - output |= add>>>0; - } - return output>>>0; +String.prototype.startswith = function (str) { + return this.indexOf(str) === 0; +}; //http://stackoverflow.com/questions/646628/how-to-check-if-a-string-startswith-another-string + +function open_websocket() { + var protocol = 'ws'; + if (window.location.toString().startsWith('https://')) { + protocol = 'wss'; + } + + ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; //guess automatically -> now default behaviour + if (!("WebSocket" in window)) + divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser."); + ws = new WebSocket(ws_url); + ws.onopen = on_ws_opened; + ws.onmessage = on_ws_recv; + ws.onclose = on_ws_closed; + ws.binaryType = "arraybuffer"; + window.onbeforeunload = function () { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript + ws.onclose = function () { + }; + ws.close(); + }; + ws.onerror = on_ws_error; +} + +function waterfall_mkcolor(db_value, waterfall_colors_arg) { + if (typeof waterfall_colors_arg === 'undefined') waterfall_colors_arg = waterfall_colors; + if (db_value < waterfall_min_level) db_value = waterfall_min_level; + if (db_value > waterfall_max_level) db_value = waterfall_max_level; + full_scale = waterfall_max_level - waterfall_min_level; + relative_value = db_value - waterfall_min_level; + value_percent = relative_value / full_scale; + percent_for_one_color = 1 / (waterfall_colors_arg.length - 1); + index = Math.floor(value_percent / percent_for_one_color); + remain = (value_percent - percent_for_one_color * index) / percent_for_one_color; + return color_between(waterfall_colors_arg[index + 1], waterfall_colors_arg[index], remain); +} + +function color_between(first, second, percent) { + output = 0; + for (i = 0; i < 4; i++) { + add = ((((first & (0xff << (i * 8))) >>> 0) * percent) + (((second & (0xff << (i * 8))) >>> 0) * (1 - percent))) & (0xff << (i * 8)); + output |= add >>> 0; + } + return output >>> 0; } @@ -2030,20 +1972,19 @@ var canvas_default_height = 200; var canvas_container; var canvas_phantom; -function add_canvas() -{ - var new_canvas = document.createElement("canvas"); - new_canvas.width=fft_size; - new_canvas.height=canvas_default_height; - canvas_actual_line=canvas_default_height-1; - new_canvas.style.width=(canvas_container.clientWidth*zoom_levels[zoom_level]).toString()+"px"; - new_canvas.style.left=zoom_offset_px.toString()+"px"; - new_canvas.style.height=canvas_default_height.toString()+"px"; - new_canvas.openwebrx_top=(-canvas_default_height+1); - new_canvas.style.top=new_canvas.openwebrx_top.toString()+"px"; - canvas_context = new_canvas.getContext("2d"); - canvas_container.appendChild(new_canvas); - canvases.push(new_canvas); +function add_canvas() { + var new_canvas = document.createElement("canvas"); + new_canvas.width = fft_size; + new_canvas.height = canvas_default_height; + canvas_actual_line = canvas_default_height - 1; + new_canvas.style.width = (canvas_container.clientWidth * zoom_levels[zoom_level]).toString() + "px"; + new_canvas.style.left = zoom_offset_px.toString() + "px"; + new_canvas.style.height = canvas_default_height.toString() + "px"; + new_canvas.openwebrx_top = (-canvas_default_height + 1); + new_canvas.style.top = new_canvas.openwebrx_top.toString() + "px"; + canvas_context = new_canvas.getContext("2d"); + canvas_container.appendChild(new_canvas); + canvases.push(new_canvas); while (canvas_container && canvas_container.clientHeight + canvas_default_height * 2 < canvases.length * canvas_default_height) { var c = canvases.shift(); if (!c) break; @@ -2052,806 +1993,673 @@ function add_canvas() } -function init_canvas_container() -{ - canvas_container=e("webrx-canvas-container"); - mathbox_container=e("openwebrx-mathbox-container"); - canvas_container.addEventListener("mouseleave",canvas_container_mouseleave, false); - canvas_container.addEventListener("mousemove", canvas_mousemove, false); - canvas_container.addEventListener("mouseup", canvas_mouseup, false); - canvas_container.addEventListener("mousedown", canvas_mousedown, false); - canvas_container.addEventListener("wheel",canvas_mousewheel, false); - var frequency_container = e("openwebrx-frequency-container"); - frequency_container.addEventListener("wheel",canvas_mousewheel, false); - add_canvas(); +function init_canvas_container() { + canvas_container = e("webrx-canvas-container"); + mathbox_container = e("openwebrx-mathbox-container"); + canvas_container.addEventListener("mouseleave", canvas_container_mouseleave, false); + canvas_container.addEventListener("mousemove", canvas_mousemove, false); + canvas_container.addEventListener("mouseup", canvas_mouseup, false); + canvas_container.addEventListener("mousedown", canvas_mousedown, false); + canvas_container.addEventListener("wheel", canvas_mousewheel, false); + var frequency_container = e("openwebrx-frequency-container"); + frequency_container.addEventListener("wheel", canvas_mousewheel, false); + add_canvas(); } -canvas_maxshift=0; +canvas_maxshift = 0; -function shift_canvases() -{ - canvases.forEach(function(p) - { - p.style.top=(p.openwebrx_top++).toString()+"px"; - }); - canvas_maxshift++; +function shift_canvases() { + canvases.forEach(function (p) { + p.style.top = (p.openwebrx_top++).toString() + "px"; + }); + canvas_maxshift++; +} + +function resize_canvases(zoom) { + if (typeof zoom === "undefined") zoom = false; + if (!zoom) mkzoomlevels(); + zoom_calc(); + new_width = (canvas_container.clientWidth * zoom_levels[zoom_level]).toString() + "px"; + var zoom_value = zoom_offset_px.toString() + "px"; + canvases.forEach(function (p) { + p.style.width = new_width; + p.style.left = zoom_value; + }); } -function resize_canvases(zoom) -{ - if(typeof zoom == "undefined") zoom=false; - if(!zoom) mkzoomlevels(); - zoom_calc(); - new_width=(canvas_container.clientWidth*zoom_levels[zoom_level]).toString()+"px"; - var zoom_value=zoom_offset_px.toString()+"px"; - canvases.forEach(function(p) - { - p.style.width=new_width; - p.style.left=zoom_value; - }); -} - -function waterfall_init() -{ - init_canvas_container(); - resize_waterfall_container(false); /* then */ resize_canvases(); - scale_setup(); - mkzoomlevels(); - waterfall_setup_done=1; +function waterfall_init() { + init_canvas_container(); + resize_waterfall_container(false); + /* then */ + resize_canvases(); + scale_setup(); + mkzoomlevels(); + waterfall_setup_done = 1; } -var waterfall_dont_scale=0; - -var mathbox_shift = function() -{ - if(mathbox_data_current_depth < mathbox_data_max_depth) mathbox_data_current_depth++; - if(mathbox_data_index+1>=mathbox_data_max_depth) mathbox_data_index = 0; - else mathbox_data_index++; - mathbox_data_global_index++; -} +var waterfall_dont_scale = 0; -var mathbox_clear_data = function() -{ - mathbox_data_index = 50; - mathbox_data_current_depth = 0; -} +var mathbox_shift = function () { + if (mathbox_data_current_depth < mathbox_data_max_depth) mathbox_data_current_depth++; + if (mathbox_data_index + 1 >= mathbox_data_max_depth) mathbox_data_index = 0; + else mathbox_data_index++; + mathbox_data_global_index++; +}; -//var mathbox_get_data_line = function(x) //x counts from 0 to mathbox_data_current_depth -//{ -// return (mathbox_data_max_depth + mathbox_data_index - mathbox_data_current_depth + x - 1) % mathbox_data_max_depth; -//} -// -//var mathbox_data_index_valid = function(x) //x counts from 0 to mathbox_data_current_depth -//{ -// return xmathbox_data_max_depth-mathbox_data_current_depth; -} +var mathbox_data_index_valid = function (x) { + return x > mathbox_data_max_depth - mathbox_data_current_depth; +}; +function waterfall_add(data) { + if (!waterfall_setup_done) return; + var w = fft_size; -function waterfall_add(data) -{ - if(!waterfall_setup_done) return; - var w=fft_size; - - if(waterfall_measure_minmax) waterfall_measure_minmax_do(data); - if(waterfall_measure_minmax_now) { - waterfall_measure_minmax_do(data); - waterfall_measure_minmax_now=false; - waterfallColorsAuto(); - } - - //waterfall_shift(); - // ==== do scaling if required ==== - /*if(waterfall_dont_scale) - { - scaled=data; - for(i=scaled.length;i1) - { - scaled[i]=data[j]*(remain/pixel_per_point)+data[j+1]*((1-remain)/pixel_per_point); - remain--; - } - else - { - j++; - scaled[i]=data[j]*(remain/pixel_per_point)+data[j+1]*((1-remain)/pixel_per_point); - remain=pixel_per_point-(1-remain); - } - } + if (waterfall_measure_minmax) waterfall_measure_minmax_do(data); + if (waterfall_measure_minmax_now) { + waterfall_measure_minmax_do(data); + waterfall_measure_minmax_now = false; + waterfallColorsAuto(); + } - } - else - { //make line smaller (linear decimation, moving average) - point_per_pixel=(to-from)/w; - scaled=Array(); - j=0; - remain=point_per_pixel; - last_pixel=0; - for(i=from; i1) - { - last_pixel+=data[i]; - remain--; - } - else - { - last_pixel+=data[i]*remain; - scaled[j++]=last_pixel/point_per_pixel; - last_pixel=data[i]*(1-remain); - remain=point_per_pixel-(1-remain); //? - } - } - } - } - - //Add line to waterfall image - base=(h-1)*w*4; - for(x=0;x>>0)>>((3-i)*8))&0xff; - }*/ - - if (mathbox_mode==MATHBOX_MODES.WATERFALL) { - //Handle mathbox - for(var i=0;i>>0)>>((3-i)*8))&0xff; + oneline_image = canvas_context.createImageData(w, 1); + for (x = 0; x < w; x++) { + color = waterfall_mkcolor(data[x]); + for (i = 0; i < 4; i++) + oneline_image.data[x * 4 + i] = ((color >>> 0) >> ((3 - i) * 8)) & 0xff; } //Draw image canvas_context.putImageData(oneline_image, 0, canvas_actual_line--); shift_canvases(); - if(canvas_actual_line<0) add_canvas(); - } + if (canvas_actual_line < 0) add_canvas(); + } } -/* -function waterfall_shift() -{ - w=canvas.width; - h=canvas.height; - for(y=0; ytl.offsetLeft-20) what.style.opacity=what.style.opacity="0"; - else wet.style.opacity=wed.style.opacity="1"; - }); + [wet, wed].map(function (what) { + if (rmf(what) > tl.offsetLeft - 20) what.style.opacity = what.style.opacity = "0"; + else wet.style.opacity = wed.style.opacity = "1"; + }); } var MATHBOX_MODES = -{ - UNINITIALIZED: 0, - NONE: 1, - WATERFALL: 2, - CONSTELLATION: 3 -}; + { + UNINITIALIZED: 0, + NONE: 1, + WATERFALL: 2, + CONSTELLATION: 3 + }; var mathbox_mode = MATHBOX_MODES.UNINITIALIZED; var mathbox; var mathbox_element; -function mathbox_init() -{ - //mathbox_waterfall_history_length is defined in the config - mathbox_data_max_depth = fft_fps * mathbox_waterfall_history_length; //how many lines can the buffer store - mathbox_data_current_depth = 0; //how many lines are in the buffer currently - mathbox_data_index = 0; //the index of the last empty line / the line to be overwritten - mathbox_data = new Float32Array(fft_size * mathbox_data_max_depth); - mathbox_data_global_index = 0; - mathbox_correction_for_z = 0; - - mathbox = mathBox({ - plugins: ['core', 'controls', 'cursor', 'stats'], - controls: { klass: THREE.OrbitControls }, +function mathbox_init() { + //mathbox_waterfall_history_length is defined in the config + mathbox_data_max_depth = fft_fps * mathbox_waterfall_history_length; //how many lines can the buffer store + mathbox_data_current_depth = 0; //how many lines are in the buffer currently + mathbox_data_index = 0; //the index of the last empty line / the line to be overwritten + mathbox_data = new Float32Array(fft_size * mathbox_data_max_depth); + mathbox_data_global_index = 0; + mathbox_correction_for_z = 0; + + mathbox = mathBox({ + plugins: ['core', 'controls', 'cursor', 'stats'], + controls: {klass: THREE.OrbitControls} }); three = mathbox.three; - if(typeof three == "undefined") divlog("3D waterfall cannot be initialized because WebGL is not supported in your browser.", true); + if (typeof three === "undefined") divlog("3D waterfall cannot be initialized because WebGL is not supported in your browser.", true); three.renderer.setClearColor(new THREE.Color(0x808080), 1.0); - mathbox_container.appendChild((mathbox_element=three.renderer.domElement)); + mathbox_container.appendChild((mathbox_element = three.renderer.domElement)); view = mathbox - .set({ - scale: 1080, - focus: 3, - }) - .camera({ - proxy: true, - position: [-2, 1, 3], - }) - .cartesian({ - range: [[-1, 1], [0, 1], [0, 1]], - scale: [2, 2/3, 1], - }); + .set({ + scale: 1080, + focus: 3 + }) + .camera({ + proxy: true, + position: [-2, 1, 3] + }) + .cartesian({ + range: [[-1, 1], [0, 1], [0, 1]], + scale: [2, 2 / 3, 1] + }); view.axis({ - axis: 1, - width: 3, - color: "#fff", - }); + axis: 1, + width: 3, + color: "#fff" + }); view.axis({ - axis: 2, - width: 3, - color: "#fff", - //offset: [0, 0, 0], - }); + axis: 2, + width: 3, + color: "#fff" + //offset: [0, 0, 0], + }); view.axis({ - axis: 3, - width: 3, - color: "#fff", - }); + axis: 3, + width: 3, + color: "#fff" + }); view.grid({ - width: 2, - opacity: 0.5, - axes: [1, 3], - zOrder: 1, - color: "#fff", + width: 2, + opacity: 0.5, + axes: [1, 3], + zOrder: 1, + color: "#fff" }); - //var remap = function (v) { return Math.sqrt(.5 + .5 * v); }; - - - var remap = function(x,z,t) - { - var currentTimePos = mathbox_data_global_index/(fft_fps*1.0); - var realZAdd = (-(t-currentTimePos)/mathbox_waterfall_history_length); - var zAdd = realZAdd - mathbox_correction_for_z; - if(zAdd<-0.2 || zAdd>0.2) { mathbox_correction_for_z = realZAdd; } - - var xIndex = Math.trunc(((x+1)/2.0)*fft_size); //x: frequency - var zIndex = Math.trunc(z*(mathbox_data_max_depth-1)); //z: time - var realZIndex = mathbox_get_data_line(zIndex); - if(!mathbox_data_index_valid(zIndex)) return {y: undefined, dBValue: undefined, zAdd: 0 }; - //if(realZIndex>=(mathbox_data_max_depth-1)) console.log("realZIndexundef", realZIndex, zIndex); - var index = Math.trunc(xIndex + realZIndex * fft_size); - /*if(mathbox_data[index]==undefined) console.log("Undef", index, mathbox_data.length, zIndex, - realZIndex, mathbox_data_max_depth, - mathbox_data_current_depth, mathbox_data_index);*/ - var dBValue = mathbox_data[index]; - //y=1; - if(dBValue>waterfall_max_level) y = 1; - else if(dBValue 0.2) { + mathbox_correction_for_z = realZAdd; + } + + var xIndex = Math.trunc(((x + 1) / 2.0) * fft_size); //x: frequency + var zIndex = Math.trunc(z * (mathbox_data_max_depth - 1)); //z: time + var realZIndex = mathbox_get_data_line(zIndex); + if (!mathbox_data_index_valid(zIndex)) return {y: undefined, dBValue: undefined, zAdd: 0}; + var index = Math.trunc(xIndex + realZIndex * fft_size); + var dBValue = mathbox_data[index]; + if (dBValue > waterfall_max_level) y = 1; + else if (dBValue < waterfall_min_level) y = 0; + else y = (dBValue - waterfall_min_level) / (waterfall_max_level - waterfall_min_level); + mathbox_dbg = {dbv: dBValue, indexval: index, mbd: mathbox_data.length, yval: y}; + if (!y) y = 0; + return {y: y, dBValue: dBValue, zAdd: zAdd}; + }; var points = view.area({ - expr: function (emit, x, z, i, j, t) { - var y; - remapResult=remap(x,z,t); - if((y=remapResult.y)==undefined) return; - emit(x, y, z+remapResult.zAdd); - }, - width: mathbox_waterfall_frequency_resolution, - height: mathbox_data_max_depth - 1, - channels: 3, - axes: [1, 3], + expr: function (emit, x, z, i, j, t) { + var y; + remapResult = remap(x, z, t); + if ((y = remapResult.y) === undefined) return; + emit(x, y, z + remapResult.zAdd); + }, + width: mathbox_waterfall_frequency_resolution, + height: mathbox_data_max_depth - 1, + channels: 3, + axes: [1, 3] }); var colors = view.area({ - expr: function (emit, x, z, i, j, t) { - var dBValue; - if((dBValue=remap(x,z,t).dBValue)==undefined) return; - var color=waterfall_mkcolor(dBValue, mathbox_waterfall_colors); - var b = (color&0xff)/255.0; - var g = ((color&0xff00)>>8)/255.0; - var r = ((color&0xff0000)>>16)/255.0; - emit(r, g, b, 1.0); - }, - width: mathbox_waterfall_frequency_resolution, - height: mathbox_data_max_depth - 1, - channels: 4, - axes: [1, 3], + expr: function (emit, x, z, i, j, t) { + var dBValue; + if ((dBValue = remap(x, z, t).dBValue) === undefined) return; + var color = waterfall_mkcolor(dBValue, mathbox_waterfall_colors); + var b = (color & 0xff) / 255.0; + var g = ((color & 0xff00) >> 8) / 255.0; + var r = ((color & 0xff0000) >> 16) / 255.0; + emit(r, g, b, 1.0); + }, + width: mathbox_waterfall_frequency_resolution, + height: mathbox_data_max_depth - 1, + channels: 4, + axes: [1, 3] }); view.surface({ - shaded: true, - points: '<<', - colors: '<', - color: 0xFFFFFF, + shaded: true, + points: '<<', + colors: '<', + color: 0xFFFFFF }); view.surface({ - fill: false, - lineX: false, - lineY: false, - points: '<<', - colors: '<', - color: 0xFFFFFF, - width: 2, - blending: 'add', - opacity: .25, - zBias: 5, + fill: false, + lineX: false, + lineY: false, + points: '<<', + colors: '<', + color: 0xFFFFFF, + width: 2, + blending: 'add', + opacity: .25, + zBias: 5 }); - mathbox_mode = MATHBOX_MODES.NONE; - - //mathbox_element.style.width="100%"; - //mathbox_element.style.height="100%"; + mathbox_mode = MATHBOX_MODES.NONE; } -function mathbox_toggle() -{ +function mathbox_toggle() { - if(mathbox_mode == MATHBOX_MODES.UNINITIALIZED) mathbox_init(); - mathbox_mode = (mathbox_mode == MATHBOX_MODES.NONE) ? MATHBOX_MODES.WATERFALL : MATHBOX_MODES.NONE; - mathbox_container.style.display = (mathbox_mode == MATHBOX_MODES.WATERFALL) ? "block" : "none"; - mathbox_clear_data(); - waterfall_clear(); + if (mathbox_mode === MATHBOX_MODES.UNINITIALIZED) mathbox_init(); + mathbox_mode = (mathbox_mode === MATHBOX_MODES.NONE) ? MATHBOX_MODES.WATERFALL : MATHBOX_MODES.NONE; + mathbox_container.style.display = (mathbox_mode === MATHBOX_MODES.WATERFALL) ? "block" : "none"; + mathbox_clear_data(); + waterfall_clear(); } -function waterfall_clear() -{ - while(canvases.length) //delete all canvases - { - var x=canvases.shift(); - x.parentNode.removeChild(x); - delete x; - } - add_canvas(); +function waterfall_clear() { + while (canvases.length) //delete all canvases + { + var x = canvases.shift(); + x.parentNode.removeChild(x); + delete x; + } + add_canvas(); } -function openwebrx_resize() -{ - resize_canvases(); - resize_waterfall_container(true); - resize_scale(); - check_top_bar_congestion(); +function openwebrx_resize() { + resize_canvases(); + resize_waterfall_container(true); + resize_scale(); + check_top_bar_congestion(); } -function init_header() -{ - $('#openwebrx-main-buttons li[data-toggle-panel]').click(function() { +function init_header() { + $('#openwebrx-main-buttons').find('li[data-toggle-panel]').click(function () { toggle_panel($(this).data('toggle-panel')); }); } var bookmarks; -function openwebrx_init() -{ - if(ios||is_chrome) e("openwebrx-big-grey").style.display="table-cell"; - (opb=e("openwebrx-play-button-text")).style.marginTop=(window.innerHeight/2-opb.clientHeight/2).toString()+"px"; - init_rx_photo(); - open_websocket(); +function openwebrx_init() { + if (ios || is_chrome) e("openwebrx-big-grey").style.display = "table-cell"; + (opb = e("openwebrx-play-button-text")).style.marginTop = (window.innerHeight / 2 - opb.clientHeight / 2).toString() + "px"; + init_rx_photo(); + open_websocket(); secondary_demod_init(); digimodes_init(); - place_panels(first_show_panel); - window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); - window.addEventListener("resize",openwebrx_resize); - check_top_bar_congestion(); - init_header(); - bookmarks = new BookmarkBar(); + place_panels(first_show_panel); + window.setTimeout(function () { + window.setInterval(debug_audio, 1000); + }, 1000); + window.addEventListener("resize", openwebrx_resize); + check_top_bar_congestion(); + init_header(); + bookmarks = new BookmarkBar(); - //Synchronise volume with slider - updateVolume(); + //Synchronise volume with slider + updateVolume(); } function digimodes_init() { - $(".openwebrx-meta-panel").each(function(_, p){ + $(".openwebrx-meta-panel").each(function (_, p) { p.openwebrxHidden = true; }); // initialze DMR timeslot muting - $('.openwebrx-dmr-timeslot-panel').click(function(e) { + $('.openwebrx-dmr-timeslot-panel').click(function (e) { $(e.currentTarget).toggleClass("muted"); update_dmr_timeslot_filtering(); }); } function update_dmr_timeslot_filtering() { - var filter = $('.openwebrx-dmr-timeslot-panel').map(function(index, el){ + var filter = $('.openwebrx-dmr-timeslot-panel').map(function (index, el) { return (!$(el).hasClass("muted")) << index; - }).toArray().reduce(function(acc, v){ + }).toArray().reduce(function (acc, v) { return acc | v; }, 0); webrx_set_param("dmr_filter", filter); } -function iosPlayButtonClick() -{ - //On iOS, we can only start audio from a click or touch event. - audio_init(); - e("openwebrx-big-grey").style.opacity=0; - window.setTimeout(function(){ e("openwebrx-big-grey").style.display="none"; },1100); - audio_allowed = 1; +function iosPlayButtonClick() { + //On iOS, we can only start audio from a click or touch event. + audio_init(); + e("openwebrx-big-grey").style.opacity = 0; + window.setTimeout(function () { + e("openwebrx-big-grey").style.display = "none"; + }, 1100); + audio_allowed = 1; } -/* -window.setInterval(function(){ - sum=0; - for(i=0;i=(c=c.charCodeAt(0)+13)?c:c-26);}); - window.location.href="mailto:"+what; -}*/ +var rt = function (s, n) { + return s.replace(/[a-zA-Z]/g, function (c) { + return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + n) ? c : c - 26); + }); +}; +var irt = function (s, n) { + return s.replace(/[a-zA-Z]/g, function (c) { + return String.fromCharCode((c >= "a" ? 97 : 65) <= (c = c.charCodeAt(0) - n) ? c : c + 26); + }); +}; +var sendmail2 = function (s) { + window.location.href = "mailto:" + irt(s.replace("=", String.fromCharCode(0100)).replace("$", "."), 8); +}; -var rt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c<="Z"?90:122)>=(c=c.charCodeAt(0)+n)?c:c-26);});} -var irt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c>="a"?97:65)<=(c=c.charCodeAt(0)-n)?c:c+26);});} -var sendmail2 = function (s) { window.location.href="mailto:"+irt(s.replace("=",String.fromCharCode(0100)).replace("$","."),8); } +var audio_debug_time_start = 0; +var audio_debug_time_last_start = 0; -var audio_debug_time_start=0; -var audio_debug_time_last_start=0; +function debug_audio() { + if (audio_debug_time_start === 0) return; //audio_init has not been called + time_now = (new Date()).getTime(); + audio_debug_time_since_last_call = (time_now - audio_debug_time_last_start) / 1000; + audio_debug_time_last_start = time_now; //now + audio_debug_time_taken = (time_now - audio_debug_time_start) / 1000; + kbps_mult = (audio_compression === "adpcm") ? 8 : 16; + //e("openwebrx-audio-sps").innerHTML= + // ((audio_compression=="adpcm")?"ADPCM compressed":"uncompressed")+" audio downlink:
    "+(audio_buffer_current_size_debug*kbps_mult/audio_debug_time_since_last_call).toFixed(0)+" kbps ("+ + // (audio_buffer_all_size_debug*kbps_mult/audio_debug_time_taken).toFixed(1)+" kbps avg.), feed at "+ + // ((audio_buffer_current_count_debug*audio_buffer_size)/audio_debug_time_taken).toFixed(1)+" sps output"; -function debug_audio() -{ - if(audio_debug_time_start==0) return; //audio_init has not been called - time_now=(new Date()).getTime(); - audio_debug_time_since_last_call=(time_now-audio_debug_time_last_start)/1000; - audio_debug_time_last_start=time_now; //now - audio_debug_time_taken=(time_now-audio_debug_time_start)/1000; - kbps_mult=(audio_compression=="adpcm")?8:16; - //e("openwebrx-audio-sps").innerHTML= - // ((audio_compression=="adpcm")?"ADPCM compressed":"uncompressed")+" audio downlink:
    "+(audio_buffer_current_size_debug*kbps_mult/audio_debug_time_since_last_call).toFixed(0)+" kbps ("+ - // (audio_buffer_all_size_debug*kbps_mult/audio_debug_time_taken).toFixed(1)+" kbps avg.), feed at "+ - // ((audio_buffer_current_count_debug*audio_buffer_size)/audio_debug_time_taken).toFixed(1)+" sps output"; - - var audio_speed_value=audio_buffer_current_size_debug*kbps_mult/audio_debug_time_since_last_call; - progressbar_set(e("openwebrx-bar-audio-speed"),audio_speed_value/500000,"Audio stream ["+(audio_speed_value/1000).toFixed(0)+" kbps]",false); - - var audio_output_value=(audio_buffer_current_count_debug*audio_buffer_size)/audio_debug_time_taken; - var audio_max_rate = audio_context.sampleRate * 1.25; - var audio_min_rate = audio_context.sampleRate * .25; - progressbar_set(e("openwebrx-bar-audio-output"),audio_output_value/audio_max_rate,"Audio output ["+(audio_output_value/1000).toFixed(1)+" ksps]",audio_output_value>audio_max_rate||audio_output_value audio_max_rate || audio_output_value < audio_min_rate); + + audio_buffer_progressbar_update(); var debug_ws_time_taken = (time_now - debug_ws_time_start) / 1000; - var network_speed_value = debug_ws_data_received / debug_ws_time_taken; - progressbar_set(e("openwebrx-bar-network-speed"),network_speed_value*8/2000,"Network usage ["+(network_speed_value*8).toFixed(1)+" kbps]",false); + var network_speed_value = debug_ws_data_received / debug_ws_time_taken; + progressbar_set(e("openwebrx-bar-network-speed"), network_speed_value * 8 / 2000, "Network usage [" + (network_speed_value * 8).toFixed(1) + " kbps]", false); - audio_buffer_current_size_debug=0; + audio_buffer_current_size_debug = 0; - if(waterfall_measure_minmax) waterfall_measure_minmax_print(); + if (waterfall_measure_minmax) waterfall_measure_minmax_print(); } // ======================================================== // ======================= PANELS ======================= // ======================================================== -panel_margin=5.9; +panel_margin = 5.9; -function pop_bottommost_panel(from) -{ - min_order=parseInt(from[0].dataset.panelOrder); - min_index=0; - for(i=0;i0.5)?-90:90; - roty=0; - if(Math.random()>0.5) - { - rottemp=rotx; - rotx=roty; - roty=rottemp; - } - if(rotx!=0 && Math.random()>0.5) rotx=270; - //console.log(rotx,roty); - transformString = "perspective( 599px ) rotateX( %1deg ) rotateY( %2deg )" - .replace("%1",rotx.toString()).replace("%2",roty.toString()); - //console.log(transformString); - //console.log(panel); - panel.style.transform=transformString; - window.setTimeout(function() { - panel.style.transitionDuration="599ms"; - panel.style.transitionDelay=(Math.floor(Math.random()*500)).toString()+"ms"; - panel.style.transform="perspective( 599px ) rotateX( 0deg ) rotateY( 0deg )"; - //panel.style.transitionDuration="0ms"; - //panel.style.transitionDelay="0"; - }, 1); -} - -function place_panels(function_apply) -{ - if(function_apply == undefined) function_apply = function(x){}; - var hoffset=0; //added this because the first panel should not have such great gap below - var left_col=[]; - var right_col=[]; - var plist=e("openwebrx-panels-container").children; - for(i=0;i= 0) - { - if(c.openwebrxHidden) - { - c.style.display="none"; - continue; - } - c.style.display="block"; - c.openwebrxPanelTransparent=(!!c.dataset.panelTransparent); - newSize=c.dataset.panelSize.split(","); - if (c.dataset.panelPos=="left") { left_col.push(c); } - else if(c.dataset.panelPos=="right") { right_col.push(c); } - c.style.width=newSize[0]+"px"; - //c.style.height=newSize[1]+"px"; - if(!c.openwebrxPanelTransparent) c.style.margin=panel_margin.toString()+"px"; - else c.style.marginLeft=panel_margin.toString()+"px"; - c.openwebrxPanelWidth=parseInt(newSize[0]); - c.openwebrxPanelHeight=parseInt(newSize[1]); - } - } - - y=hoffset; //was y=0 before hoffset - while(left_col.length>0) - { - p=pop_bottommost_panel(left_col); - p.style.left="0px"; - p.style.bottom=y.toString()+"px"; - p.style.visibility="visible"; - y+=p.openwebrxPanelHeight+((p.openwebrxPanelTransparent)?0:3)*panel_margin; - if(function_apply) function_apply(p); + if (typeof on !== "undefined") { + if (item.openwebrxHidden && !on) return; + if (!item.openwebrxHidden && on) return; + } + if (item.openwebrxDisableClick) return; + item.style.transitionDuration = "599ms"; + item.style.transitionDelay = "0ms"; + if (!item.openwebrxHidden) { + window.setTimeout(function () { + item.openwebrxHidden = !item.openwebrxHidden; + place_panels(); + item.openwebrxDisableClick = false; + }, 700); + item.style.transform = "perspective( 599px ) rotateX( 90deg )"; + } + else { + item.openwebrxHidden = !item.openwebrxHidden; + place_panels(); + window.setTimeout(function () { + item.openwebrxDisableClick = false; + }, 700); + item.style.transform = "perspective( 599px ) rotateX( 0deg )"; + } + item.style.transitionDuration = "0"; + + item.openwebrxDisableClick = true; + +} + +function first_show_panel(panel) { + panel.style.transitionDuration = 0; + panel.style.transitionDelay = 0; + rotx = (Math.random() > 0.5) ? -90 : 90; + roty = 0; + if (Math.random() > 0.5) { + rottemp = rotx; + rotx = roty; + roty = rottemp; + } + if (rotx !== 0 && Math.random() > 0.5) rotx = 270; + //console.log(rotx,roty); + transformString = "perspective( 599px ) rotateX( %1deg ) rotateY( %2deg )" + .replace("%1", rotx.toString()).replace("%2", roty.toString()); + //console.log(transformString); + //console.log(panel); + panel.style.transform = transformString; + window.setTimeout(function () { + panel.style.transitionDuration = "599ms"; + panel.style.transitionDelay = (Math.floor(Math.random() * 500)).toString() + "ms"; + panel.style.transform = "perspective( 599px ) rotateX( 0deg ) rotateY( 0deg )"; + //panel.style.transitionDuration="0ms"; + //panel.style.transitionDelay="0"; + }, 1); +} + +function place_panels(function_apply) { + if (function_apply === undefined) function_apply = function (x) { + }; + var hoffset = 0; //added this because the first panel should not have such great gap below + var left_col = []; + var right_col = []; + var plist = e("openwebrx-panels-container").children; + for (i = 0; i < plist.length; i++) { + c = plist[i]; + if (c.className.indexOf("openwebrx-panel") >= 0) { + if (c.openwebrxHidden) { + c.style.display = "none"; + continue; + } + c.style.display = "block"; + c.openwebrxPanelTransparent = (!!c.dataset.panelTransparent); + newSize = c.dataset.panelSize.split(","); + if (c.dataset.panelPos === "left") { + left_col.push(c); + } + else if (c.dataset.panelPos === "right") { + right_col.push(c); + } + c.style.width = newSize[0] + "px"; + //c.style.height=newSize[1]+"px"; + if (!c.openwebrxPanelTransparent) c.style.margin = panel_margin.toString() + "px"; + else c.style.marginLeft = panel_margin.toString() + "px"; + c.openwebrxPanelWidth = parseInt(newSize[0]); + c.openwebrxPanelHeight = parseInt(newSize[1]); + } + } + + y = hoffset; //was y=0 before hoffset + while (left_col.length > 0) { + p = pop_bottommost_panel(left_col); + p.style.left = "0px"; + p.style.bottom = y.toString() + "px"; + p.style.visibility = "visible"; + y += p.openwebrxPanelHeight + ((p.openwebrxPanelTransparent) ? 0 : 3) * panel_margin; + if (function_apply) function_apply(p); //console.log(p.id, y, p.openwebrxPanelTransparent); - } - y=hoffset; - while(right_col.length>0) - { - p=pop_bottommost_panel(right_col); - p.style.right=(e("webrx-canvas-container").offsetWidth-e("webrx-canvas-container").clientWidth).toString()+"px"; //get scrollbar width - p.style.bottom=y.toString()+"px"; - p.style.visibility="visible"; - y+=p.openwebrxPanelHeight+((p.openwebrxPanelTransparent)?0:3)*panel_margin; - if(function_apply) function_apply(p); - } -} - -function progressbar_set(obj,val,text,over) -{ - if (val<0.05) val=0; - if (val>1) val=1; - var innerBar=null; - var innerText=null; - for(var i=0;i0) - $("#openwebrx-button-usb").addClass("highlighted"); - else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted"); - } - break; - default: + } + y = hoffset; + while (right_col.length > 0) { + p = pop_bottommost_panel(right_col); + p.style.right = (e("webrx-canvas-container").offsetWidth - e("webrx-canvas-container").clientWidth).toString() + "px"; //get scrollbar width + p.style.bottom = y.toString() + "px"; + p.style.visibility = "visible"; + y += p.openwebrxPanelHeight + ((p.openwebrxPanelTransparent) ? 0 : 3) * panel_margin; + if (function_apply) function_apply(p); + } +} + +function progressbar_set(obj, val, text, over) { + if (val < 0.05) val = 0; + if (val > 1) val = 1; + var innerBar = null; + var innerText = null; + for (var i = 0; i < obj.children.length; i++) { + if (obj.children[i].className === "openwebrx-progressbar-text") innerText = obj.children[i]; + else if (obj.children[i].className === "openwebrx-progressbar-bar") innerBar = obj.children[i]; + } + if (innerBar == null) return; + //.h: function animate(object,style_name,unit,from,to,accel,time_ms,fps,to_exec) + animate(innerBar, "width", "px", innerBar.clientWidth, val * obj.clientWidth, 0.7, 700, 60); + //innerBar.style.width=(val*100).toFixed(0)+"%"; + innerBar.style.backgroundColor = (over) ? "#ff6262" : "#00aba6"; + if (innerText == null) return; + innerText.innerHTML = text; +} + +function demodulator_buttons_update() { + $(".openwebrx-demodulator-button").removeClass("highlighted"); + if (secondary_demod) { + $("#openwebrx-button-dig").addClass("highlighted"); + $('#openwebrx-secondary-demod-listbox').val(secondary_demod); + } else switch (demodulators[0].subtype) { + case "lsb": + case "usb": + case "cw": + if (demodulators[0].high_cut - demodulators[0].low_cut < 300) + $("#openwebrx-button-cw").addClass("highlighted"); + else { + if (demodulators[0].high_cut < 0) + $("#openwebrx-button-lsb").addClass("highlighted"); + else if (demodulators[0].low_cut > 0) + $("#openwebrx-button-usb").addClass("highlighted"); + else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted"); + } + break; + default: var mod = demodulators[0].subtype; $("#openwebrx-button-" + mod).addClass("highlighted"); break; - } + } +} + +function demodulator_analog_replace_last() { + demodulator_analog_replace(last_analog_demodulator_subtype); } -function demodulator_analog_replace_last() { demodulator_analog_replace(last_analog_demodulator_subtype); } /* - _____ _ _ _ - | __ \(_) (_) | | - | | | |_ __ _ _ _ __ ___ ___ __| | ___ ___ + _____ _ _ _ + | __ \(_) (_) | | + | | | |_ __ _ _ _ __ ___ ___ __| | ___ ___ | | | | |/ _` | | '_ ` _ \ / _ \ / _` |/ _ \/ __| | |__| | | (_| | | | | | | | (_) | (_| | __/\__ \ |_____/|_|\__, |_|_| |_| |_|\___/ \__,_|\___||___/ - __/ | - |___/ + __/ | + |___/ */ secondary_demod = false; secondary_demod_offset_freq = 0; -function demodulator_digital_replace_last() -{ - demodulator_digital_replace(last_digital_demodulator_subtype); +function demodulator_digital_replace_last() { + demodulator_digital_replace(last_digital_demodulator_subtype); secondary_demod_listbox_update(); } -function demodulator_digital_replace(subtype) -{ - switch(subtype) - { - case "bpsk31": - case "rtty": - case "ft8": - case "jt65": - case "jt9": - case "ft4": - secondary_demod_start(subtype); - demodulator_analog_replace('usb', true); - break; - case "wspr": - secondary_demod_start(subtype); - demodulator_analog_replace('usb', true); - // WSPR only samples between 1400 and 1600 Hz - demodulators[0].low_cut = 1350; - demodulators[0].high_cut = 1650; - demodulators[0].set(); - break; - case "packet": - secondary_demod_start(subtype); - demodulator_analog_replace('nfm', true); - break; + +function demodulator_digital_replace(subtype) { + switch (subtype) { + case "bpsk31": + case "rtty": + case "ft8": + case "jt65": + case "jt9": + case "ft4": + secondary_demod_start(subtype); + demodulator_analog_replace('usb', true); + break; + case "wspr": + secondary_demod_start(subtype); + demodulator_analog_replace('usb', true); + // WSPR only samples between 1400 and 1600 Hz + demodulators[0].low_cut = 1350; + demodulators[0].high_cut = 1650; + demodulators[0].set(); + break; + case "packet": + secondary_demod_start(subtype); + demodulator_analog_replace('nfm', true); + break; } demodulator_buttons_update(); $('#openwebrx-panel-digimodes').attr('data-mode', subtype); toggle_panel("openwebrx-panel-digimodes", true); toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(subtype) >= 0); - toggle_panel("openwebrx-panel-packet-message", subtype == "packet"); + toggle_panel("openwebrx-panel-packet-message", subtype === "packet"); } -function secondary_demod_create_canvas() -{ - var new_canvas = document.createElement("canvas"); - new_canvas.width=secondary_fft_size; - new_canvas.height=$(secondary_demod_canvas_container).height(); - new_canvas.style.width=$(secondary_demod_canvas_container).width()+"px"; - new_canvas.style.height=$(secondary_demod_canvas_container).height()+"px"; +function secondary_demod_create_canvas() { + var new_canvas = document.createElement("canvas"); + new_canvas.width = secondary_fft_size; + new_canvas.height = $(secondary_demod_canvas_container).height(); + new_canvas.style.width = $(secondary_demod_canvas_container).width() + "px"; + new_canvas.style.height = $(secondary_demod_canvas_container).height() + "px"; console.log(new_canvas.width, new_canvas.height, new_canvas.style.width, new_canvas.style.height); - secondary_demod_current_canvas_actual_line=new_canvas.height-1; - $(secondary_demod_canvas_container).children().last().before(new_canvas); + secondary_demod_current_canvas_actual_line = new_canvas.height - 1; + $(secondary_demod_canvas_container).children().last().before(new_canvas); return new_canvas; } -function secondary_demod_remove_canvases() -{ +function secondary_demod_remove_canvases() { $(secondary_demod_canvas_container).children("canvas").remove(); } -function secondary_demod_init_canvases() -{ +function secondary_demod_init_canvases() { secondary_demod_remove_canvases(); - secondary_demod_canvases=[]; + secondary_demod_canvases = []; secondary_demod_canvases.push(secondary_demod_create_canvas()); secondary_demod_canvases.push(secondary_demod_create_canvas()); - secondary_demod_canvases[0].openwebrx_top=-$(secondary_demod_canvas_container).height(); - secondary_demod_canvases[1].openwebrx_top=0; + secondary_demod_canvases[0].openwebrx_top = -$(secondary_demod_canvas_container).height(); + secondary_demod_canvases[1].openwebrx_top = 0; secondary_demod_canvases_update_top(); secondary_demod_current_canvas_context = secondary_demod_canvases[0].getContext("2d"); - secondary_demod_current_canvas_actual_line=$(secondary_demod_canvas_container).height()-1; - secondary_demod_current_canvas_index=0; - secondary_demod_canvases_initialized=true; + secondary_demod_current_canvas_actual_line = $(secondary_demod_canvas_container).height() - 1; + secondary_demod_current_canvas_index = 0; + secondary_demod_canvases_initialized = true; //secondary_demod_update_channel_freq_from_event(); mkscale(); //so that the secondary waterfall zoom level will be initialized } -function secondary_demod_canvases_update_top() -{ - for(var i=0;i<2;i++) secondary_demod_canvases[i].style.top=secondary_demod_canvases[i].openwebrx_top+"px"; +function secondary_demod_canvases_update_top() { + for (var i = 0; i < 2; i++) secondary_demod_canvases[i].style.top = secondary_demod_canvases[i].openwebrx_top + "px"; } -function secondary_demod_swap_canvases() -{ +function secondary_demod_swap_canvases() { console.log("swap"); - secondary_demod_canvases[0+!secondary_demod_current_canvas_index].openwebrx_top-=$(secondary_demod_canvas_container).height()*2; - secondary_demod_current_canvas_index=0+!secondary_demod_current_canvas_index; + secondary_demod_canvases[0 + !secondary_demod_current_canvas_index].openwebrx_top -= $(secondary_demod_canvas_container).height() * 2; + secondary_demod_current_canvas_index = 0 + !secondary_demod_current_canvas_index; secondary_demod_current_canvas_context = secondary_demod_canvases[secondary_demod_current_canvas_index].getContext("2d"); - secondary_demod_current_canvas_actual_line=$(secondary_demod_canvas_container).height()-1; + secondary_demod_current_canvas_actual_line = $(secondary_demod_canvas_container).height() - 1; } -function secondary_demod_init() -{ +function secondary_demod_init() { $("#openwebrx-panel-digimodes")[0].openwebrxHidden = true; $("#openwebrx-panel-wsjt-message")[0].openwebrxHidden = true; $("#openwebrx-panel-packet-message")[0].openwebrxHidden = true; @@ -2865,90 +2673,91 @@ function secondary_demod_init() init_digital_removal_timer(); } -function secondary_demod_start(subtype) -{ +function secondary_demod_start(subtype) { secondary_demod_canvases_initialized = false; - ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":subtype}})); + ws.send(JSON.stringify({"type": "dspcontrol", "params": {"secondary_mod": subtype}})); secondary_demod = subtype; } -function secondary_demod_set() -{ - ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_offset_freq":secondary_demod_offset_freq}})); +function secondary_demod_set() { + ws.send(JSON.stringify({"type": "dspcontrol", "params": {"secondary_offset_freq": secondary_demod_offset_freq}})); } -function secondary_demod_stop() -{ - ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":false}})); +function secondary_demod_stop() { + ws.send(JSON.stringify({"type": "dspcontrol", "params": {"secondary_mod": false}})); secondary_demod = false; } -function secondary_demod_push_binary_data(x) -{ - secondary_demod_push_data(Array.from(x).map( y => (y)?"1":"0" ).join("")); -} - -function secondary_demod_push_data(x) -{ - x=Array.from(x).filter((y) => { - var c=y.charCodeAt(0); - return (c == 10 || (c >= 32 && c <= 126)); - }).map((y) => { - if(y=="&") return "&"; - if(y=="<") return "<"; - if(y==">") return ">"; - if(y==" ") return " "; +function secondary_demod_push_binary_data(x) { + secondary_demod_push_data(Array.from(x).map(function (y) { + return y ? "1" : "0" + }).join("") + ); +} + +function secondary_demod_push_data(x) { + x = Array.from(x).filter(function (y) { + var c = y.charCodeAt(0); + return (c === 10 || (c >= 32 && c <= 126)); + }).map(function (y) { + if (y === "&" + ) + return "&"; + if (y === "<") return "<"; + if (y === ">") return ">"; + if (y === " ") return " "; return y; - }).map((y) => { - if (y == "\n") return "
    "; - return ""+y+""; + }).map(function (y) { + if (y === "\n" + ) + return "
    "; + return "" + y + ""; }).join(""); $("#openwebrx-cursor-blink").before(x); } -function secondary_demod_data_clear() -{ +function secondary_demod_data_clear() { $("#openwebrx-cursor-blink").prevAll().remove(); } -function secondary_demod_close_window() -{ +function secondary_demod_close_window() { secondary_demod_stop(); toggle_panel("openwebrx-panel-digimodes", false); toggle_panel("openwebrx-panel-wsjt-message", false); toggle_panel("openwebrx-panel-packet-message", false); } -secondary_demod_fft_offset_db=30; //need to calculate that later +secondary_demod_fft_offset_db = 30; //need to calculate that later -function secondary_demod_waterfall_add(data) -{ - if(!secondary_demod) return; - var w=secondary_fft_size; - - //Add line to waterfall image - var oneline_image = secondary_demod_current_canvas_context.createImageData(w,1); - for(x=0;x>>0)>>((3-i)*8))&0xff; - } - - //Draw image - secondary_demod_current_canvas_context.putImageData(oneline_image, 0, secondary_demod_current_canvas_actual_line--); - secondary_demod_canvases.map((x)=>{x.openwebrx_top += 1;}); +function secondary_demod_waterfall_add(data) { + if (!secondary_demod) return; + var w = secondary_fft_size; + + //Add line to waterfall image + var oneline_image = secondary_demod_current_canvas_context.createImageData(w, 1); + for (x = 0; x < w; x++) { + var color = waterfall_mkcolor(data[x] + secondary_demod_fft_offset_db); + for (i = 0; i < 4; i++) oneline_image.data[x * 4 + i] = ((color >>> 0) >> ((3 - i) * 8)) & 0xff; + } + + //Draw image + secondary_demod_current_canvas_context.putImageData(oneline_image, 0, secondary_demod_current_canvas_actual_line--); + secondary_demod_canvases.map(function (x) { + x.openwebrx_top += 1; + }) + ; secondary_demod_canvases_update_top(); - if(secondary_demod_current_canvas_actual_line<0) secondary_demod_swap_canvases(); + if (secondary_demod_current_canvas_actual_line < 0) secondary_demod_swap_canvases(); } var secondary_demod_canvases_initialized = false; secondary_demod_listbox_updating = false; -function secondary_demod_listbox_changed() -{ + +function secondary_demod_listbox_changed() { if (secondary_demod_listbox_updating) return; var sdm = $("#openwebrx-secondary-demod-listbox")[0].value; - if (sdm == "none") { + if (sdm === "none") { demodulator_analog_replace_last(); } else { demodulator_digital_replace(sdm); @@ -2956,100 +2765,99 @@ function secondary_demod_listbox_changed() update_dial_button(); } -function secondary_demod_listbox_update() -{ +function secondary_demod_listbox_update() { secondary_demod_listbox_updating = true; - $("#openwebrx-secondary-demod-listbox").val((secondary_demod)?secondary_demod:"none"); + $("#openwebrx-secondary-demod-listbox").val((secondary_demod) ? secondary_demod : "none"); console.log("update"); secondary_demod_listbox_updating = false; } -secondary_demod_channel_freq=1000; -function secondary_demod_update_marker() -{ - var width = Math.max( (secondary_bw / (if_samp_rate/2)) * secondary_demod_canvas_width, 5); - var center_at = (secondary_demod_channel_freq / (if_samp_rate/2)) * secondary_demod_canvas_width + secondary_demod_canvas_left; - var left = center_at-width/2; +secondary_demod_channel_freq = 1000; + +function secondary_demod_update_marker() { + var width = Math.max((secondary_bw / (if_samp_rate / 2)) * secondary_demod_canvas_width, 5); + var center_at = (secondary_demod_channel_freq / (if_samp_rate / 2)) * secondary_demod_canvas_width + secondary_demod_canvas_left; + var left = center_at - width / 2; //console.log("sdum", width, left); - $("#openwebrx-digimode-select-channel").width(width).css("left",left+"px") + $("#openwebrx-digimode-select-channel").width(width).css("left", left + "px") } secondary_demod_waiting_for_set = false; -function secondary_demod_update_channel_freq_from_event(evt) -{ - if(typeof evt !== "undefined") - { - var relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; - secondary_demod_channel_freq=secondary_demod_low_cut + - (relativeX/$(secondary_demod_canvas_container).width()) * (secondary_demod_high_cut-secondary_demod_low_cut); + +function secondary_demod_update_channel_freq_from_event(evt) { + if (typeof evt !== "undefined") { + var relativeX = (evt.offsetX) ? evt.offsetX : evt.layerX; + secondary_demod_channel_freq = secondary_demod_low_cut + + (relativeX / $(secondary_demod_canvas_container).width()) * (secondary_demod_high_cut - secondary_demod_low_cut); } //console.log("toset:", secondary_demod_channel_freq); - if(!secondary_demod_waiting_for_set) - { + if (!secondary_demod_waiting_for_set) { secondary_demod_waiting_for_set = true; - window.setTimeout(()=>{ - ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_offset_freq":Math.floor(secondary_demod_channel_freq)}})); - //console.log("doneset:", secondary_demod_channel_freq); - secondary_demod_waiting_for_set = false; - }, 50); + window.setTimeout(function () { + ws.send(JSON.stringify({ + "type": "dspcontrol", + "params": {"secondary_offset_freq": Math.floor(secondary_demod_channel_freq)} + })); + //console.log("doneset:", secondary_demod_channel_freq); + secondary_demod_waiting_for_set = false; + }, + 50 + ) + ; } secondary_demod_update_marker(); } -secondary_demod_mousedown=false; -function secondary_demod_canvas_container_mousein() -{ - $("#openwebrx-digimode-select-channel").css("opacity","0.7"); //.css("border-width", "1px"); +secondary_demod_mousedown = false; + +function secondary_demod_canvas_container_mousein() { + $("#openwebrx-digimode-select-channel").css("opacity", "0.7"); //.css("border-width", "1px"); } -function secondary_demod_canvas_container_mouseleave() -{ - $("#openwebrx-digimode-select-channel").css("opacity","0"); +function secondary_demod_canvas_container_mouseleave() { + $("#openwebrx-digimode-select-channel").css("opacity", "0"); } -function secondary_demod_canvas_container_mousemove(evt) -{ - if(secondary_demod_mousedown) secondary_demod_update_channel_freq_from_event(evt); +function secondary_demod_canvas_container_mousemove(evt) { + if (secondary_demod_mousedown) secondary_demod_update_channel_freq_from_event(evt); } -function secondary_demod_canvas_container_mousedown(evt) -{ - if(evt.which==1) secondary_demod_mousedown=true; +function secondary_demod_canvas_container_mousedown(evt) { + if (evt.which === 1) secondary_demod_mousedown = true; } -function secondary_demod_canvas_container_mouseup(evt) -{ - if(evt.which==1) secondary_demod_mousedown=false; +function secondary_demod_canvas_container_mouseup(evt) { + if (evt.which === 1) secondary_demod_mousedown = false; secondary_demod_update_channel_freq_from_event(evt); } -function secondary_demod_waterfall_set_zoom(low_cut, high_cut) -{ - if(!secondary_demod || !secondary_demod_canvases_initialized) return; - if(low_cut<0 && high_cut<0) - { +function secondary_demod_waterfall_set_zoom(low_cut, high_cut) { + if (!secondary_demod || !secondary_demod_canvases_initialized) return; + if (low_cut < 0 && high_cut < 0) { var hctmp = high_cut; var lctmp = low_cut; low_cut = -hctmp; low_cut = -lctmp; } - else if(low_cut<0 && high_cut>0) - { - high_cut=Math.max(Math.abs(high_cut), Math.abs(low_cut)); - low_cut=0; + else if (low_cut < 0 && high_cut > 0) { + high_cut = Math.max(Math.abs(high_cut), Math.abs(low_cut)); + low_cut = 0; } secondary_demod_low_cut = low_cut; secondary_demod_high_cut = high_cut; - var shown_bw = high_cut-low_cut; - secondary_demod_canvas_width = $(secondary_demod_canvas_container).width() * (if_samp_rate/2)/shown_bw; - secondary_demod_canvas_left = -secondary_demod_canvas_width*(low_cut/(if_samp_rate/2)); + var shown_bw = high_cut - low_cut; + secondary_demod_canvas_width = $(secondary_demod_canvas_container).width() * (if_samp_rate / 2) / shown_bw; + secondary_demod_canvas_left = -secondary_demod_canvas_width * (low_cut / (if_samp_rate / 2)); //console.log("setzoom", secondary_demod_canvas_width, secondary_demod_canvas_left, low_cut, high_cut); - secondary_demod_canvases.map((x)=>{$(x).css("left",secondary_demod_canvas_left+"px").css("width",secondary_demod_canvas_width+"px");}); + secondary_demod_canvases.map(function (x) { + $(x).css("left", secondary_demod_canvas_left + "px").css("width", secondary_demod_canvas_width + "px"); + }) + ; secondary_demod_update_channel_freq_from_event(); } function sdr_profile_changed() { value = $('#openwebrx-sdr-profiles-listbox').val(); - ws.send(JSON.stringify({ type:"selectprofile", params:{ profile:value }})); + ws.send(JSON.stringify({type: "selectprofile", params: {profile: value}})); } From d53d3b7a516b2be8619f8ffa0e1efca1b0397b3e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 16 Oct 2019 17:11:09 +0200 Subject: [PATCH 0488/2616] clean up javascript as good as possible with the help of the IDE --- htdocs/css/openwebrx.css | 12 - htdocs/index.html | 2 +- htdocs/openwebrx.js | 740 +++++++++++++++------------------------ 3 files changed, 291 insertions(+), 463 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index a2ca86315..29ae34a1e 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -302,18 +302,6 @@ input[type=range]:focus::-ms-fill-upper color: #ff6262; } -#openwebrx-problems span -{ - background: #ff6262; - padding: 3px; - font-size: 8pt; - color: white; - font-weight: bold; - border-radius: 4px; - -moz-border-radius: 4px; - margin: 0px 2px 0px 2px; -} - /*#webrx-freq-show { visibility: hidden; diff --git a/htdocs/index.html b/htdocs/index.html index a7dfa10ae..30dcce3fb 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -129,7 +129,7 @@
    -
    OpenWebRX client log
    +
    OpenWebRX client log
    Author: András Retzler, HA7ILM
    You can support OpenWebRX development via PayPal!
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index a45b2e5d2..13a6c5a24 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -33,7 +33,6 @@ var center_freq; var audio_buffer_current_size_debug = 0; var audio_buffer_all_size_debug = 0; var audio_buffer_current_count_debug = 0; -var audio_buffer_current_size = 0; var fft_size; var fft_fps; var fft_compression = "none"; @@ -42,23 +41,6 @@ var audio_compression = "none"; var waterfall_setup_done = 0; var secondary_fft_size; var audio_allowed; - -/*function fade(something,from,to,time_ms,fps) -{ - something.style.opacity=from; - something.fade_i=0; - n_of_iters=time_ms/(1000/fps); - change=(to-from)/(n_of_iters-1); - - something.fade_timer=window.setInterval( - function(){ - if(something.fade_i++= parseInt(wfmax.value)) { if (!which) wfmin.value = (parseInt(wfmax.value) - 1).toString(); else wfmax.value = (parseInt(wfmin.value) + 1).toString(); @@ -208,9 +193,9 @@ function setSmeterRelativeValue(value) { var bar = e("openwebrx-smeter-bar"); var outer = e("openwebrx-smeter-outer"); bar.style.width = (outer.offsetWidth * value).toString() + "px"; - bgRed = "linear-gradient(to top, #ff5939 , #961700)"; - bgGreen = "linear-gradient(to top, #22ff2f , #008908)"; - bgYellow = "linear-gradient(to top, #fff720 , #a49f00)"; + var bgRed = "linear-gradient(to top, #ff5939 , #961700)"; + var bgGreen = "linear-gradient(to top, #22ff2f , #008908)"; + var bgYellow = "linear-gradient(to top, #fff720 , #a49f00)"; bar.style.background = (value > 0.9) ? bgRed : ((value > 0.7) ? bgYellow : bgGreen); } @@ -253,8 +238,8 @@ function animate(object, style_name, unit, from, to, accel, time_ms, fps, to_exe if (typeof to_exec === "undefined") to_exec = 0; object.style[style_name] = from.toString() + unit; object.anim_i = 0; - n_of_iters = time_ms / (1000 / fps); - change = (to - from) / (n_of_iters); + var n_of_iters = time_ms / (1000 / fps); + var change = (to - from) / (n_of_iters); if (typeof object.anim_timer !== "undefined") { window.clearInterval(object.anim_timer); } @@ -263,7 +248,8 @@ function animate(object, style_name, unit, from, to, accel, time_ms, fps, to_exe if (object.anim_i++ < n_of_iters) { if (accel === 1) object.style[style_name] = (parseFloat(object.style[style_name]) + change).toString() + unit; else { - remain = parseFloat(object.style[style_name]) - to; + var remain = parseFloat(object.style[style_name]) - to; + var new_val; if (Math.abs(remain) > 9 || unit !== "px") new_val = (to + accel * remain); else { if (Math.abs(remain) < 2) new_val = to; @@ -282,7 +268,7 @@ function animate(object, style_name, unit, from, to, accel, time_ms, fps, to_exe } function animate_to(object, style_name, unit, to, accel, time_ms, fps, to_exec) { - from = parseFloat(style_value(object, style_name)); + var from = parseFloat(style_value(object, style_name)); animate(object, style_name, unit, from, to, accel, time_ms, fps, to_exec); } @@ -293,8 +279,8 @@ function animate_to(object, style_name, unit, to, accel, time_ms, fps, to_exec) demodulators = []; -demodulator_color_index = 0; -demodulator_colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"] +var demodulator_color_index = 0; +var demodulator_colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"]; function demodulators_get_next_color() { if (demodulator_color_index >= demodulator_colors.length) demodulator_color_index = 0; @@ -308,17 +294,17 @@ function demod_envelope_draw(range, from, to, color, line) { // // A "drag range" object is returned, containing information about the draggable areas of the envelope // (beginning, ending and the line showing the offset frequency). if (typeof color === "undefined") color = "#ffff00"; //yellow - env_bounding_line_w = 5; // - env_att_w = 5; // _______ ___env_h2 in px ___|_____ - env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_ - env_h2 = 5; // |||env_att_line_w |_env_lineplus - env_lineplus = 1; // ||env_bounding_line_w - env_line_click_area = 6; + var env_bounding_line_w = 5; // + var env_att_w = 5; // _______ ___env_h2 in px ___|_____ + var env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_ + var env_h2 = 5; // |||env_att_line_w |_env_lineplus + var env_lineplus = 1; // ||env_bounding_line_w + var env_line_click_area = 6; //range=get_visible_freq_range(); - from_px = scale_px_from_freq(from, range); - to_px = scale_px_from_freq(to, range); + var from_px = scale_px_from_freq(from, range); + var to_px = scale_px_from_freq(to, range); if (to_px < from_px) /* swap'em */ { - temp_px = to_px; + var temp_px = to_px; to_px = from_px; from_px = temp_px; } @@ -352,7 +338,7 @@ function demod_envelope_draw(range, from, to, color, line) { // } if (typeof line !== "undefined") // out of screen? { - line_px = scale_px_from_freq(line, range); + var line_px = scale_px_from_freq(line, range); if (!(line_px < 0 || line_px > window.innerWidth)) { drag_ranges.line = {x1: line_px - env_line_click_area / 2, x2: line_px + env_line_click_area / 2}; drag_ranges.line_on_screen = true; @@ -365,10 +351,10 @@ function demod_envelope_draw(range, from, to, color, line) { // } function demod_envelope_where_clicked(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by demod_envelope_draw(). - in_range = function (x, range) { + var in_range = function (x, range) { return range.x1 <= x && range.x2 >= x; }; - dr = demodulator.draggable_ranges; + var dr = Demodulator.draggable_ranges; if (key_modifiers.shiftKey) { //Check first: shift + center drag emulates BFO knob @@ -386,29 +372,27 @@ function demod_envelope_where_clicked(x, drag_ranges, key_modifiers) { // Check return dr.none; //User doesn't drag the envelope for this demodulator } -//******* class demodulator ******* +//******* class Demodulator ******* // this can be used as a base class for ANY demodulator -demodulator = function (offset_frequency) { +Demodulator = function (offset_frequency) { //console.log("this too"); this.offset_frequency = offset_frequency; - this.has_audio_output = true; - this.has_text_output = false; this.envelope = {}; this.color = demodulators_get_next_color(); this.stop = function () { }; -} +}; //ranges on filter envelope that can be dragged: -demodulator.draggable_ranges = { +Demodulator.draggable_ranges = { none: 0, beginning: 1 /*from*/, ending: 2 /*to*/, anything_else: 3, bfo: 4 /*line (while holding shift)*/, pbs: 5 -} //to which parameter these correspond in demod_envelope_draw() +}; //to which parameter these correspond in demod_envelope_draw() -//******* class demodulator_default_analog ******* +//******* class Demodulator_default_analog ******* // This can be used as a base for basic audio demodulators. // It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB @@ -416,10 +400,10 @@ demodulator_response_time = 50; //in ms; if we don't limit the number of SETs sent to the server, audio will underrun (possibly output buffer is cleared on SETs in GNU Radio -function demodulator_default_analog(offset_frequency, subtype) { +function Demodulator_default_analog(offset_frequency, subtype) { //console.log("hopefully this happens"); //http://stackoverflow.com/questions/4152931/javascript-inheritance-call-super-constructor-or-use-prototype-chain - demodulator.call(this, offset_frequency); + Demodulator.call(this, offset_frequency); this.subtype = subtype; this.filter = { min_passband: 100, @@ -469,7 +453,7 @@ function demodulator_default_analog(offset_frequency, subtype) { this.doset(false); this.set_after = false; this.wait_for_timer = true; - timeout_this = this; //http://stackoverflow.com/a/2130411 + var timeout_this = this; //http://stackoverflow.com/a/2130411 window.setTimeout(function () { timeout_this.wait_for_timer = false; if (timeout_this.set_after) timeout_this.set(); @@ -481,7 +465,7 @@ function demodulator_default_analog(offset_frequency, subtype) { }; this.doset = function (first_time) { //this function sends demodulator parameters to the server - params = { + var params = { "low_cut": this.low_cut, "high_cut": this.high_cut, "offset_freq": this.offset_frequency @@ -503,38 +487,31 @@ function demodulator_default_analog(offset_frequency, subtype) { this.color, center_freq + this.parent.offset_frequency); }; - this.envelope.dragged_range = demodulator.draggable_ranges.none; + this.envelope.dragged_range = Demodulator.draggable_ranges.none; // event handlers this.envelope.drag_start = function (x, key_modifiers) { this.key_modifiers = key_modifiers; this.dragged_range = demod_envelope_where_clicked(x, this.drag_ranges, key_modifiers); - //console.log("dragged_range: "+this.dragged_range.toString()); this.drag_origin = { x: x, low_cut: this.parent.low_cut, high_cut: this.parent.high_cut, offset_frequency: this.parent.offset_frequency }; - return this.dragged_range !== demodulator.draggable_ranges.none; + return this.dragged_range !== Demodulator.draggable_ranges.none; }; this.envelope.drag_move = function (x) { - dr = demodulator.draggable_ranges; + var dr = Demodulator.draggable_ranges; + var new_value; if (this.dragged_range === dr.none) return false; // we return if user is not dragging (us) at all - freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x)); - /*if(this.dragged_range==dr.beginning||this.dragged_range==dr.ending) - { - //we don't let the passband be too small - if(this.parent.low_cut+new_freq_change<=this.parent.high_cut-this.parent.filter.min_passband) this.freq_change=new_freq_change; - else return; - } - var new_value;*/ + var freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x)); //dragging the line in the middle of the filter envelope while holding Shift does emulate //the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged //Filter passband moves in the opposite direction than dragged, hence the minus below. - minus = (this.dragged_range === dr.bfo) ? -1 : 1; + var minus = (this.dragged_range === dr.bfo) ? -1 : 1; //dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob //(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset //frequency. @@ -570,16 +547,16 @@ function demodulator_default_analog(offset_frequency, subtype) { return true; }; - this.envelope.drag_end = function (x) { //in this demodulator we've already changed values in the drag_move() function so we shouldn't do too much here. + this.envelope.drag_end = function () { //in this demodulator we've already changed values in the drag_move() function so we shouldn't do too much here. demodulator_buttons_update(); - to_return = this.dragged_range !== demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset - this.dragged_range = demodulator.draggable_ranges.none; + var to_return = this.dragged_range !== Demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset + this.dragged_range = Demodulator.draggable_ranges.none; return to_return; }; } -demodulator_default_analog.prototype = new demodulator(); +Demodulator_default_analog.prototype = new Demodulator(); function mkenvelopes(visible_range) //called from mkscale { @@ -600,8 +577,8 @@ function demodulator_add(what) { mkenvelopes(get_visible_freq_range()); } -last_analog_demodulator_subtype = 'nfm'; -last_digital_demodulator_subtype = 'bpsk31'; +var last_analog_demodulator_subtype = 'nfm'; +var last_digital_demodulator_subtype = 'bpsk31'; function demodulator_analog_replace(subtype, for_digital) { //this function should only exist until the multi-demodulator capability is added if (!(typeof for_digital !== "undefined" && for_digital && secondary_demod)) { @@ -614,7 +591,7 @@ function demodulator_analog_replace(subtype, for_digital) { //this function shou temp_offset = demodulators[0].offset_frequency; demodulator_remove(0); } - demodulator_add(new demodulator_default_analog(temp_offset, subtype)); + demodulator_add(new Demodulator_default_analog(temp_offset, subtype)); demodulator_buttons_update(); update_digitalvoice_panels("openwebrx-panel-metadata-" + subtype); } @@ -655,14 +632,12 @@ var scale_canvas_drag_params = { }; function scale_canvas_mousedown(evt) { - with (scale_canvas_drag_params) { - mouse_down = true; - drag = false; - start_x = evt.pageX; - key_modifiers.shiftKey = evt.shiftKey; - key_modifiers.altKey = evt.altKey; - key_modifiers.ctrlKey = evt.ctrlKey; - } + scale_canvas_drag_params.mouse_down = true; + scale_canvas_drag_params.drag = false; + scale_canvas_drag_params.start_x = evt.pageX; + scale_canvas_drag_params.key_modifiers.shiftKey = evt.shiftKey; + scale_canvas_drag_params.key_modifiers.altKey = evt.altKey; + scale_canvas_drag_params.key_modifiers.ctrlKey = evt.ctrlKey; evt.preventDefault(); } @@ -672,18 +647,19 @@ function scale_offset_freq_from_px(x, visible_range) { } function scale_canvas_mousemove(evt) { - var event_handled; + var event_handled = false; + var i; if (scale_canvas_drag_params.mouse_down && !scale_canvas_drag_params.drag && Math.abs(evt.pageX - scale_canvas_drag_params.start_x) > canvas_drag_min_delta) //we can use the main drag_min_delta thing of the main canvas { scale_canvas_drag_params.drag = true; //call the drag_start for all demodulators (and they will decide if they're dragged, based on X coordinate) - for (var i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_start(evt.pageX, scale_canvas_drag_params.key_modifiers); + for (i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_start(evt.pageX, scale_canvas_drag_params.key_modifiers); scale_canvas.style.cursor = "move"; } else if (scale_canvas_drag_params.drag) { //call the drag_move for all demodulators (and they will decide if they're dragged) - for (var i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_move(evt.pageX); + for (i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_move(evt.pageX); if (!event_handled) demodulator_set_offset_frequency(0, scale_offset_freq_from_px(evt.pageX)); } @@ -699,7 +675,7 @@ function scale_canvas_end_drag(x) { scale_canvas_drag_params.drag = false; scale_canvas_drag_params.mouse_down = false; var event_handled = false; - for (var i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_end(x); + for (var i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_end(); if (!event_handled) demodulator_set_offset_frequency(0, scale_offset_freq_from_px(x)); } @@ -712,8 +688,8 @@ function scale_px_from_freq(f, range) { } function get_visible_freq_range() { - out = {}; - fcalc = function (x) { + var out = {}; + var fcalc = function (x) { return Math.round(((-zoom_offset_px + x) / canvases[0].clientWidth) * bandwidth) + (center_freq - bandwidth / 2); }; out.start = fcalc(0); @@ -793,8 +769,8 @@ var scale_min_space_bw_texts = 50; var scale_min_space_bw_small_markers = 7; function get_scale_mark_spacing(range) { - out = {}; - fcalc = function (freq) { + var out = {}; + var fcalc = function (freq) { out.numlarge = (range.bw / freq); out.large = canvas_container.clientWidth / out.numlarge; //distance between large markers (these have text) out.ratio = 5; //(ratio-1) small markers exist per large marker @@ -807,8 +783,8 @@ function get_scale_mark_spacing(range) { out.smallbw = freq / out.ratio; return true; }; - for (i = scale_markers_levels.length - 1; i >= 0; i--) { - mp = scale_markers_levels[i]; + for (var i = scale_markers_levels.length - 1; i >= 0; i--) { + var mp = scale_markers_levels[i]; if (!fcalc(mp.large_marker_per_hz)) continue; //console.log(mp.large_marker_per_hz); //console.log(out); @@ -818,6 +794,8 @@ function get_scale_mark_spacing(range) { return out; } +var range; + function mkscale() { //clear the lower part of the canvas (where frequency scale resides; the upper part is used by filter envelopes): range = get_visible_freq_range(); @@ -827,17 +805,18 @@ function mkscale() { scale_ctx.font = "bold 11px sans-serif"; scale_ctx.textBaseline = "top"; scale_ctx.fillStyle = "#fff"; - spacing = get_scale_mark_spacing(range); + var spacing = get_scale_mark_spacing(range); //console.log(spacing); - marker_hz = Math.ceil(range.start / spacing.smallbw) * spacing.smallbw; - text_h_pos = 22 + 10 + ((is_firefox) ? 3 : 0); - var text_to_draw; + var marker_hz = Math.ceil(range.start / spacing.smallbw) * spacing.smallbw; + var text_h_pos = 22 + 10 + ((is_firefox) ? 3 : 0); + var text_to_draw = ''; var ftext = function (f) { text_to_draw = format_frequency(spacing.params.format, f, spacing.params.pre_divide, spacing.params.decimals); }; var last_large; + var x; for (; ;) { - var x = scale_px_from_freq(marker_hz, range); + x = scale_px_from_freq(marker_hz, range); if (x > window.innerWidth) break; scale_ctx.beginPath(); scale_ctx.moveTo(x, 22); @@ -875,7 +854,7 @@ function mkscale() { // on the left side scale_ctx.textAlign = "center"; var f = first_large - spacing.smallbw * spacing.ratio; - var x = scale_px_from_freq(f, range); + x = scale_px_from_freq(f, range); ftext(f); var w = scale_ctx.measureText(text_to_draw).width; if (x + w / 2 > 0) scale_ctx.fillText(text_to_draw, x, 22 + 10); @@ -889,7 +868,7 @@ function mkscale() { } function resize_scale() { - ratio = window.devicePixelRatio || 1; + var ratio = window.devicePixelRatio || 1; var w = window.innerWidth; var h = 47; scale_canvas.style.width = w + "px"; @@ -904,7 +883,7 @@ function resize_scale() { } function canvas_get_freq_offset(relativeX) { - rel = (relativeX / canvases[0].clientWidth); + var rel = (relativeX / canvases[0].clientWidth); return Math.round((bandwidth * rel) - (bandwidth / 2)); } @@ -912,14 +891,10 @@ function canvas_get_frequency(relativeX) { return center_freq + canvas_get_freq_offset(relativeX); } -/*function canvas_format_frequency(relativeX) -{ - return (canvas_get_frequency(relativeX)/1e6).toFixed(3)+" MHz"; -}*/ function format_frequency(format, freq_hz, pre_divide, decimals) { - out = format.replace("{x}", (freq_hz / pre_divide).toFixed(decimals)); - at = out.indexOf(".") + 4; + var out = format.replace("{x}", (freq_hz / pre_divide).toFixed(decimals)); + var at = out.indexOf(".") + 4; while (decimals > 3) { out = out.substr(0, at) + "," + out.substr(at); at += 4; @@ -928,9 +903,13 @@ function format_frequency(format, freq_hz, pre_divide, decimals) { return out; } -canvas_drag = false; -canvas_drag_min_delta = 1; -canvas_mouse_down = false; +var canvas_drag = false; +var canvas_drag_min_delta = 1; +var canvas_mouse_down = false; +var canvas_drag_last_x; +var canvas_drag_last_y; +var canvas_drag_start_x; +var canvas_drag_start_y; function canvas_mousedown(evt) { canvas_mouse_down = true; @@ -942,7 +921,7 @@ function canvas_mousedown(evt) { function canvas_mousemove(evt) { if (!waterfall_setup_done) return; - relativeX = get_relative_x(evt); + var relativeX = get_relative_x(evt); if (canvas_mouse_down) { if (!canvas_drag && Math.abs(evt.pageX - canvas_drag_start_x) > canvas_drag_min_delta) { canvas_drag = true; @@ -950,7 +929,6 @@ function canvas_mousemove(evt) { } if (canvas_drag) { var deltaX = canvas_drag_last_x - evt.pageX; - var deltaY = canvas_drag_last_y - evt.pageY; var dpx = range.hps * deltaX; if ( !(zoom_center_rel + dpx > (bandwidth / 2 - canvas_container.clientWidth * (1 - zoom_center_where) * range.hps)) && @@ -968,13 +946,13 @@ function canvas_mousemove(evt) { else e("webrx-mouse-freq").innerHTML = format_frequency("{x} MHz", canvas_get_frequency(relativeX), 1e6, 4); } -function canvas_container_mouseleave(evt) { +function canvas_container_mouseleave() { canvas_end_drag(); } function canvas_mouseup(evt) { if (!waterfall_setup_done) return; - relativeX = get_relative_x(evt); + var relativeX = get_relative_x(evt); if (!canvas_drag) { demodulator_set_offset_frequency(0, canvas_get_freq_offset(relativeX)); @@ -1011,30 +989,29 @@ function canvas_mousewheel(evt) { } -zoom_max_level_hps = 33; //Hz/pixel -zoom_levels_count = 14; +var zoom_max_level_hps = 33; //Hz/pixel +var zoom_levels_count = 14; function get_zoom_coeff_from_hps(hps) { var shown_bw = (window.innerWidth * hps); return bandwidth / shown_bw; } -zoom_levels = [1]; -zoom_level = 0; -zoom_freq = 0; -zoom_offset_px = 0; -zoom_center_rel = 0; -zoom_center_where = 0; +var zoom_levels = [1]; +var zoom_level = 0; +var zoom_offset_px = 0; +var zoom_center_rel = 0; +var zoom_center_where = 0; -smeter_level = 0; +var smeter_level = 0; function mkzoomlevels() { zoom_levels = [1]; - maxc = get_zoom_coeff_from_hps(zoom_max_level_hps); + var maxc = get_zoom_coeff_from_hps(zoom_max_level_hps); if (maxc < 1) return; // logarithmic interpolation - zoom_ratio = Math.pow(maxc, 1 / zoom_levels_count); - for (i = 1; i < zoom_levels_count; i++) + var zoom_ratio = Math.pow(maxc, 1 / zoom_levels_count); + for (var i = 1; i < zoom_levels_count; i++) zoom_levels.push(Math.pow(zoom_ratio, i)); } @@ -1059,14 +1036,13 @@ function zoom_set(level) { //zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+canvas_container.clientWidth/2); //zoom to screen center instead of demod envelope zoom_center_rel = demodulators[0].offset_frequency; zoom_center_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack - console.log(zoom_center_where, zoom_center_rel, -canvases[0].offsetLeft + canvas_container.clientWidth / 2); resize_canvases(true); mkscale(); bookmarks.position(); } function zoom_calc() { - winsize = canvas_container.clientWidth; + var winsize = canvas_container.clientWidth; var canvases_new_width = winsize * zoom_levels[zoom_level]; zoom_offset_px = -((canvases_new_width * (0.5 + zoom_center_rel / bandwidth)) - (winsize * zoom_center_where)); if (zoom_offset_px > 0) zoom_offset_px = 0; @@ -1088,14 +1064,14 @@ function resize_waterfall_container(check_init) { } -audio_server_output_rate = 11025; -audio_client_resampling_factor = 4; +var audio_server_output_rate = 11025; +var audio_client_resampling_factor = 4; function audio_calculate_resampling(targetRate) { //both at the server and the client - output_range_max = 12000; - output_range_min = 8000; - i = 1; + var output_range_max = 12000; + var output_range_min = 8000; + var i = 1; while (true) { audio_server_output_rate = Math.floor(targetRate / i); if (audio_server_output_rate < output_range_min) { @@ -1110,10 +1086,10 @@ function audio_calculate_resampling(targetRate) { //both at the server and the c } -debug_ws_data_received = 0; -debug_ws_time_start = 0; -max_clients_num = 0; -client_num = 0; +var debug_ws_data_received = 0; +var debug_ws_time_start = 0; +var max_clients_num = 0; +var client_num = 0; var currentprofile; var COMPRESS_FFT_PAD_N = 10; //should be the same as in csdr.c @@ -1127,32 +1103,32 @@ function on_ws_recv(evt) { divlog("Server acknowledged WebSocket connection."); } else { try { - json = JSON.parse(evt.data); + var json = JSON.parse(evt.data); switch (json.type) { case "config": - config = json.value; - window.waterfall_colors = config.waterfall_colors; - window.waterfall_min_level_default = config.waterfall_min_level; - window.waterfall_max_level_default = config.waterfall_max_level; - window.waterfall_auto_level_margin = config.waterfall_auto_level_margin; + var config = json['value']; + window.waterfall_colors = config['waterfall_colors']; + window.waterfall_min_level_default = config['waterfall_min_level']; + window.waterfall_max_level_default = config['waterfall_max_level']; + window.waterfall_auto_level_margin = config['waterfall_auto_level_margin']; waterfallColorsDefault(); - window.starting_mod = config.start_mod; - window.starting_offset_frequency = config.start_offset_freq; - window.audio_buffering_fill_to = config.client_audio_buffer_size; - bandwidth = config.samp_rate; - center_freq = config.center_freq + config.lfo_offset; - fft_size = config.fft_size; - fft_fps = config.fft_fps; - audio_compression = config.audio_compression; + window.starting_mod = config['start_mod']; + window.starting_offset_frequency = config['start_offset_freq']; + window.audio_buffering_fill_to = config['client_audio_buffer_size']; + bandwidth = config['samp_rate']; + center_freq = config['center_freq'] + config['lfo_offset']; + fft_size = config['fft_size']; + fft_fps = config['fft_fps']; + audio_compression = config['audio_compression']; divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + "."); - fft_compression = config.fft_compression; + fft_compression = config['fft_compression']; divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + "."); - max_clients_num = config.max_clients; + max_clients_num = config['max_clients']; progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num * 0.85); - mathbox_waterfall_colors = config.mathbox_waterfall_colors; - mathbox_waterfall_frequency_resolution = config.mathbox_waterfall_frequency_resolution; - mathbox_waterfall_history_length = config.mathbox_waterfall_history_length; + mathbox_waterfall_colors = config['mathbox_waterfall_colors']; + mathbox_waterfall_frequency_resolution = config['mathbox_waterfall_frequency_resolution']; + mathbox_waterfall_history_length = config['mathbox_waterfall_history_length']; waterfall_init(); audio_preinit(); @@ -1167,75 +1143,79 @@ function on_ws_recv(evt) { } waterfall_clear(); - currentprofile = config.profile_id; + currentprofile = config['profile_id']; $('#openwebrx-sdr-profiles-listbox').val(currentprofile); break; case "secondary_config": - window.secondary_fft_size = json.value.secondary_fft_size; - window.secondary_bw = json.value.secondary_bw; - window.if_samp_rate = json.value.if_samp_rate; + var s = json['value']; + window.secondary_fft_size = s['secondary_fft_size']; + window.secondary_bw = s['secondary_bw']; + window.if_samp_rate = s['if_samp_rate']; secondary_demod_init_canvases(); break; case "receiver_details": - var r = json.value; - e('webrx-rx-title').innerHTML = r.receiver_name; - e('webrx-rx-desc').innerHTML = r.receiver_location + ' | Loc: ' + r.locator + ', ASL: ' + r.receiver_asl + ' m, [maps]'; - e('webrx-rx-photo-title').innerHTML = r.photo_title; - e('webrx-rx-photo-desc').innerHTML = r.photo_desc; + var r = json['value']; + e('webrx-rx-title').innerHTML = r['receiver_name']; + e('webrx-rx-desc').innerHTML = r['receiver_location'] + ' | Loc: ' + r['locator'] + ', ASL: ' + r['receiver_asl'] + ' m, [maps]'; + e('webrx-rx-photo-title').innerHTML = r['photo_title']; + e('webrx-rx-photo-desc').innerHTML = r['photo_desc']; break; case "smeter": - smeter_level = json.value; + smeter_level = json['value']; setSmeterAbsoluteValue(smeter_level); break; case "cpuusage": - var server_cpu_usage = json.value; + var server_cpu_usage = json['value']; progressbar_set(e("openwebrx-bar-server-cpu"), server_cpu_usage, "Server CPU [" + Math.round(server_cpu_usage * 100) + "%]", server_cpu_usage > 85); break; case "clients": - client_num = json.value; + client_num = json['value']; progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num * 0.85); break; case "profiles": var listbox = e("openwebrx-sdr-profiles-listbox"); - listbox.innerHTML = json.value.map(function (profile) { - return '"; + listbox.innerHTML = json['value'].map(function (profile) { + return '"; }).join(""); if (currentprofile) { $('#openwebrx-sdr-profiles-listbox').val(currentprofile); } break; case "features": - for (var feature in json.value) { - $('[data-feature="' + feature + '"')[json.value[feature] ? "show" : "hide"](); + var features = json['value']; + for (var feature in features) { + if (features.hasOwnProperty(feature)) { + $('[data-feature="' + feature + '"')[features[feature] ? "show" : "hide"](); + } } break; case "metadata": - update_metadata(json.value); + update_metadata(json['value']); break; case "wsjt_message": - update_wsjt_panel(json.value); + update_wsjt_panel(json['value']); break; case "dial_frequencies": - var as_bookmarks = json.value.map(function (d) { + var as_bookmarks = json['value'].map(function (d) { return { - name: d.mode.toUpperCase(), - digital_modulation: d.mode, - frequency: d.frequency + name: d['mode'].toUpperCase(), + digital_modulation: d['mode'], + frequency: d['frequency'] }; }); bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies'); break; case "aprs_data": - update_packet_panel(json.value); + update_packet_panel(json['value']); break; case "bookmarks": - bookmarks.replace_bookmarks(json.value, "server"); + bookmarks.replace_bookmarks(json['value'], "server"); break; case "sdr_error": - divlog(json.value, true); + divlog(json['value'], true); break; default: - console.warn('received message of unknown type: ' + json.type); + console.warn('received message of unknown type: ' + json['type']); } } catch (e) { // don't lose exception @@ -1246,8 +1226,12 @@ function on_ws_recv(evt) { // binary messages debug_ws_data_received += evt.data.byteLength / 1000; - type = new Uint8Array(evt.data, 0, 1)[0]; - data = evt.data.slice(1); + var type = new Uint8Array(evt.data, 0, 1)[0]; + var data = evt.data.slice(1); + + var waterfall_i16; + var waterfall_f32; + var i; switch (type) { case 1: @@ -1257,9 +1241,9 @@ function on_ws_recv(evt) { } else if (fft_compression === "adpcm") { fft_codec.reset(); - var waterfall_i16 = fft_codec.decode(new Uint8Array(data)); - var waterfall_f32 = new Float32Array(waterfall_i16.length - COMPRESS_FFT_PAD_N); - for (var i = 0; i < waterfall_i16.length; i++) waterfall_f32[i] = waterfall_i16[i + COMPRESS_FFT_PAD_N] / 100; + waterfall_i16 = fft_codec.decode(new Uint8Array(data)); + waterfall_f32 = new Float32Array(waterfall_i16.length - COMPRESS_FFT_PAD_N); + for (i = 0; i < waterfall_i16.length; i++) waterfall_f32[i] = waterfall_i16[i + COMPRESS_FFT_PAD_N] / 100; waterfall_add(waterfall_f32); } break; @@ -1274,7 +1258,7 @@ function on_ws_recv(evt) { audio_prepare(audio_data); audio_buffer_current_size_debug += audio_data.length; audio_buffer_all_size_debug += audio_data.length; - if (!(ios || is_chrome) && (audio_initialized === 0 && audio_prepared_buffers.length > audio_buffering_fill_to)) audio_init() + if (!(ios || is_chrome) && (audio_initialized === 0 && audio_prepared_buffers.length > audio_buffering_fill_to)) audio_init(); break; case 3: // secondary FFT @@ -1283,10 +1267,10 @@ function on_ws_recv(evt) { } else if (fft_compression === "adpcm") { fft_codec.reset(); - var waterfall_i16 = fft_codec.decode(new Uint8Array(data)); - var waterfall_f32 = new Float32Array(waterfall_i16.length - COMPRESS_FFT_PAD_N); - for (var i = 0; i < waterfall_i16.length; i++) waterfall_f32[i] = waterfall_i16[i + COMPRESS_FFT_PAD_N] / 100; - secondary_demod_waterfall_add(waterfall_f32); //TODO digimodes + waterfall_i16 = fft_codec.decode(new Uint8Array(data)); + waterfall_f32 = new Float32Array(waterfall_i16.length - COMPRESS_FFT_PAD_N); + for (i = 0; i < waterfall_i16.length; i++) waterfall_f32[i] = waterfall_i16[i + COMPRESS_FFT_PAD_N] / 100; + secondary_demod_waterfall_add(waterfall_f32); } break; case 4: @@ -1300,24 +1284,25 @@ function on_ws_recv(evt) { } function update_metadata(meta) { - if (meta.protocol) switch (meta.protocol) { + var el; + if (meta['protocol']) switch (meta['protocol']) { case 'DMR': - if (meta.slot) { - var el = $("#openwebrx-panel-metadata-dmr").find(".openwebrx-dmr-timeslot-panel").get(meta.slot); + if (meta['slot']) { + el = $("#openwebrx-panel-metadata-dmr").find(".openwebrx-dmr-timeslot-panel").get(meta['slot']); var id = ""; var name = ""; var target = ""; var group = false; - $(el)[meta.sync ? "addClass" : "removeClass"]("sync"); - if (meta.sync && meta.sync === "voice") { - id = (meta.additional && meta.additional.callsign) || meta.source || ""; - name = (meta.additional && meta.additional.fname) || ""; - if (meta.type === "group") { + $(el)[meta['sync'] ? "addClass" : "removeClass"]("sync"); + if (meta['sync'] && meta['sync'] === "voice") { + id = (meta['additional'] && meta['additional']['callsign']) || meta['source'] || ""; + name = (meta['additional'] && meta['additional']['fname']) || ""; + if (meta['type'] === "group") { target = "Talkgroup: "; group = true; } - if (meta.type === "direct") target = "Direct: "; - target += meta.target || ""; + if (meta['type'] === "direct") target = "Direct: "; + target += meta['target'] || ""; $(el).addClass("active"); } else { $(el).removeClass("active"); @@ -1331,20 +1316,20 @@ function update_metadata(meta) { } break; case 'YSF': - var el = $("#openwebrx-panel-metadata-ysf"); + el = $("#openwebrx-panel-metadata-ysf"); - var mode = " " + var mode = " "; var source = ""; var up = ""; var down = ""; - if (meta.mode && meta.mode !== "") { - mode = "Mode: " + meta.mode; - source = meta.source || ""; - if (meta.lat && meta.lon && meta.source) { - source = "" + source; + if (meta['mode'] && meta['mode'] !== "") { + mode = "Mode: " + meta['mode']; + source = meta['source'] || ""; + if (meta['lat'] && meta['lon'] && meta['source']) { + source = "" + source; } - up = meta.up ? "Up: " + meta.up : ""; - down = meta.down ? "Down: " + meta.down : ""; + up = meta['up'] ? "Up: " + meta['up'] : ""; + down = meta['down'] ? "Down: " + meta['down'] : ""; $(el).find(".openwebrx-meta-slot").addClass("active"); } else { $(el).find(".openwebrx-meta-slot").removeClass("active"); @@ -1372,15 +1357,16 @@ function update_wsjt_panel(msg) { return ('' + i).padStart(2, "0"); }; var linkedmsg = msg['msg']; + var matches; if (['FT8', 'JT65', 'JT9', 'FT4'].indexOf(msg['mode']) >= 0) { - var matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); + matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); if (matches && matches[2] !== 'RR73') { linkedmsg = html_escape(matches[1]) + '' + matches[2] + ''; } else { linkedmsg = html_escape(linkedmsg); } } else if (msg['mode'] === 'WSPR') { - var matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/); + matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/); if (matches) { linkedmsg = html_escape(matches[1]) + '' + matches[2] + '' + html_escape(matches[3]); } else { @@ -1455,7 +1441,7 @@ function update_packet_panel(msg) { styles['background-position-x'] = -(msg.symbol.index % 16) * 15 + 'px'; styles['background-position-y'] = -Math.floor(msg.symbol.index / 16) * 15 + 'px'; if (msg.symbol.table !== '/' && msg.symbol.table !== '\\') { - s = {} + var s = {}; s['background-position-x'] = -(msg.symbol.tableindex % 16) * 15 + 'px'; s['background-position-y'] = -Math.floor(msg.symbol.tableindex / 16) * 15 + 'px'; overlay = '
    '; @@ -1497,21 +1483,10 @@ function clear_metadata() { $(".openwebrx-dmr-timeslot-panel").removeClass("muted"); } -function add_problem(what) { - problems_span = e("openwebrx-problems"); - for (var i = 0; i < problems_span.children.length; i++) if (problems_span.children[i].innerHTML === what) return; - new_span = document.createElement("span"); - new_span.innerHTML = what; - problems_span.appendChild(new_span); - window.setTimeout(function (ps, ns) { - ps.removeChild(ns); - }, 1000, problems_span, new_span); -} - -waterfall_measure_minmax = false; -waterfall_measure_minmax_now = false; -waterfall_measure_minmax_min = 1e100; -waterfall_measure_minmax_max = -1e100; +var waterfall_measure_minmax = false; +var waterfall_measure_minmax_now = false; +var waterfall_measure_minmax_min = 1e100; +var waterfall_measure_minmax_max = -1e100; function waterfall_measure_minmax_do(what) { waterfall_measure_minmax_min = Math.min(waterfall_measure_minmax_min, Math.min.apply(Math, what)); @@ -1524,7 +1499,7 @@ function waterfall_measure_minmax_print() { function on_ws_opened() { ws.send("SERVER DE CLIENT client=openwebrx.js type=receiver"); - divlog("WebSocket opened to " + ws_url); + divlog("WebSocket opened to " + ws.url); debug_ws_data_received = 0; debug_ws_time_start = new Date().getTime(); reconnect_timeout = false; @@ -1540,10 +1515,9 @@ function divlog(what, is_error) { if (e("openwebrx-panel-log").openwebrxHidden) toggle_panel("openwebrx-panel-log"); //show panel if any error is present } e("openwebrx-debugdiv").innerHTML += what + "
    "; - //var wls=e("openwebrx-log-scroll"); - //wls.scrollTop=wls.scrollHeight; //scroll to bottom - $(".nano").nanoScroller(); - $(".nano").nanoScroller({scroll: 'bottom'}); + var nano = $('.nano'); + nano.nanoScroller(); + nano.nanoScroller({scroll: 'bottom'}); } var audio_context; @@ -1552,13 +1526,9 @@ var volume = 1.0; var volumeBeforeMute = 100.0; var mute = false; -var audio_received = Array(); -var audio_buffer_index = 0; var audio_resampler; var audio_codec = new sdrjs.ImaAdpcm(); var audio_node; -//var audio_received_sample_rate = 48000; -var audio_input_buffer_size; // Optimalise these if audio lags or is choppy: var audio_buffer_size; @@ -1569,7 +1539,6 @@ var audio_flush_interval_ms = 500; //the interval in which audio_flush() is call var audio_prepared_buffers = Array(); var audio_rebuffer; var audio_last_output_buffer; -var audio_last_output_offset = 0; var audio_buffering = false; //var audio_buffering_fill_to=4; //on audio underrun we wait until this n*audio_buffer_size samples are present //tnx to the hint from HA3FLT, now we have about half the response time! (original value: 10) @@ -1602,55 +1571,6 @@ function audio_prepare(data) { } } - -function audio_prepare_without_resampler(data) { - audio_rebuffer.push(sdrjs.ConvertI16_F(data)); - console.log("prepare", data.length, audio_rebuffer.remaining()); - while (audio_rebuffer.remaining()) { - audio_prepared_buffers.push(audio_rebuffer.take()); - audio_buffer_current_count_debug++; - } - if (audio_buffering && audio_prepared_buffers.length > audio_buffering_fill_to) audio_buffering = false; -} - -function audio_prepare_old(data) { - //console.log("audio_prepare :: "+data.length.toString()); - //console.log("data.len = "+data.length.toString()); - var dopush = function () { - console.log(audio_last_output_buffer); - audio_prepared_buffers.push(audio_last_output_buffer); - audio_last_output_offset = 0; - audio_last_output_buffer = new Float32Array(audio_buffer_size); - audio_buffer_current_count_debug++; - }; - - var original_data_length = data.length; - var f32data = new Float32Array(data.length); - for (var i = 0; i < data.length; i++) f32data[i] = data[i] / 32768; //convert_i16_f - data = audio_resampler.process(f32data); - console.log(data, data.length, original_data_length); - if (data.length === 0) return; - if (audio_last_output_offset + data.length <= audio_buffer_size) { //array fits into output buffer - for (var i = 0; i < data.length; i++) audio_last_output_buffer[i + audio_last_output_offset] = data[i]; - audio_last_output_offset += data.length; - console.log("fits into; offset=" + audio_last_output_offset.toString()); - if (audio_last_output_offset === audio_buffer_size) dopush(); - } - else { //array is larger than the remaining space in the output buffer - var copied = audio_buffer_size - audio_last_output_offset; - var remain = data.length - copied; - for (var i = 0; i < audio_buffer_size - audio_last_output_offset; i++) //fill the remaining space in the output buffer - audio_last_output_buffer[i + audio_last_output_offset] = data[i];///32768; - dopush();//push the output buffer and create a new one - console.log("larger than; copied half: " + copied.toString() + ", now at: " + audio_last_output_offset.toString()); - for (var i = 0; i < remain; i++) //copy the remaining input samples to the new output buffer - audio_last_output_buffer[i] = data[i + copied];///32768; - audio_last_output_offset += remain; - console.log("larger than; remained: " + remain.toString() + ", now at: " + audio_last_output_offset.toString()); - } - if (audio_buffering && audio_prepared_buffers.length > audio_buffering_fill_to) audio_buffering = false; -} - if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefox does AudioBuffer.prototype.copyToChannel = function (input, channel) //input is Float32Array { @@ -1668,7 +1588,6 @@ function audio_onprocess(e) { } if (audio_prepared_buffers.length === 0) { audio_buffer_progressbar_update(); - /*add_problem("audio underrun");*/ audio_buffering = true; e.outputBuffer.copyToChannel(silence, 0); } else { @@ -1712,8 +1631,8 @@ function audio_buffer_progressbar_update() { function audio_flush() { - flushed = false; - we_have_more_than = function (sec) { + var flushed = false; + var we_have_more_than = function (sec) { return sec * audio_context.sampleRate < audio_prepared_buffers.length * audio_buffer_size; }; if (we_have_more_than(audio_buffer_maximal_length_sec)) while (we_have_more_than(audio_buffer_decrease_to_on_overrun_sec)) { @@ -1721,77 +1640,23 @@ function audio_flush() { flushed = true; audio_prepared_buffers.shift(); } - //if(flushed) add_problem("audio overrun"); -} - - -function audio_onprocess_notused(e) { - //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js - if (audio_received.length === 0) { - add_problem("audio underrun"); - return; - } - output = e.outputBuffer.getChannelData(0); - int_buffer = audio_received[0]; - read_remain = audio_buffer_size; - //audio_buffer_maximal_length=120; - - obi = 0; //output buffer index - debug_str = ""; - while (1) { - if (int_buffer.length - audio_buffer_index > read_remain) { - for (i = audio_buffer_index; i < audio_buffer_index + read_remain; i++) - output[obi++] = int_buffer[i] / 32768; - //debug_str+="added whole ibl="+int_buffer.length.toString()+" abi="+audio_buffer_index.toString()+" "+(int_buffer.length-audio_buffer_index).toString()+">"+read_remain.toString()+" obi="+obi.toString()+"\n"; - audio_buffer_index += read_remain; - break; - } - else { - for (i = audio_buffer_index; i < int_buffer.length; i++) - output[obi++] = int_buffer[i] / 32768; - read_remain -= (int_buffer.length - audio_buffer_index); - audio_buffer_current_size -= audio_received[0].length; - /*if (audio_received.length>audio_buffer_maximal_length) - { - add_problem("audio overrun"); - audio_received.splice(0,audio_received.length-audio_buffer_maximal_length); - } - else*/ - audio_received.splice(0, 1); - //debug_str+="added remain, remain="+read_remain.toString()+" abi="+audio_buffer_index.toString()+" alen="+int_buffer.length.toString()+" i="+i.toString()+" arecva="+audio_received.length.toString()+" obi="+obi.toString()+"\n"; - audio_buffer_index = 0; - if (audio_received.length === 0 || read_remain === 0) return; - int_buffer = audio_received[0]; - } - } - //debug_str+="obi="+obi.toString(); - //alert(debug_str); -} - -function audio_flush_notused() { - if (audio_buffer_current_size > audio_buffer_maximal_length_sec * audio_context.sampleRate) { - add_problem("audio overrun"); - console.log("audio_flush() :: size: " + audio_buffer_current_size.toString() + " allowed: " + (audio_buffer_maximal_length_sec * audio_context.sampleRate).toString()); - while (audio_buffer_current_size > audio_buffer_maximal_length_sec * audio_context.sampleRate * 0.5) { - audio_buffer_current_size -= audio_received[0].length; - audio_received.splice(0, 1); - } - } } function webrx_set_param(what, value) { - params = {}; + var params = {}; params[what] = value; ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); } var starting_mute = false; +var starting_offset_frequency; +var starting_mod; function parsehash() { + var h; if (h = window.location.hash) { h.substring(1).split(",").forEach(function (x) { - harr = x.split("="); - //console.log(harr); + var harr = x.split("="); if (harr[0] === "mute") toggleMute(); else if (harr[0] === "mod") starting_mod = harr[1]; else if (harr[0] === "sql") { @@ -1857,7 +1722,7 @@ function audio_init() { audio_initialized = 1; // only tell on_ws_recv() not to call it again //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor - createjsnode_function = (audio_context.createJavaScriptNode === undefined) ? audio_context.createScriptProcessor.bind(audio_context) : audio_context.createJavaScriptNode.bind(audio_context); + var createjsnode_function = (audio_context.createJavaScriptNode === undefined) ? audio_context.createScriptProcessor.bind(audio_context) : audio_context.createJavaScriptNode.bind(audio_context); audio_node = createjsnode_function(audio_buffer_size, 0, 1); audio_node.onaudioprocess = audio_onprocess; audio_node.connect(audio_context.destination); @@ -1865,7 +1730,7 @@ function audio_init() { //https://github.com/grantgalitz/XAudioJS/blob/master/XAudioServer.js //audio_resampler = new Resampler(audio_received_sample_rate, audio_context.sampleRate, 1, audio_buffer_size, true); //audio_input_buffer_size = audio_buffer_size*(audio_received_sample_rate/audio_context.sampleRate); - webrx_set_param("audio_rate", audio_context.sampleRate); //Don't try to resample //TODO remove this + webrx_set_param("audio_rate", audio_context.sampleRate); window.setInterval(audio_flush, audio_flush_interval_ms); divlog('Web Audio API succesfully initialized, sample rate: ' + audio_context.sampleRate.toString() + " sps"); @@ -1913,7 +1778,7 @@ function on_ws_closed() { setTimeout(open_websocket, reconnect_timeout); } -function on_ws_error(event) { +function on_ws_error() { divlog("WebSocket error.", 1); } @@ -1921,13 +1786,15 @@ String.prototype.startswith = function (str) { return this.indexOf(str) === 0; }; //http://stackoverflow.com/questions/646628/how-to-check-if-a-string-startswith-another-string +var ws; + function open_websocket() { var protocol = 'ws'; if (window.location.toString().startsWith('https://')) { protocol = 'wss'; } - ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; //guess automatically -> now default behaviour + var ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; //guess automatically -> now default behaviour if (!("WebSocket" in window)) divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser."); ws = new WebSocket(ws_url); @@ -1947,19 +1814,19 @@ function waterfall_mkcolor(db_value, waterfall_colors_arg) { if (typeof waterfall_colors_arg === 'undefined') waterfall_colors_arg = waterfall_colors; if (db_value < waterfall_min_level) db_value = waterfall_min_level; if (db_value > waterfall_max_level) db_value = waterfall_max_level; - full_scale = waterfall_max_level - waterfall_min_level; - relative_value = db_value - waterfall_min_level; - value_percent = relative_value / full_scale; - percent_for_one_color = 1 / (waterfall_colors_arg.length - 1); - index = Math.floor(value_percent / percent_for_one_color); - remain = (value_percent - percent_for_one_color * index) / percent_for_one_color; + var full_scale = waterfall_max_level - waterfall_min_level; + var relative_value = db_value - waterfall_min_level; + var value_percent = relative_value / full_scale; + var percent_for_one_color = 1 / (waterfall_colors_arg.length - 1); + var index = Math.floor(value_percent / percent_for_one_color); + var remain = (value_percent - percent_for_one_color * index) / percent_for_one_color; return color_between(waterfall_colors_arg[index + 1], waterfall_colors_arg[index], remain); } function color_between(first, second, percent) { - output = 0; - for (i = 0; i < 4; i++) { - add = ((((first & (0xff << (i * 8))) >>> 0) * percent) + (((second & (0xff << (i * 8))) >>> 0) * (1 - percent))) & (0xff << (i * 8)); + var output = 0; + for (var i = 0; i < 4; i++) { + var add = ((((first & (0xff << (i * 8))) >>> 0) * percent) + (((second & (0xff << (i * 8))) >>> 0) * (1 - percent))) & (0xff << (i * 8)); output |= add >>> 0; } return output >>> 0; @@ -1970,7 +1837,7 @@ var canvas_context; var canvases = []; var canvas_default_height = 200; var canvas_container; -var canvas_phantom; +var canvas_actual_line; function add_canvas() { var new_canvas = document.createElement("canvas"); @@ -2019,7 +1886,7 @@ function resize_canvases(zoom) { if (typeof zoom === "undefined") zoom = false; if (!zoom) mkzoomlevels(); zoom_calc(); - new_width = (canvas_container.clientWidth * zoom_levels[zoom_level]).toString() + "px"; + var new_width = (canvas_container.clientWidth * zoom_levels[zoom_level]).toString() + "px"; var zoom_value = zoom_offset_px.toString() + "px"; canvases.forEach(function (p) { p.style.width = new_width; @@ -2037,8 +1904,6 @@ function waterfall_init() { waterfall_setup_done = 1; } -var waterfall_dont_scale = 0; - var mathbox_shift = function () { if (mathbox_data_current_depth < mathbox_data_max_depth) mathbox_data_current_depth++; if (mathbox_data_index + 1 >= mathbox_data_max_depth) mathbox_data_index = 0; @@ -2077,9 +1942,9 @@ function waterfall_add(data) { mathbox_shift(); } else { //Add line to waterfall image - oneline_image = canvas_context.createImageData(w, 1); - for (x = 0; x < w; x++) { - color = waterfall_mkcolor(data[x]); + var oneline_image = canvas_context.createImageData(w, 1); + for (var x = 0; x < w; x++) { + var color = waterfall_mkcolor(data[x]); for (i = 0; i < 4; i++) oneline_image.data[x * 4 + i] = ((color >>> 0) >> ((3 - i) * 8)) & 0xff; } @@ -2099,7 +1964,6 @@ function check_top_bar_congestion() { }; var wet = e("webrx-rx-title"); var wed = e("webrx-rx-desc"); - var rightmost = Math.max(rmf(wet), rmf(wed)); var tl = e("openwebrx-main-buttons"); [wet, wed].map(function (what) { @@ -2119,6 +1983,16 @@ var MATHBOX_MODES = var mathbox_mode = MATHBOX_MODES.UNINITIALIZED; var mathbox; var mathbox_element; +var mathbox_waterfall_colors; +var mathbox_waterfall_frequency_resolution; +var mathbox_waterfall_history_length; +var mathbox_correction_for_z; +var mathbox_data_max_depth; +var mathbox_data_current_depth; +var mathbox_data_index; +var mathbox_data; +var mathbox_data_global_index; +var mathbox_container; function mathbox_init() { //mathbox_waterfall_history_length is defined in the config @@ -2133,12 +2007,12 @@ function mathbox_init() { plugins: ['core', 'controls', 'cursor', 'stats'], controls: {klass: THREE.OrbitControls} }); - three = mathbox.three; + var three = mathbox.three; if (typeof three === "undefined") divlog("3D waterfall cannot be initialized because WebGL is not supported in your browser.", true); three.renderer.setClearColor(new THREE.Color(0x808080), 1.0); mathbox_container.appendChild((mathbox_element = three.renderer.domElement)); - view = mathbox + var view = mathbox .set({ scale: 1080, focus: 3 @@ -2191,18 +2065,18 @@ function mathbox_init() { if (!mathbox_data_index_valid(zIndex)) return {y: undefined, dBValue: undefined, zAdd: 0}; var index = Math.trunc(xIndex + realZIndex * fft_size); var dBValue = mathbox_data[index]; + var y; if (dBValue > waterfall_max_level) y = 1; else if (dBValue < waterfall_min_level) y = 0; else y = (dBValue - waterfall_min_level) / (waterfall_max_level - waterfall_min_level); - mathbox_dbg = {dbv: dBValue, indexval: index, mbd: mathbox_data.length, yval: y}; if (!y) y = 0; return {y: y, dBValue: dBValue, zAdd: zAdd}; }; - var points = view.area({ + view.area({ expr: function (emit, x, z, i, j, t) { var y; - remapResult = remap(x, z, t); + var remapResult = remap(x, z, t); if ((y = remapResult.y) === undefined) return; emit(x, y, z + remapResult.zAdd); }, @@ -2212,7 +2086,7 @@ function mathbox_init() { axes: [1, 3] }); - var colors = view.area({ + view.area({ expr: function (emit, x, z, i, j, t) { var dBValue; if ((dBValue = remap(x, z, t).dBValue) === undefined) return; @@ -2265,7 +2139,6 @@ function waterfall_clear() { { var x = canvases.shift(); x.parentNode.removeChild(x); - delete x; } add_canvas(); } @@ -2287,7 +2160,8 @@ var bookmarks; function openwebrx_init() { if (ios || is_chrome) e("openwebrx-big-grey").style.display = "table-cell"; - (opb = e("openwebrx-play-button-text")).style.marginTop = (window.innerHeight / 2 - opb.clientHeight / 2).toString() + "px"; + var opb = e("openwebrx-play-button-text"); + opb.style.marginTop = (window.innerHeight / 2 - opb.clientHeight / 2).toString() + "px"; init_rx_photo(); open_websocket(); secondary_demod_init(); @@ -2342,25 +2216,17 @@ var rt = function (s, n) { return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + n) ? c : c - 26); }); }; -var irt = function (s, n) { - return s.replace(/[a-zA-Z]/g, function (c) { - return String.fromCharCode((c >= "a" ? 97 : 65) <= (c = c.charCodeAt(0) - n) ? c : c + 26); - }); -}; -var sendmail2 = function (s) { - window.location.href = "mailto:" + irt(s.replace("=", String.fromCharCode(0100)).replace("$", "."), 8); -}; var audio_debug_time_start = 0; var audio_debug_time_last_start = 0; function debug_audio() { if (audio_debug_time_start === 0) return; //audio_init has not been called - time_now = (new Date()).getTime(); - audio_debug_time_since_last_call = (time_now - audio_debug_time_last_start) / 1000; + var time_now = (new Date()).getTime(); + var audio_debug_time_since_last_call = (time_now - audio_debug_time_last_start) / 1000; audio_debug_time_last_start = time_now; //now - audio_debug_time_taken = (time_now - audio_debug_time_start) / 1000; - kbps_mult = (audio_compression === "adpcm") ? 8 : 16; + var audio_debug_time_taken = (time_now - audio_debug_time_start) / 1000; + var kbps_mult = (audio_compression === "adpcm") ? 8 : 16; //e("openwebrx-audio-sps").innerHTML= // ((audio_compression=="adpcm")?"ADPCM compressed":"uncompressed")+" audio downlink:
    "+(audio_buffer_current_size_debug*kbps_mult/audio_debug_time_since_last_call).toFixed(0)+" kbps ("+ // (audio_buffer_all_size_debug*kbps_mult/audio_debug_time_taken).toFixed(1)+" kbps avg.), feed at "+ @@ -2389,19 +2255,19 @@ function debug_audio() { // ======================= PANELS ======================= // ======================================================== -panel_margin = 5.9; +var panel_margin = 5.9; function pop_bottommost_panel(from) { - min_order = parseInt(from[0].dataset.panelOrder); - min_index = 0; - for (i = 0; i < from.length; i++) { - actual_order = parseInt(from[i].dataset.panelOrder); + var min_order = parseInt(from[0].dataset.panelOrder); + var min_index = 0; + for (var i = 0; i < from.length; i++) { + var actual_order = parseInt(from[i].dataset.panelOrder); if (actual_order < min_order) { min_index = i; min_order = actual_order; } } - to_return = from[min_index]; + var to_return = from[min_index]; from.splice(min_index, 1); return to_return; } @@ -2441,26 +2307,20 @@ function toggle_panel(what, on) { function first_show_panel(panel) { panel.style.transitionDuration = 0; panel.style.transitionDelay = 0; - rotx = (Math.random() > 0.5) ? -90 : 90; - roty = 0; + var rotx = (Math.random() > 0.5) ? -90 : 90; + var roty = 0; if (Math.random() > 0.5) { - rottemp = rotx; + var rottemp = rotx; rotx = roty; roty = rottemp; } if (rotx !== 0 && Math.random() > 0.5) rotx = 270; - //console.log(rotx,roty); - transformString = "perspective( 599px ) rotateX( %1deg ) rotateY( %2deg )" + panel.style.transform = "perspective( 599px ) rotateX( %1deg ) rotateY( %2deg )" .replace("%1", rotx.toString()).replace("%2", roty.toString()); - //console.log(transformString); - //console.log(panel); - panel.style.transform = transformString; window.setTimeout(function () { panel.style.transitionDuration = "599ms"; panel.style.transitionDelay = (Math.floor(Math.random() * 500)).toString() + "ms"; panel.style.transform = "perspective( 599px ) rotateX( 0deg ) rotateY( 0deg )"; - //panel.style.transitionDuration="0ms"; - //panel.style.transitionDelay="0"; }, 1); } @@ -2471,8 +2331,8 @@ function place_panels(function_apply) { var left_col = []; var right_col = []; var plist = e("openwebrx-panels-container").children; - for (i = 0; i < plist.length; i++) { - c = plist[i]; + for (var i = 0; i < plist.length; i++) { + var c = plist[i]; if (c.className.indexOf("openwebrx-panel") >= 0) { if (c.openwebrxHidden) { c.style.display = "none"; @@ -2480,7 +2340,7 @@ function place_panels(function_apply) { } c.style.display = "block"; c.openwebrxPanelTransparent = (!!c.dataset.panelTransparent); - newSize = c.dataset.panelSize.split(","); + var newSize = c.dataset.panelSize.split(","); if (c.dataset.panelPos === "left") { left_col.push(c); } @@ -2496,7 +2356,8 @@ function place_panels(function_apply) { } } - y = hoffset; //was y=0 before hoffset + var y = hoffset; //was y=0 before hoffset + var p; while (left_col.length > 0) { p = pop_bottommost_panel(left_col); p.style.left = "0px"; @@ -2576,8 +2437,22 @@ function demodulator_analog_replace_last() { |___/ */ -secondary_demod = false; -secondary_demod_offset_freq = 0; +var secondary_demod = false; +var secondary_demod_fft_offset_db = 30; //need to calculate that later +var secondary_demod_canvases_initialized = false; +var secondary_demod_listbox_updating = false; +var secondary_demod_channel_freq = 1000; +var secondary_demod_waiting_for_set = false; +var secondary_demod_low_cut; +var secondary_demod_high_cut; +var secondary_demod_mousedown = false; +var secondary_demod_canvas_width; +var secondary_demod_canvas_left; +var secondary_demod_canvas_container; +var secondary_demod_current_canvas_actual_line; +var secondary_demod_current_canvas_context; +var secondary_demod_current_canvas_index; +var secondary_demod_canvases; function demodulator_digital_replace_last() { demodulator_digital_replace(last_digital_demodulator_subtype); @@ -2621,7 +2496,6 @@ function secondary_demod_create_canvas() { new_canvas.height = $(secondary_demod_canvas_container).height(); new_canvas.style.width = $(secondary_demod_canvas_container).width() + "px"; new_canvas.style.height = $(secondary_demod_canvas_container).height() + "px"; - console.log(new_canvas.width, new_canvas.height, new_canvas.style.width, new_canvas.style.height); secondary_demod_current_canvas_actual_line = new_canvas.height - 1; $(secondary_demod_canvas_container).children().last().before(new_canvas); return new_canvas; @@ -2643,7 +2517,6 @@ function secondary_demod_init_canvases() { secondary_demod_current_canvas_actual_line = $(secondary_demod_canvas_container).height() - 1; secondary_demod_current_canvas_index = 0; secondary_demod_canvases_initialized = true; - //secondary_demod_update_channel_freq_from_event(); mkscale(); //so that the secondary waterfall zoom level will be initialized } @@ -2652,7 +2525,6 @@ function secondary_demod_canvases_update_top() { } function secondary_demod_swap_canvases() { - console.log("swap"); secondary_demod_canvases[0 + !secondary_demod_current_canvas_index].openwebrx_top -= $(secondary_demod_canvas_container).height() * 2; secondary_demod_current_canvas_index = 0 + !secondary_demod_current_canvas_index; secondary_demod_current_canvas_context = secondary_demod_canvases[secondary_demod_current_canvas_index].getContext("2d"); @@ -2679,22 +2551,11 @@ function secondary_demod_start(subtype) { secondary_demod = subtype; } -function secondary_demod_set() { - ws.send(JSON.stringify({"type": "dspcontrol", "params": {"secondary_offset_freq": secondary_demod_offset_freq}})); -} - function secondary_demod_stop() { ws.send(JSON.stringify({"type": "dspcontrol", "params": {"secondary_mod": false}})); secondary_demod = false; } -function secondary_demod_push_binary_data(x) { - secondary_demod_push_data(Array.from(x).map(function (y) { - return y ? "1" : "0" - }).join("") - ); -} - function secondary_demod_push_data(x) { x = Array.from(x).filter(function (y) { var c = y.charCodeAt(0); @@ -2716,10 +2577,6 @@ function secondary_demod_push_data(x) { $("#openwebrx-cursor-blink").before(x); } -function secondary_demod_data_clear() { - $("#openwebrx-cursor-blink").prevAll().remove(); -} - function secondary_demod_close_window() { secondary_demod_stop(); toggle_panel("openwebrx-panel-digimodes", false); @@ -2727,17 +2584,15 @@ function secondary_demod_close_window() { toggle_panel("openwebrx-panel-packet-message", false); } -secondary_demod_fft_offset_db = 30; //need to calculate that later - function secondary_demod_waterfall_add(data) { if (!secondary_demod) return; var w = secondary_fft_size; //Add line to waterfall image var oneline_image = secondary_demod_current_canvas_context.createImageData(w, 1); - for (x = 0; x < w; x++) { + for (var x = 0; x < w; x++) { var color = waterfall_mkcolor(data[x] + secondary_demod_fft_offset_db); - for (i = 0; i < 4; i++) oneline_image.data[x * 4 + i] = ((color >>> 0) >> ((3 - i) * 8)) & 0xff; + for (var i = 0; i < 4; i++) oneline_image.data[x * 4 + i] = ((color >>> 0) >> ((3 - i) * 8)) & 0xff; } //Draw image @@ -2750,10 +2605,6 @@ function secondary_demod_waterfall_add(data) { if (secondary_demod_current_canvas_actual_line < 0) secondary_demod_swap_canvases(); } -var secondary_demod_canvases_initialized = false; - -secondary_demod_listbox_updating = false; - function secondary_demod_listbox_changed() { if (secondary_demod_listbox_updating) return; var sdm = $("#openwebrx-secondary-demod-listbox")[0].value; @@ -2762,35 +2613,27 @@ function secondary_demod_listbox_changed() { } else { demodulator_digital_replace(sdm); } - update_dial_button(); } function secondary_demod_listbox_update() { secondary_demod_listbox_updating = true; $("#openwebrx-secondary-demod-listbox").val((secondary_demod) ? secondary_demod : "none"); - console.log("update"); secondary_demod_listbox_updating = false; } -secondary_demod_channel_freq = 1000; - function secondary_demod_update_marker() { var width = Math.max((secondary_bw / (if_samp_rate / 2)) * secondary_demod_canvas_width, 5); var center_at = (secondary_demod_channel_freq / (if_samp_rate / 2)) * secondary_demod_canvas_width + secondary_demod_canvas_left; var left = center_at - width / 2; - //console.log("sdum", width, left); $("#openwebrx-digimode-select-channel").width(width).css("left", left + "px") } -secondary_demod_waiting_for_set = false; - function secondary_demod_update_channel_freq_from_event(evt) { if (typeof evt !== "undefined") { var relativeX = (evt.offsetX) ? evt.offsetX : evt.layerX; secondary_demod_channel_freq = secondary_demod_low_cut + (relativeX / $(secondary_demod_canvas_container).width()) * (secondary_demod_high_cut - secondary_demod_low_cut); } - //console.log("toset:", secondary_demod_channel_freq); if (!secondary_demod_waiting_for_set) { secondary_demod_waiting_for_set = true; window.setTimeout(function () { @@ -2798,7 +2641,6 @@ function secondary_demod_update_channel_freq_from_event(evt) { "type": "dspcontrol", "params": {"secondary_offset_freq": Math.floor(secondary_demod_channel_freq)} })); - //console.log("doneset:", secondary_demod_channel_freq); secondary_demod_waiting_for_set = false; }, 50 @@ -2808,8 +2650,6 @@ function secondary_demod_update_channel_freq_from_event(evt) { secondary_demod_update_marker(); } -secondary_demod_mousedown = false; - function secondary_demod_canvas_container_mousein() { $("#openwebrx-digimode-select-channel").css("opacity", "0.7"); //.css("border-width", "1px"); } @@ -2838,7 +2678,7 @@ function secondary_demod_waterfall_set_zoom(low_cut, high_cut) { var hctmp = high_cut; var lctmp = low_cut; low_cut = -hctmp; - low_cut = -lctmp; + high_cut = -lctmp; } else if (low_cut < 0 && high_cut > 0) { high_cut = Math.max(Math.abs(high_cut), Math.abs(low_cut)); @@ -2858,6 +2698,6 @@ function secondary_demod_waterfall_set_zoom(low_cut, high_cut) { } function sdr_profile_changed() { - value = $('#openwebrx-sdr-profiles-listbox').val(); + var value = $('#openwebrx-sdr-profiles-listbox').val(); ws.send(JSON.stringify({type: "selectprofile", params: {profile: value}})); } From 93d4e629d13dd9df6138535328f4154c8519cecf Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 17 Oct 2019 19:28:05 +0200 Subject: [PATCH 0489/2616] more bookmarks --- bookmarks.json | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/bookmarks.json b/bookmarks.json index f0182dd22..6260233e6 100644 --- a/bookmarks.json +++ b/bookmarks.json @@ -178,5 +178,45 @@ "name": "DM0ULR", "frequency": 439337500, "modulation": "nxdn" + }, + { + "name": "DB0MIR", + "frequency": 439300000, + "modulation": "nfm" + }, + { + "name": "DB0PM", + "frequency": 439075000, + "modulation": "nfm" + }, + { + "name": "DB0CP", + "frequency": 439025000, + "modulation": "nfm" + }, + { + "name": "OE7XGR", + "frequency": 438925000, + "modulation": "dmr" + }, + { + "name": "DB0TOL", + "frequency": 438725000, + "modulation": "nfm" + }, + { + "name": "DB0OAL", + "frequency": 438325000, + "modulation": "dstar" + }, + { + "name": "DB0ROL", + "frequency": 439237500, + "modulation": "nfm" + }, + { + "name": "DB0ABX", + "frequency": 439137500, + "modulation": "nfm" } ] From 0b2c45703065f9c5cb4b67c941f14860783871e4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 18 Oct 2019 21:13:48 +0200 Subject: [PATCH 0490/2616] kill client-side early rebuffering, improving the latency --- config_webrx.py | 5 --- htdocs/openwebrx.js | 88 +++++++++++++++++++-------------------------- owrx/connection.py | 1 - 3 files changed, 37 insertions(+), 57 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index 5613d39a7..c856578f5 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -181,11 +181,6 @@ # ==== Misc settings ==== -client_audio_buffer_size = 5 -# increasing client_audio_buffer_size will: -# - also increase the latency -# - decrease the chance of audio underruns - iq_port_range = [ 4950, 4960, diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 13a6c5a24..f0b797130 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1115,7 +1115,6 @@ function on_ws_recv(evt) { window.starting_mod = config['start_mod']; window.starting_offset_frequency = config['start_offset_freq']; - window.audio_buffering_fill_to = config['client_audio_buffer_size']; bandwidth = config['samp_rate']; center_freq = config['center_freq'] + config['lfo_offset']; fft_size = config['fft_size']; @@ -1258,7 +1257,7 @@ function on_ws_recv(evt) { audio_prepare(audio_data); audio_buffer_current_size_debug += audio_data.length; audio_buffer_all_size_debug += audio_data.length; - if (!(ios || is_chrome) && (audio_initialized === 0 && audio_prepared_buffers.length > audio_buffering_fill_to)) audio_init(); + if (!(ios || is_chrome) && (audio_initialized === 0)) audio_init(); break; case 3: // secondary FFT @@ -1536,12 +1535,8 @@ var audio_buffer_maximal_length_sec = 3; //actual number of samples are calculat var audio_buffer_decrease_to_on_overrun_sec = 2.2; var audio_flush_interval_ms = 500; //the interval in which audio_flush() is called -var audio_prepared_buffers = Array(); -var audio_rebuffer; +var audio_buffers = []; var audio_last_output_buffer; -var audio_buffering = false; -//var audio_buffering_fill_to=4; //on audio underrun we wait until this n*audio_buffer_size samples are present -//tnx to the hint from HA3FLT, now we have about half the response time! (original value: 10) function gain_ff(gain_value, data) //great! solved clicking! will have to move to sdr.js { @@ -1551,24 +1546,12 @@ function gain_ff(gain_value, data) //great! solved clicking! will have to move t } function audio_prepare(data) { - - //audio_rebuffer.push(sdrjs.ConvertI16_F(data));//no resampling - //audio_rebuffer.push(audio_resampler.process(sdrjs.ConvertI16_F(data)));//resampling without ADPCM - if (audio_compression === "none") - audio_rebuffer.push(audio_resampler.process(gain_ff(volume, sdrjs.ConvertI16_F(data))));//resampling without ADPCM - else if (audio_compression === "adpcm") - audio_rebuffer.push(audio_resampler.process(gain_ff(volume, sdrjs.ConvertI16_F(audio_codec.decode(data))))); //resampling & ADPCM - else return; - - //console.log("prepare",data.length,audio_rebuffer.remaining()); - while (audio_rebuffer.remaining()) { - audio_prepared_buffers.push(audio_rebuffer.take()); - audio_buffer_current_count_debug++; - } - if (audio_buffering && audio_prepared_buffers.length > audio_buffering_fill_to) { - console.log("buffers now: " + audio_prepared_buffers.length.toString()); - audio_buffering = false; + var buffer = data; + if (audio_compression === "adpcm") { + //resampling & ADPCM + buffer = audio_codec.decode(buffer); } + audio_buffers.push(audio_resampler.process(gain_ff(volume, sdrjs.ConvertI16_F(buffer)))); } if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefox does @@ -1579,45 +1562,50 @@ if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefo } } -var silence = new Float32Array(4096); - function audio_onprocess(e) { - if (audio_buffering) { - e.outputBuffer.copyToChannel(silence, 0); - return; - } - if (audio_prepared_buffers.length === 0) { - audio_buffer_progressbar_update(); - audio_buffering = true; - e.outputBuffer.copyToChannel(silence, 0); - } else { - var buf = audio_prepared_buffers.shift(); - e.outputBuffer.copyToChannel(buf, 0); + var total = 0; + var out = new Float32Array(audio_buffer_size); + while (audio_buffers.length) { + var b = audio_buffers.shift(); + var newLength = total + b.length; + // not enough space to fit all data, so splice and put back in the queue + if (newLength > audio_buffer_size) { + var tokeep = b.slice(0, audio_buffer_size - total); + out.set(tokeep, total); + var tobuffer = b.slice(audio_buffer_size - total, b.length); + audio_buffers.unshift(tobuffer); + break; + } else { + out.set(b, total); + } + total = newLength; } + + e.outputBuffer.copyToChannel(out, 0); } var audio_buffer_progressbar_update_disabled = false; var audio_buffer_total_average_level = 0; var audio_buffer_total_average_level_length = 0; -var audio_overrun_cnt = 0; -var audio_underrun_cnt = 0; + +function audio_buffers_total_length() { + return audio_buffers.map(function(b){ return b.length; }).reduce(function(a, b){ return a + b; }, 0); +} function audio_buffer_progressbar_update() { if (audio_buffer_progressbar_update_disabled) return; - var audio_buffer_value = (audio_prepared_buffers.length * audio_buffer_size) / audio_context.sampleRate; + var audio_buffer_value = audio_buffers_total_length() / audio_context.sampleRate; audio_buffer_total_average_level_length++; audio_buffer_total_average_level = (audio_buffer_total_average_level * ((audio_buffer_total_average_level_length - 1) / audio_buffer_total_average_level_length)) + (audio_buffer_value / audio_buffer_total_average_level_length); var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; - var underrun = audio_prepared_buffers.length === 0; + var underrun = audio_buffers.length === 0; var text = "buffer"; if (overrun) { text = "overrun"; - console.log("audio overrun, " + (++audio_overrun_cnt).toString()); } if (underrun) { text = "underrun"; - console.log("audio underrun, " + (++audio_underrun_cnt).toString()); } if (overrun || underrun) { audio_buffer_progressbar_update_disabled = true; @@ -1633,12 +1621,12 @@ function audio_buffer_progressbar_update() { function audio_flush() { var flushed = false; var we_have_more_than = function (sec) { - return sec * audio_context.sampleRate < audio_prepared_buffers.length * audio_buffer_size; + return sec * audio_context.sampleRate < audio_buffers_total_length(); }; if (we_have_more_than(audio_buffer_maximal_length_sec)) while (we_have_more_than(audio_buffer_decrease_to_on_overrun_sec)) { if (!flushed) audio_buffer_progressbar_update(); flushed = true; - audio_prepared_buffers.shift(); + audio_buffers.shift(); } } @@ -1690,13 +1678,11 @@ function audio_preinit() { else if (audio_context.sampleRate > 44100 * 4) audio_buffer_size = 4096 * 4; - if (!audio_rebuffer) { - audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size, sdrjs.REBUFFER_FIXED); - audio_last_output_buffer = new Float32Array(audio_buffer_size); - - //we send our setup packet - parsehash(); + //we send our setup packet + // TODO this should be moved to another stage of initialization + parsehash(); + if (!audio_resampler) { audio_calculate_resampling(audio_context.sampleRate); audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor, 1); } diff --git a/owrx/connection.py b/owrx/connection.py index ab0cabb8b..4ee8734ac 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -65,7 +65,6 @@ class OpenWebRxReceiverClient(Client): "fft_compression", "max_clients", "start_mod", - "client_audio_buffer_size", "start_freq", "center_freq", "mathbox_waterfall_colors", From 6bc928b5b60a4d07da6add841f0723c032b2d596 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 18 Oct 2019 21:34:00 +0200 Subject: [PATCH 0491/2616] fine-tune audio buffering --- htdocs/openwebrx.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index f0b797130..0df4b5182 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -31,7 +31,6 @@ function arrayBufferToString(buf) { var bandwidth; var center_freq; var audio_buffer_current_size_debug = 0; -var audio_buffer_all_size_debug = 0; var audio_buffer_current_count_debug = 0; var fft_size; var fft_fps; @@ -1256,7 +1255,6 @@ function on_ws_recv(evt) { } audio_prepare(audio_data); audio_buffer_current_size_debug += audio_data.length; - audio_buffer_all_size_debug += audio_data.length; if (!(ios || is_chrome) && (audio_initialized === 0)) audio_init(); break; case 3: @@ -1531,8 +1529,8 @@ var audio_node; // Optimalise these if audio lags or is choppy: var audio_buffer_size; -var audio_buffer_maximal_length_sec = 3; //actual number of samples are calculated from sample rate -var audio_buffer_decrease_to_on_overrun_sec = 2.2; +var audio_buffer_maximal_length_sec = 1; //actual number of samples are calculated from sample rate +var audio_buffer_decrease_to_on_overrun_sec = 0.8; var audio_flush_interval_ms = 500; //the interval in which audio_flush() is called var audio_buffers = []; @@ -1582,6 +1580,10 @@ function audio_onprocess(e) { } e.outputBuffer.copyToChannel(out, 0); + + if (!audio_buffers.length) { + audio_buffer_progressbar_update(); + } } var audio_buffer_progressbar_update_disabled = false; @@ -1614,7 +1616,7 @@ function audio_buffer_progressbar_update() { audio_buffer_progressbar_update(); }, 1000); } - progressbar_set(e("openwebrx-bar-audio-buffer"), (underrun) ? 1 : audio_buffer_value / 1.5, "Audio " + text + " [" + (audio_buffer_value).toFixed(1) + " s]", overrun || underrun || audio_buffer_value < 0.25); + progressbar_set(e("openwebrx-bar-audio-buffer"), (underrun) ? 1 : audio_buffer_value, "Audio " + text + " [" + (audio_buffer_value).toFixed(1) + " s]", overrun || underrun); } @@ -2213,10 +2215,6 @@ function debug_audio() { audio_debug_time_last_start = time_now; //now var audio_debug_time_taken = (time_now - audio_debug_time_start) / 1000; var kbps_mult = (audio_compression === "adpcm") ? 8 : 16; - //e("openwebrx-audio-sps").innerHTML= - // ((audio_compression=="adpcm")?"ADPCM compressed":"uncompressed")+" audio downlink:
    "+(audio_buffer_current_size_debug*kbps_mult/audio_debug_time_since_last_call).toFixed(0)+" kbps ("+ - // (audio_buffer_all_size_debug*kbps_mult/audio_debug_time_taken).toFixed(1)+" kbps avg.), feed at "+ - // ((audio_buffer_current_count_debug*audio_buffer_size)/audio_debug_time_taken).toFixed(1)+" sps output"; var audio_speed_value = audio_buffer_current_size_debug * kbps_mult / audio_debug_time_since_last_call; progressbar_set(e("openwebrx-bar-audio-speed"), audio_speed_value / 500000, "Audio stream [" + (audio_speed_value / 1000).toFixed(0) + " kbps]", false); From 778591d460b72d47352356b3ca8be887158753e3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 19 Oct 2019 01:19:19 +0200 Subject: [PATCH 0492/2616] an attempt to implement audioworklets was made. works mostly, but skips samples --- htdocs/lib/AudioProcessor.js | 59 ++++++++++++++++++++++++++++ htdocs/openwebrx.js | 75 ++++++++++++++++++++++++++---------- 2 files changed, 114 insertions(+), 20 deletions(-) create mode 100644 htdocs/lib/AudioProcessor.js diff --git a/htdocs/lib/AudioProcessor.js b/htdocs/lib/AudioProcessor.js new file mode 100644 index 000000000..f0a70257f --- /dev/null +++ b/htdocs/lib/AudioProcessor.js @@ -0,0 +1,59 @@ +class OwrxAudioProcessor extends AudioWorkletProcessor { + constructor(options){ + super(options); + this.maxLength = options.processorOptions.maxLength; + this.reduceToLength = options.processorOptions.reduceToLength; + this.audio_buffers = []; + this.port.addEventListener('message', (m) => { + if (typeof(m.data) === 'string') { + const json = JSON.parse(m.data); + if (json.cmd && json.cmd == 'getBuffers') { + this.reportBuffers(); + } + } else { + this.audio_buffers.push(new Float32Array(m.data)); + } + }); + this.port.addEventListener('messageerror', console.error); + this.port.start(); + } + process(inputs, outputs, parameters) { + //console.time('audio::process'); + outputs[0].forEach((output) => { + let total = 0; + while (this.audio_buffers.length) { + const b = this.audio_buffers.shift(); + const newLength = total + b.length; + const ol = output.length; + // not enough space to fit all data, so splice and put back in the queue + if (newLength > ol) { + const tokeep = b.slice(0, ol - total); + output.set(tokeep, total); + const tobuffer = b.slice(ol - total, b.length); + this.audio_buffers.unshift(tobuffer); + break; + } else { + output.set(b, total); + } + total = newLength; + } + }); + //console.timeEnd('audio::process'); + return true; + } + bufferLength() { + return this.audio_buffers.map(function(b){ return b.length; }).reduce(function(a, b){ return a + b; }, 0); + } + reportBuffers() { + var we_have_more_than = (sec) => { + return sec * sampleRate < this.bufferLength(); + }; + if (we_have_more_than(this.maxLength)) while (we_have_more_than(this.reduceToLength)) { + this.audio_buffers.shift(); + } + + this.port.postMessage(JSON.stringify({buffersize: this.bufferLength()})); + } +} + +registerProcessor('openwebrx-audio-processor', OwrxAudioProcessor); \ No newline at end of file diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 0df4b5182..912bba007 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1544,12 +1544,19 @@ function gain_ff(gain_value, data) //great! solved clicking! will have to move t } function audio_prepare(data) { + if (!audio_node) return; var buffer = data; if (audio_compression === "adpcm") { //resampling & ADPCM buffer = audio_codec.decode(buffer); } - audio_buffers.push(audio_resampler.process(gain_ff(volume, sdrjs.ConvertI16_F(buffer)))); + buffer = audio_resampler.process(gain_ff(volume, sdrjs.ConvertI16_F(buffer))); + if (audio_node.port) { + // AudioWorklets supported + audio_node.port.postMessage(buffer, [buffer.buffer]); + } else { + audio_buffers.push(buffer); + } } if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefox does @@ -1595,13 +1602,13 @@ function audio_buffers_total_length() { return audio_buffers.map(function(b){ return b.length; }).reduce(function(a, b){ return a + b; }, 0); } -function audio_buffer_progressbar_update() { +function audio_buffer_progressbar_update(reportedValue) { if (audio_buffer_progressbar_update_disabled) return; - var audio_buffer_value = audio_buffers_total_length() / audio_context.sampleRate; + var audio_buffer_value = (reportedValue || audio_buffers_total_length()) / audio_context.sampleRate; audio_buffer_total_average_level_length++; audio_buffer_total_average_level = (audio_buffer_total_average_level * ((audio_buffer_total_average_level_length - 1) / audio_buffer_total_average_level_length)) + (audio_buffer_value / audio_buffer_total_average_level_length); var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; - var underrun = audio_buffers.length === 0; + var underrun = audio_buffer_value === 0; var text = "buffer"; if (overrun) { text = "overrun"; @@ -1665,21 +1672,14 @@ function parsehash() { function audio_preinit() { try { - window.AudioContext = window.AudioContext || window.webkitAudioContext; - audio_context = new AudioContext(); + var ctx = window.AudioContext || window.webkitAudioContext; + audio_context = new ctx(); } catch (e) { divlog('Your browser does not support Web Audio API, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser.', 1); return; } - if (audio_context.sampleRate < 44100 * 2) - audio_buffer_size = 4096; - else if (audio_context.sampleRate >= 44100 * 2 && audio_context.sampleRate < 44100 * 4) - audio_buffer_size = 4096 * 2; - else if (audio_context.sampleRate > 44100 * 4) - audio_buffer_size = 4096 * 4; - //we send our setup packet // TODO this should be moved to another stage of initialization parsehash(); @@ -1706,21 +1706,55 @@ function audio_init() { audio_debug_time_last_start = audio_debug_time_start; audio_buffer_current_count_debug = 0; + if (audio_context.sampleRate < 44100 * 2) + audio_buffer_size = 4096; + else if (audio_context.sampleRate >= 44100 * 2 && audio_context.sampleRate < 44100 * 4) + audio_buffer_size = 4096 * 2; + else if (audio_context.sampleRate > 44100 * 4) + audio_buffer_size = 4096 * 4; + //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js audio_initialized = 1; // only tell on_ws_recv() not to call it again - //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor - var createjsnode_function = (audio_context.createJavaScriptNode === undefined) ? audio_context.createScriptProcessor.bind(audio_context) : audio_context.createJavaScriptNode.bind(audio_context); - audio_node = createjsnode_function(audio_buffer_size, 0, 1); - audio_node.onaudioprocess = audio_onprocess; - audio_node.connect(audio_context.destination); + if (audio_context.audioWorklet) { + audio_context.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){ + audio_node = new AudioWorkletNode(audio_context, 'openwebrx-audio-processor', { + numberOfInputs: 0, + numberOfOutputs: 1, + outputChannelCount: [1], + processorOptions: { + maxLength: audio_buffer_maximal_length_sec, + reduceToLength: audio_buffer_decrease_to_on_overrun_sec + } + }); + audio_node.connect(audio_context.destination); + window.setInterval(function(){ + audio_node.port.postMessage(JSON.stringify({cmd:'getBuffers'})); + }, audio_flush_interval_ms); + audio_node.port.addEventListener('message', function(m){ + var json = JSON.parse(m.data); + if (json.buffersize) { + audio_buffer_progressbar_update_disabled = false; + audio_buffer_progressbar_update(json.buffersize); + } + }); + audio_node.port.start(); + }); + } else { + //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor + var createjsnode_function = (audio_context.createJavaScriptNode === undefined) ? audio_context.createScriptProcessor.bind(audio_context) : audio_context.createJavaScriptNode.bind(audio_context); + audio_node = createjsnode_function(audio_buffer_size, 0, 1); + audio_node.onaudioprocess = audio_onprocess; + audio_node.connect(audio_context.destination); + window.setInterval(audio_flush, audio_flush_interval_ms); + } + // --- Resampling --- //https://github.com/grantgalitz/XAudioJS/blob/master/XAudioServer.js //audio_resampler = new Resampler(audio_received_sample_rate, audio_context.sampleRate, 1, audio_buffer_size, true); //audio_input_buffer_size = audio_buffer_size*(audio_received_sample_rate/audio_context.sampleRate); webrx_set_param("audio_rate", audio_context.sampleRate); - window.setInterval(audio_flush, audio_flush_interval_ms); divlog('Web Audio API succesfully initialized, sample rate: ' + audio_context.sampleRate.toString() + " sps"); initialize_demodulator(); @@ -2224,7 +2258,8 @@ function debug_audio() { var audio_min_rate = audio_context.sampleRate * .25; progressbar_set(e("openwebrx-bar-audio-output"), audio_output_value / audio_max_rate, "Audio output [" + (audio_output_value / 1000).toFixed(1) + " ksps]", audio_output_value > audio_max_rate || audio_output_value < audio_min_rate); - audio_buffer_progressbar_update(); + // disable when audioworklets used + if (!audio_node.port) audio_buffer_progressbar_update(); var debug_ws_time_taken = (time_now - debug_ws_time_start) / 1000; var network_speed_value = debug_ws_data_received / debug_ws_time_taken; From a102ee181a455640fe8a88e36503738ce3a39770 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 19 Oct 2019 12:39:42 +0200 Subject: [PATCH 0493/2616] show wht method is being used in the log; fix console errors; --- htdocs/openwebrx.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 912bba007..85efbcaf8 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1716,7 +1716,9 @@ function audio_init() { //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js audio_initialized = 1; // only tell on_ws_recv() not to call it again + var tech; if (audio_context.audioWorklet) { + tech = "AudioWorklet"; audio_context.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){ audio_node = new AudioWorkletNode(audio_context, 'openwebrx-audio-processor', { numberOfInputs: 0, @@ -1741,6 +1743,7 @@ function audio_init() { audio_node.port.start(); }); } else { + tech = "ScriptProcessorNode"; //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor var createjsnode_function = (audio_context.createJavaScriptNode === undefined) ? audio_context.createScriptProcessor.bind(audio_context) : audio_context.createJavaScriptNode.bind(audio_context); audio_node = createjsnode_function(audio_buffer_size, 0, 1); @@ -1755,7 +1758,7 @@ function audio_init() { //audio_input_buffer_size = audio_buffer_size*(audio_received_sample_rate/audio_context.sampleRate); webrx_set_param("audio_rate", audio_context.sampleRate); - divlog('Web Audio API succesfully initialized, sample rate: ' + audio_context.sampleRate.toString() + " sps"); + divlog('Web Audio API succesfully initialized, using ' + tech + ', sample rate: ' + audio_context.sampleRate.toString() + " sps"); initialize_demodulator(); //hide log panel in a second (if user has not hidden it yet) @@ -2259,7 +2262,7 @@ function debug_audio() { progressbar_set(e("openwebrx-bar-audio-output"), audio_output_value / audio_max_rate, "Audio output [" + (audio_output_value / 1000).toFixed(1) + " ksps]", audio_output_value > audio_max_rate || audio_output_value < audio_min_rate); // disable when audioworklets used - if (!audio_node.port) audio_buffer_progressbar_update(); + if (audio_node && !audio_node.port) audio_buffer_progressbar_update(); var debug_ws_time_taken = (time_now - debug_ws_time_start) / 1000; var network_speed_value = debug_ws_data_received / debug_ws_time_taken; From 72329a8a2ab199c133df76b1c6e7ccd6c3a3726e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 19 Oct 2019 12:58:09 +0200 Subject: [PATCH 0494/2616] use a GainNode for volume control instead of custom code, thus improving the feedback --- htdocs/openwebrx.js | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 85efbcaf8..014667de3 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -108,7 +108,7 @@ function style_value(of_what, which) { } function updateVolume() { - volume = parseFloat(e("openwebrx-panel-volume").value) / 100; + gainNode.gain.value = parseFloat(e("openwebrx-panel-volume").value) / 100; } function toggleMute() { @@ -1519,7 +1519,7 @@ function divlog(what, is_error) { var audio_context; var audio_initialized = 0; -var volume = 1.0; +var gainNode; var volumeBeforeMute = 100.0; var mute = false; @@ -1536,13 +1536,6 @@ var audio_flush_interval_ms = 500; //the interval in which audio_flush() is call var audio_buffers = []; var audio_last_output_buffer; -function gain_ff(gain_value, data) //great! solved clicking! will have to move to sdr.js -{ - for (var i = 0; i < data.length; i++) - data[i] *= gain_value; - return data; -} - function audio_prepare(data) { if (!audio_node) return; var buffer = data; @@ -1550,7 +1543,7 @@ function audio_prepare(data) { //resampling & ADPCM buffer = audio_codec.decode(buffer); } - buffer = audio_resampler.process(gain_ff(volume, sdrjs.ConvertI16_F(buffer))); + buffer = audio_resampler.process(sdrjs.ConvertI16_F(buffer)); if (audio_node.port) { // AudioWorklets supported audio_node.port.postMessage(buffer, [buffer.buffer]); @@ -1717,6 +1710,8 @@ function audio_init() { audio_initialized = 1; // only tell on_ws_recv() not to call it again var tech; + gainNode = audio_context.createGain(); + gainNode.connect(audio_context.destination); if (audio_context.audioWorklet) { tech = "AudioWorklet"; audio_context.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){ @@ -1729,7 +1724,7 @@ function audio_init() { reduceToLength: audio_buffer_decrease_to_on_overrun_sec } }); - audio_node.connect(audio_context.destination); + audio_node.connect(gainNode); window.setInterval(function(){ audio_node.port.postMessage(JSON.stringify({cmd:'getBuffers'})); }, audio_flush_interval_ms); @@ -1748,10 +1743,13 @@ function audio_init() { var createjsnode_function = (audio_context.createJavaScriptNode === undefined) ? audio_context.createScriptProcessor.bind(audio_context) : audio_context.createJavaScriptNode.bind(audio_context); audio_node = createjsnode_function(audio_buffer_size, 0, 1); audio_node.onaudioprocess = audio_onprocess; - audio_node.connect(audio_context.destination); + audio_node.connect(gainNode); window.setInterval(audio_flush, audio_flush_interval_ms); } + //Synchronise volume with slider + updateVolume(); + // --- Resampling --- //https://github.com/grantgalitz/XAudioJS/blob/master/XAudioServer.js //audio_resampler = new Resampler(audio_received_sample_rate, audio_context.sampleRate, 1, audio_buffer_size, true); @@ -2200,9 +2198,6 @@ function openwebrx_init() { init_header(); bookmarks = new BookmarkBar(); - //Synchronise volume with slider - updateVolume(); - } function digimodes_init() { From cc32e28b36f6f1a061836cc629fb3d30b696f631 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 19 Oct 2019 13:09:41 +0200 Subject: [PATCH 0495/2616] use the raw object name --- htdocs/openwebrx.js | 47 +++++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 014667de3..4eced9442 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1709,11 +1709,29 @@ function audio_init() { //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js audio_initialized = 1; // only tell on_ws_recv() not to call it again - var tech; + // --- Resampling --- + webrx_set_param("audio_rate", audio_context.sampleRate); + + var finish = function() { + divlog('Web Audio API succesfully initialized, using ' + audio_node.constructor.name + ', sample rate: ' + audio_context.sampleRate.toString() + " sps"); + initialize_demodulator(); + + //hide log panel in a second (if user has not hidden it yet) + window.setTimeout(function () { + if (typeof e("openwebrx-panel-log").openwebrxHidden === "undefined" && !was_error) { + toggle_panel("openwebrx-panel-log"); + //animate(e("openwebrx-panel-log"),"opacity","",1,0,0.9,1000,60); + //window.setTimeout(function(){toggle_panel("openwebrx-panel-log");e("openwebrx-panel-log").style.opacity="1";},1200) + } + }, 2000); + }; + gainNode = audio_context.createGain(); gainNode.connect(audio_context.destination); + //Synchronise volume with slider + updateVolume(); + if (audio_context.audioWorklet) { - tech = "AudioWorklet"; audio_context.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){ audio_node = new AudioWorkletNode(audio_context, 'openwebrx-audio-processor', { numberOfInputs: 0, @@ -1736,38 +1754,17 @@ function audio_init() { } }); audio_node.port.start(); + finish(); }); } else { - tech = "ScriptProcessorNode"; //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor var createjsnode_function = (audio_context.createJavaScriptNode === undefined) ? audio_context.createScriptProcessor.bind(audio_context) : audio_context.createJavaScriptNode.bind(audio_context); audio_node = createjsnode_function(audio_buffer_size, 0, 1); audio_node.onaudioprocess = audio_onprocess; audio_node.connect(gainNode); window.setInterval(audio_flush, audio_flush_interval_ms); + finish(); } - - //Synchronise volume with slider - updateVolume(); - - // --- Resampling --- - //https://github.com/grantgalitz/XAudioJS/blob/master/XAudioServer.js - //audio_resampler = new Resampler(audio_received_sample_rate, audio_context.sampleRate, 1, audio_buffer_size, true); - //audio_input_buffer_size = audio_buffer_size*(audio_received_sample_rate/audio_context.sampleRate); - webrx_set_param("audio_rate", audio_context.sampleRate); - - divlog('Web Audio API succesfully initialized, using ' + tech + ', sample rate: ' + audio_context.sampleRate.toString() + " sps"); - initialize_demodulator(); - - //hide log panel in a second (if user has not hidden it yet) - window.setTimeout(function () { - if (typeof e("openwebrx-panel-log").openwebrxHidden === "undefined" && !was_error) { - toggle_panel("openwebrx-panel-log"); - //animate(e("openwebrx-panel-log"),"opacity","",1,0,0.9,1000,60); - //window.setTimeout(function(){toggle_panel("openwebrx-panel-log");e("openwebrx-panel-log").style.opacity="1";},1200) - } - }, 2000); - } function initialize_demodulator() { From 00c5467a8925e5c6b0102926a3da33ea0aa23a30 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 19 Oct 2019 18:09:50 +0200 Subject: [PATCH 0496/2616] implement a ringbuffer in the audioworklet to optimize runtimes --- htdocs/lib/AudioProcessor.js | 57 ++++++++++++++++-------------------- htdocs/openwebrx.js | 21 +++++-------- 2 files changed, 33 insertions(+), 45 deletions(-) diff --git a/htdocs/lib/AudioProcessor.js b/htdocs/lib/AudioProcessor.js index f0a70257f..378960845 100644 --- a/htdocs/lib/AudioProcessor.js +++ b/htdocs/lib/AudioProcessor.js @@ -3,7 +3,11 @@ class OwrxAudioProcessor extends AudioWorkletProcessor { super(options); this.maxLength = options.processorOptions.maxLength; this.reduceToLength = options.processorOptions.reduceToLength; - this.audio_buffers = []; + // initialize ringbuffer, make sure it aligns with the expected buffer size of 128 + this.bufferSize = Math.round(sampleRate * this.maxLength / 128) * 128 + this.audioBuffer = new Float32Array(this.bufferSize); + this.inPos = 0; + this.outPos = 0; this.port.addEventListener('message', (m) => { if (typeof(m.data) === 'string') { const json = JSON.parse(m.data); @@ -11,48 +15,39 @@ class OwrxAudioProcessor extends AudioWorkletProcessor { this.reportBuffers(); } } else { - this.audio_buffers.push(new Float32Array(m.data)); + // the ringbuffer size is aligned to the output buffer size, which means that the input buffers might + // need to wrap around the end of the ringbuffer, back to the start. + // it is better to have this processing here instead of in the time-critical process function. + if (this.inPos + m.data.length <= this.bufferSize) { + // we have enough space, so just copy data over. + this.audioBuffer.set(m.data, this.inPos); + } else { + // we don't have enough space, so we need to split the data. + const remaining = this.bufferSize - this.inPos; + this.audioBuffer.set(m.data.subarray(0, remaining), this.inPos); + this.audioBuffer.set(m.data.subarray(remaining)); + } + this.inPos = (this.inPos + m.data.length) % this.bufferSize; } }); this.port.addEventListener('messageerror', console.error); this.port.start(); } process(inputs, outputs, parameters) { - //console.time('audio::process'); + const samples = Math.min(128, this.remaining()); outputs[0].forEach((output) => { - let total = 0; - while (this.audio_buffers.length) { - const b = this.audio_buffers.shift(); - const newLength = total + b.length; - const ol = output.length; - // not enough space to fit all data, so splice and put back in the queue - if (newLength > ol) { - const tokeep = b.slice(0, ol - total); - output.set(tokeep, total); - const tobuffer = b.slice(ol - total, b.length); - this.audio_buffers.unshift(tobuffer); - break; - } else { - output.set(b, total); - } - total = newLength; - } + output.set(this.audioBuffer.subarray(this.outPos, this.outPos + samples)); }); - //console.timeEnd('audio::process'); + this.outPos = (this.outPos + samples) % this.bufferSize; return true; } - bufferLength() { - return this.audio_buffers.map(function(b){ return b.length; }).reduce(function(a, b){ return a + b; }, 0); + remaining() { + const mod = (this.inPos - this.outPos) % this.bufferSize; + if (mod >= 0) return mod; + return mod + this.bufferSize; } reportBuffers() { - var we_have_more_than = (sec) => { - return sec * sampleRate < this.bufferLength(); - }; - if (we_have_more_than(this.maxLength)) while (we_have_more_than(this.reduceToLength)) { - this.audio_buffers.shift(); - } - - this.port.postMessage(JSON.stringify({buffersize: this.bufferLength()})); + this.port.postMessage(JSON.stringify({buffersize: this.remaining()})); } } diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 4eced9442..1722b431b 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1586,8 +1586,6 @@ function audio_onprocess(e) { } } -var audio_buffer_progressbar_update_disabled = false; - var audio_buffer_total_average_level = 0; var audio_buffer_total_average_level_length = 0; @@ -1596,8 +1594,11 @@ function audio_buffers_total_length() { } function audio_buffer_progressbar_update(reportedValue) { - if (audio_buffer_progressbar_update_disabled) return; - var audio_buffer_value = (reportedValue || audio_buffers_total_length()) / audio_context.sampleRate; + var audio_buffer_value = reportedValue; + if (typeof(audio_buffer_value) === 'undefined') { + audio_buffer_value = audio_buffers_total_length(); + } + audio_buffer_value /= audio_context.sampleRate; audio_buffer_total_average_level_length++; audio_buffer_total_average_level = (audio_buffer_total_average_level * ((audio_buffer_total_average_level_length - 1) / audio_buffer_total_average_level_length)) + (audio_buffer_value / audio_buffer_total_average_level_length); var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; @@ -1609,14 +1610,7 @@ function audio_buffer_progressbar_update(reportedValue) { if (underrun) { text = "underrun"; } - if (overrun || underrun) { - audio_buffer_progressbar_update_disabled = true; - window.setTimeout(function () { - audio_buffer_progressbar_update_disabled = false; - audio_buffer_progressbar_update(); - }, 1000); - } - progressbar_set(e("openwebrx-bar-audio-buffer"), (underrun) ? 1 : audio_buffer_value, "Audio " + text + " [" + (audio_buffer_value).toFixed(1) + " s]", overrun || underrun); + progressbar_set(e("openwebrx-bar-audio-buffer"), audio_buffer_value, "Audio " + text + " [" + (audio_buffer_value).toFixed(1) + " s]", overrun || underrun); } @@ -1748,8 +1742,7 @@ function audio_init() { }, audio_flush_interval_ms); audio_node.port.addEventListener('message', function(m){ var json = JSON.parse(m.data); - if (json.buffersize) { - audio_buffer_progressbar_update_disabled = false; + if (typeof(json.buffersize) !== 'undefined') { audio_buffer_progressbar_update(json.buffersize); } }); From 91b8c55de9242b6a67f0a8eab50696b726e96183 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 20 Oct 2019 13:28:25 +0200 Subject: [PATCH 0497/2616] optimize --- htdocs/lib/AudioProcessor.js | 1 - htdocs/openwebrx.js | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/htdocs/lib/AudioProcessor.js b/htdocs/lib/AudioProcessor.js index 378960845..525299ff4 100644 --- a/htdocs/lib/AudioProcessor.js +++ b/htdocs/lib/AudioProcessor.js @@ -2,7 +2,6 @@ class OwrxAudioProcessor extends AudioWorkletProcessor { constructor(options){ super(options); this.maxLength = options.processorOptions.maxLength; - this.reduceToLength = options.processorOptions.reduceToLength; // initialize ringbuffer, make sure it aligns with the expected buffer size of 128 this.bufferSize = Math.round(sampleRate * this.maxLength / 128) * 128 this.audioBuffer = new Float32Array(this.bufferSize); diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 1722b431b..855018e01 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1546,7 +1546,7 @@ function audio_prepare(data) { buffer = audio_resampler.process(sdrjs.ConvertI16_F(buffer)); if (audio_node.port) { // AudioWorklets supported - audio_node.port.postMessage(buffer, [buffer.buffer]); + audio_node.port.postMessage(buffer); } else { audio_buffers.push(buffer); } @@ -1568,9 +1568,9 @@ function audio_onprocess(e) { var newLength = total + b.length; // not enough space to fit all data, so splice and put back in the queue if (newLength > audio_buffer_size) { - var tokeep = b.slice(0, audio_buffer_size - total); + var tokeep = b.subarray(0, audio_buffer_size - total); out.set(tokeep, total); - var tobuffer = b.slice(audio_buffer_size - total, b.length); + var tobuffer = b.subarray(audio_buffer_size - total, b.length); audio_buffers.unshift(tobuffer); break; } else { @@ -1732,8 +1732,7 @@ function audio_init() { numberOfOutputs: 1, outputChannelCount: [1], processorOptions: { - maxLength: audio_buffer_maximal_length_sec, - reduceToLength: audio_buffer_decrease_to_on_overrun_sec + maxLength: audio_buffer_maximal_length_sec } }); audio_node.connect(gainNode); From 13d7686258c3d59a771ff4ab10ad0195f9921993 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 20 Oct 2019 18:53:23 +0200 Subject: [PATCH 0498/2616] refactor all the audio stuff into classes and a separate file --- htdocs/index.html | 3 +- htdocs/lib/AudioEngine.js | 214 +++++++++++++++++++ htdocs/lib/AudioProcessor.js | 3 +- htdocs/openwebrx.js | 391 ++++++++--------------------------- owrx/connection.py | 2 + 5 files changed, 305 insertions(+), 308 deletions(-) create mode 100644 htdocs/lib/AudioEngine.js diff --git a/htdocs/index.html b/htdocs/index.html index 30dcce3fb..c5d26c481 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -28,6 +28,7 @@ + @@ -209,7 +210,7 @@
    -
    +


    Start OpenWebRX diff --git a/htdocs/lib/AudioEngine.js b/htdocs/lib/AudioEngine.js new file mode 100644 index 000000000..aa79e7209 --- /dev/null +++ b/htdocs/lib/AudioEngine.js @@ -0,0 +1,214 @@ +// this controls if the new AudioWorklet API should be used if available. +// the engine will still fall back to the ScriptProcessorNode if this is set to true but not available in the browser. +var useAudioWorklets = true; + +function AudioEngine(maxBufferLength, audioReporter) { + this.audioReporter = audioReporter; + this.resetStats(); + var ctx = window.AudioContext || window.webkitAudioContext; + if (!ctx) { + return; + } + this.audioContext = new ctx(); + this.allowed = this.audioContext.state === 'running'; + this.started = false; + + this.audioCodec = new sdrjs.ImaAdpcm(); + this.compression = 'none'; + + this.setupResampling(); + this.resampler = new sdrjs.RationalResamplerFF(this.resamplingFactor, 1); + + this.maxBufferSize = maxBufferLength * this.getSampleRate(); +} + +AudioEngine.prototype.start = function(callback) { + var me = this; + if (me.resamplingFactor === 0) return; //if failed to find a valid resampling factor... + if (me.started) { + if (callback) callback(false); + return; + } + + me.audioContext.resume().then(function(){ + me.allowed = me.audioContext.state === 'running'; + if (!me.allowed) { + if (callback) callback(false); + return; + } + me.started = true; + + me.gainNode = me.audioContext.createGain(); + me.gainNode.connect(me.audioContext.destination); + + if (useAudioWorklets && me.audioContext.audioWorklet) { + me.audioContext.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){ + me.audioNode = new AudioWorkletNode(me.audioContext, 'openwebrx-audio-processor', { + numberOfInputs: 0, + numberOfOutputs: 1, + outputChannelCount: [1], + processorOptions: { + maxBufferSize: me.maxBufferSize + } + }); + me.audioNode.connect(me.gainNode); + me.audioNode.port.addEventListener('message', function(m){ + var json = JSON.parse(m.data); + if (typeof(json.buffersize) !== 'undefined') { + me.audioReporter(json); + } + }); + me.audioNode.port.start(); + if (callback) callback(true, 'AudioWorklet'); + }); + } else { + me.audioBuffers = []; + + if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefox does + AudioBuffer.prototype.copyToChannel = function (input, channel) //input is Float32Array + { + var cd = this.getChannelData(channel); + for (var i = 0; i < input.length; i++) cd[i] = input[i]; + } + } + + var bufferSize; + if (me.audioContext.sampleRate < 44100 * 2) + bufferSize = 4096; + else if (me.audioContext.sampleRate >= 44100 * 2 && me.audioContext.sampleRate < 44100 * 4) + bufferSize = 4096 * 2; + else if (me.audioContext.sampleRate > 44100 * 4) + bufferSize = 4096 * 4; + + + function audio_onprocess(e) { + var total = 0; + var out = new Float32Array(bufferSize); + while (me.audioBuffers.length) { + var b = me.audioBuffers.shift(); + var newLength = total + b.length; + // not enough space to fit all data, so splice and put back in the queue + if (newLength > bufferSize) { + var tokeep = b.subarray(0, bufferSize - total); + out.set(tokeep, total); + var tobuffer = b.subarray(bufferSize - total, b.length); + me.audioBuffers.unshift(tobuffer); + break; + } else { + out.set(b, total); + } + total = newLength; + } + + e.outputBuffer.copyToChannel(out, 0); + + } + + //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor + var method = 'createScriptProcessor'; + if (me.audioContext.createJavaScriptNode) { + method = 'createJavaScriptNode'; + } + me.audioNode = me.audioContext[method](bufferSize, 0, 1); + me.audioNode.onaudioprocess = audio_onprocess; + me.audioNode.connect(me.gainNode); + if (callback) callback(true, 'ScriptProcessorNode'); + } + + setInterval(me.reportStats.bind(me), 1000); + }); +} + +AudioEngine.prototype.isAllowed = function() { + return this.allowed; +} + +AudioEngine.prototype.reportStats = function() { + var stats = {} + if (this.audioNode.port) { + this.audioNode.port.postMessage(JSON.stringify({cmd:'getBuffers'})); + } else { + stats.buffersize = this.getBuffersize(); + } + stats.audioRate = this.stats.audioSamples; + var elapsed = new Date() - this.stats.startTime; + stats.audioByteRate = this.stats.audioBytes * 1000 / elapsed + this.audioReporter(stats); + + // sample rate is just measuring the last seconds + this.stats.audioSamples = 0; +} + +AudioEngine.prototype.resetStats = function() { + this.stats = { + startTime: new Date(), + audioBytes: 0, + audioSamples: 0 + }; +} + +AudioEngine.prototype.setupResampling = function() { //both at the server and the client + var output_range_max = 12000; + var output_range_min = 8000; + var targetRate = this.audioContext.sampleRate; + var i = 1; + while (true) { + var audio_server_output_rate = Math.floor(targetRate / i); + if (audio_server_output_rate < output_range_min) { + this.resamplingFactor = 0; + this.outputRate = 0; + divlog('Your audio card sampling rate (' + targetRate + ') is not supported.
    Please change your operating system default settings in order to fix this.', 1); + break; + } else if (audio_server_output_rate >= output_range_min && audio_server_output_rate <= output_range_max) { + this.resamplingFactor = i; + this.outputRate = audio_server_output_rate; + break; //okay, we're done + } + i++; + } +} + +AudioEngine.prototype.getOutputRate = function() { + return this.outputRate; +} + +AudioEngine.prototype.getSampleRate = function() { + return this.audioContext.sampleRate; +} + +AudioEngine.prototype.pushAudio = function(data) { + if (!this.audioNode) return; + this.stats.audioBytes += data.byteLength; + var buffer; + if (this.compression === "adpcm") { + //resampling & ADPCM + buffer = this.audioCodec.decode(new Uint8Array(data)); + } else { + buffer = new Int16Array(data); + } + buffer = this.resampler.process(sdrjs.ConvertI16_F(buffer)); + this.stats.audioSamples += buffer.length; + if (this.audioNode.port) { + // AudioWorklets supported + this.audioNode.port.postMessage(buffer); + } else { + // silently drop excess samples + if (this.getBuffersize() + buffer.length <= this.maxBufferSize) { + this.audioBuffers.push(buffer); + } + } +} + +AudioEngine.prototype.setCompression = function(compression) { + this.compression = compression; +} + +AudioEngine.prototype.setVolume = function(volume) { + this.gainNode.gain.value = volume; +} + +AudioEngine.prototype.getBuffersize = function() { + // only available when using ScriptProcessorNode + if (!this.audioBuffers) return 0; + return this.audioBuffers.map(function(b){ return b.length; }).reduce(function(a, b){ return a + b; }, 0); +} diff --git a/htdocs/lib/AudioProcessor.js b/htdocs/lib/AudioProcessor.js index 525299ff4..0902c5034 100644 --- a/htdocs/lib/AudioProcessor.js +++ b/htdocs/lib/AudioProcessor.js @@ -1,9 +1,8 @@ class OwrxAudioProcessor extends AudioWorkletProcessor { constructor(options){ super(options); - this.maxLength = options.processorOptions.maxLength; // initialize ringbuffer, make sure it aligns with the expected buffer size of 128 - this.bufferSize = Math.round(sampleRate * this.maxLength / 128) * 128 + this.bufferSize = Math.round(options.processorOptions.maxBufferSize / 128) * 128 this.audioBuffer = new Float32Array(this.bufferSize); this.inPos = 0; this.outPos = 0; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 855018e01..58ff2d538 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -30,16 +30,12 @@ function arrayBufferToString(buf) { var bandwidth; var center_freq; -var audio_buffer_current_size_debug = 0; -var audio_buffer_current_count_debug = 0; var fft_size; var fft_fps; var fft_compression = "none"; var fft_codec = new sdrjs.ImaAdpcm(); -var audio_compression = "none"; var waterfall_setup_done = 0; var secondary_fft_size; -var audio_allowed; var rx_photo_state = 1; function e(what) { @@ -108,7 +104,7 @@ function style_value(of_what, which) { } function updateVolume() { - gainNode.gain.value = parseFloat(e("openwebrx-panel-volume").value) / 100; + audioEngine.setVolume(parseFloat(e("openwebrx-panel-volume").value) / 100); } function toggleMute() { @@ -406,8 +402,8 @@ function Demodulator_default_analog(offset_frequency, subtype) { this.subtype = subtype; this.filter = { min_passband: 100, - high_cut_limit: (audio_server_output_rate / 2) - 1, //audio_context.sampleRate/2, - low_cut_limit: (-audio_server_output_rate / 2) + 1 //-audio_context.sampleRate/2 + high_cut_limit: (audioEngine.getOutputRate() / 2) - 1, + low_cut_limit: (-audioEngine.getOutputRate() / 2) + 1 }; //Subtypes only define some filter parameters and the mod string sent to server, //so you may set these parameters in your custom child class. @@ -689,7 +685,8 @@ function scale_px_from_freq(f, range) { function get_visible_freq_range() { var out = {}; var fcalc = function (x) { - return Math.round(((-zoom_offset_px + x) / canvases[0].clientWidth) * bandwidth) + (center_freq - bandwidth / 2); + var canvasWidth = canvas_container.clientWidth * zoom_levels[zoom_level]; + return Math.round(((-zoom_offset_px + x) / canvasWidth) * bandwidth) + (center_freq - bandwidth / 2); }; out.start = fcalc(0); out.center = fcalc(canvas_container.clientWidth / 2); @@ -1063,30 +1060,8 @@ function resize_waterfall_container(check_init) { } -var audio_server_output_rate = 11025; -var audio_client_resampling_factor = 4; - - -function audio_calculate_resampling(targetRate) { //both at the server and the client - var output_range_max = 12000; - var output_range_min = 8000; - var i = 1; - while (true) { - audio_server_output_rate = Math.floor(targetRate / i); - if (audio_server_output_rate < output_range_min) { - audio_client_resampling_factor = audio_server_output_rate = 0; - divlog("Your audio card sampling rate (" + targetRate.toString() + ") is not supported.
    Please change your operating system default settings in order to fix this.", 1); - } - if (audio_server_output_rate >= output_range_min && audio_server_output_rate <= output_range_max) break; //okay, we're done - i++; - } - audio_client_resampling_factor = i; - console.log("audio_calculate_resampling() :: " + audio_client_resampling_factor.toString() + ", " + audio_server_output_rate.toString()); -} - - var debug_ws_data_received = 0; -var debug_ws_time_start = 0; +var debug_ws_time_start; var max_clients_num = 0; var client_num = 0; var currentprofile; @@ -1096,7 +1071,7 @@ var COMPRESS_FFT_PAD_N = 10; //should be the same as in csdr.c function on_ws_recv(evt) { if (typeof evt.data === 'string') { // text messages - debug_ws_data_received += evt.data.length / 1000; + debug_ws_data_received += evt.data.length; if (evt.data.substr(0, 16) === "CLIENT DE SERVER") { divlog("Server acknowledged WebSocket connection."); @@ -1106,19 +1081,20 @@ function on_ws_recv(evt) { switch (json.type) { case "config": var config = json['value']; - window.waterfall_colors = config['waterfall_colors']; - window.waterfall_min_level_default = config['waterfall_min_level']; - window.waterfall_max_level_default = config['waterfall_max_level']; - window.waterfall_auto_level_margin = config['waterfall_auto_level_margin']; + waterfall_colors = config['waterfall_colors']; + waterfall_min_level_default = config['waterfall_min_level']; + waterfall_max_level_default = config['waterfall_max_level']; + waterfall_auto_level_margin = config['waterfall_auto_level_margin']; waterfallColorsDefault(); - window.starting_mod = config['start_mod']; - window.starting_offset_frequency = config['start_offset_freq']; + starting_mod = config['start_mod']; + starting_offset_frequency = config['start_offset_freq']; bandwidth = config['samp_rate']; center_freq = config['center_freq'] + config['lfo_offset']; fft_size = config['fft_size']; fft_fps = config['fft_fps']; - audio_compression = config['audio_compression']; + var audio_compression = config['audio_compression']; + audioEngine.setCompression(audio_compression); divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + "."); fft_compression = config['fft_compression']; divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + "."); @@ -1129,20 +1105,14 @@ function on_ws_recv(evt) { mathbox_waterfall_history_length = config['mathbox_waterfall_history_length']; waterfall_init(); - audio_preinit(); + initialize_demodulator(); bookmarks.loadLocalBookmarks(); - if (audio_allowed) { - if (audio_initialized) { - initialize_demodulator(); - } else { - audio_init(); - } - } waterfall_clear(); currentprofile = config['profile_id']; $('#openwebrx-sdr-profiles-listbox').val(currentprofile); + break; case "secondary_config": var s = json['value']; @@ -1222,7 +1192,7 @@ function on_ws_recv(evt) { } } else if (evt.data instanceof ArrayBuffer) { // binary messages - debug_ws_data_received += evt.data.byteLength / 1000; + debug_ws_data_received += evt.data.byteLength; var type = new Uint8Array(evt.data, 0, 1)[0]; var data = evt.data.slice(1); @@ -1247,15 +1217,7 @@ function on_ws_recv(evt) { break; case 2: // audio data - var audio_data; - if (audio_compression === "adpcm") { - audio_data = new Uint8Array(data); - } else { - audio_data = new Int16Array(data); - } - audio_prepare(audio_data); - audio_buffer_current_size_debug += audio_data.length; - if (!(ios || is_chrome) && (audio_initialized === 0)) audio_init(); + audioEngine.pushAudio(data); break; case 3: // secondary FFT @@ -1498,8 +1460,13 @@ function on_ws_opened() { ws.send("SERVER DE CLIENT client=openwebrx.js type=receiver"); divlog("WebSocket opened to " + ws.url); debug_ws_data_received = 0; - debug_ws_time_start = new Date().getTime(); + debug_ws_time_start = new Date(); reconnect_timeout = false; + ws.send(JSON.stringify({ + "type": "dspcontrol", + "action": "start", + "params": {"output_rate": audioEngine.getOutputRate()} + })); } var was_error = 0; @@ -1517,114 +1484,11 @@ function divlog(what, is_error) { nano.nanoScroller({scroll: 'bottom'}); } -var audio_context; -var audio_initialized = 0; -var gainNode; var volumeBeforeMute = 100.0; var mute = false; -var audio_resampler; -var audio_codec = new sdrjs.ImaAdpcm(); -var audio_node; - // Optimalise these if audio lags or is choppy: -var audio_buffer_size; var audio_buffer_maximal_length_sec = 1; //actual number of samples are calculated from sample rate -var audio_buffer_decrease_to_on_overrun_sec = 0.8; -var audio_flush_interval_ms = 500; //the interval in which audio_flush() is called - -var audio_buffers = []; -var audio_last_output_buffer; - -function audio_prepare(data) { - if (!audio_node) return; - var buffer = data; - if (audio_compression === "adpcm") { - //resampling & ADPCM - buffer = audio_codec.decode(buffer); - } - buffer = audio_resampler.process(sdrjs.ConvertI16_F(buffer)); - if (audio_node.port) { - // AudioWorklets supported - audio_node.port.postMessage(buffer); - } else { - audio_buffers.push(buffer); - } -} - -if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefox does - AudioBuffer.prototype.copyToChannel = function (input, channel) //input is Float32Array - { - var cd = this.getChannelData(channel); - for (var i = 0; i < input.length; i++) cd[i] = input[i]; - } -} - -function audio_onprocess(e) { - var total = 0; - var out = new Float32Array(audio_buffer_size); - while (audio_buffers.length) { - var b = audio_buffers.shift(); - var newLength = total + b.length; - // not enough space to fit all data, so splice and put back in the queue - if (newLength > audio_buffer_size) { - var tokeep = b.subarray(0, audio_buffer_size - total); - out.set(tokeep, total); - var tobuffer = b.subarray(audio_buffer_size - total, b.length); - audio_buffers.unshift(tobuffer); - break; - } else { - out.set(b, total); - } - total = newLength; - } - - e.outputBuffer.copyToChannel(out, 0); - - if (!audio_buffers.length) { - audio_buffer_progressbar_update(); - } -} - -var audio_buffer_total_average_level = 0; -var audio_buffer_total_average_level_length = 0; - -function audio_buffers_total_length() { - return audio_buffers.map(function(b){ return b.length; }).reduce(function(a, b){ return a + b; }, 0); -} - -function audio_buffer_progressbar_update(reportedValue) { - var audio_buffer_value = reportedValue; - if (typeof(audio_buffer_value) === 'undefined') { - audio_buffer_value = audio_buffers_total_length(); - } - audio_buffer_value /= audio_context.sampleRate; - audio_buffer_total_average_level_length++; - audio_buffer_total_average_level = (audio_buffer_total_average_level * ((audio_buffer_total_average_level_length - 1) / audio_buffer_total_average_level_length)) + (audio_buffer_value / audio_buffer_total_average_level_length); - var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; - var underrun = audio_buffer_value === 0; - var text = "buffer"; - if (overrun) { - text = "overrun"; - } - if (underrun) { - text = "underrun"; - } - progressbar_set(e("openwebrx-bar-audio-buffer"), audio_buffer_value, "Audio " + text + " [" + (audio_buffer_value).toFixed(1) + " s]", overrun || underrun); -} - - -function audio_flush() { - var flushed = false; - var we_have_more_than = function (sec) { - return sec * audio_context.sampleRate < audio_buffers_total_length(); - }; - if (we_have_more_than(audio_buffer_maximal_length_sec)) while (we_have_more_than(audio_buffer_decrease_to_on_overrun_sec)) { - if (!flushed) audio_buffer_progressbar_update(); - flushed = true; - audio_buffers.shift(); - } -} function webrx_set_param(what, value) { var params = {}; @@ -1632,11 +1496,10 @@ function webrx_set_param(what, value) { ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); } -var starting_mute = false; var starting_offset_frequency; var starting_mod; -function parsehash() { +function parseHash() { var h; if (h = window.location.hash) { h.substring(1).split(",").forEach(function (x) { @@ -1657,106 +1520,21 @@ function parsehash() { } } -function audio_preinit() { - try { - var ctx = window.AudioContext || window.webkitAudioContext; - audio_context = new ctx(); - } - catch (e) { - divlog('Your browser does not support Web Audio API, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser.', 1); - return; - } - - //we send our setup packet - // TODO this should be moved to another stage of initialization - parsehash(); +function onAudioStart(success, apiType){ + divlog('Web Audio API succesfully initialized, using ' + apiType + ' API, sample rate: ' + audioEngine.getSampleRate() + " Hz"); - if (!audio_resampler) { - audio_calculate_resampling(audio_context.sampleRate); - audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor, 1); - } + // canvas_container is set after waterfall_init() has been called. we cannot initialize before. + if (canvas_container) initialize_demodulator(); - ws.send(JSON.stringify({ - "type": "dspcontrol", - "action": "start", - "params": {"output_rate": audio_server_output_rate} - })); -} - -function audio_init() { - if (is_chrome) audio_context.resume(); - if (starting_mute) toggleMute(); - - if (audio_client_resampling_factor === 0) return; //if failed to find a valid resampling factor... - - audio_debug_time_start = (new Date()).getTime(); - audio_debug_time_last_start = audio_debug_time_start; - audio_buffer_current_count_debug = 0; - - if (audio_context.sampleRate < 44100 * 2) - audio_buffer_size = 4096; - else if (audio_context.sampleRate >= 44100 * 2 && audio_context.sampleRate < 44100 * 4) - audio_buffer_size = 4096 * 2; - else if (audio_context.sampleRate > 44100 * 4) - audio_buffer_size = 4096 * 4; - - //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js - audio_initialized = 1; // only tell on_ws_recv() not to call it again - - // --- Resampling --- - webrx_set_param("audio_rate", audio_context.sampleRate); - - var finish = function() { - divlog('Web Audio API succesfully initialized, using ' + audio_node.constructor.name + ', sample rate: ' + audio_context.sampleRate.toString() + " sps"); - initialize_demodulator(); - - //hide log panel in a second (if user has not hidden it yet) - window.setTimeout(function () { - if (typeof e("openwebrx-panel-log").openwebrxHidden === "undefined" && !was_error) { - toggle_panel("openwebrx-panel-log"); - //animate(e("openwebrx-panel-log"),"opacity","",1,0,0.9,1000,60); - //window.setTimeout(function(){toggle_panel("openwebrx-panel-log");e("openwebrx-panel-log").style.opacity="1";},1200) - } - }, 2000); - }; + //hide log panel in a second (if user has not hidden it yet) + window.setTimeout(function () { + if (typeof e("openwebrx-panel-log").openwebrxHidden === "undefined" && !was_error) { + toggle_panel("openwebrx-panel-log"); + } + }, 2000); - gainNode = audio_context.createGain(); - gainNode.connect(audio_context.destination); //Synchronise volume with slider updateVolume(); - - if (audio_context.audioWorklet) { - audio_context.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){ - audio_node = new AudioWorkletNode(audio_context, 'openwebrx-audio-processor', { - numberOfInputs: 0, - numberOfOutputs: 1, - outputChannelCount: [1], - processorOptions: { - maxLength: audio_buffer_maximal_length_sec - } - }); - audio_node.connect(gainNode); - window.setInterval(function(){ - audio_node.port.postMessage(JSON.stringify({cmd:'getBuffers'})); - }, audio_flush_interval_ms); - audio_node.port.addEventListener('message', function(m){ - var json = JSON.parse(m.data); - if (typeof(json.buffersize) !== 'undefined') { - audio_buffer_progressbar_update(json.buffersize); - } - }); - audio_node.port.start(); - finish(); - }); - } else { - //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor - var createjsnode_function = (audio_context.createJavaScriptNode === undefined) ? audio_context.createScriptProcessor.bind(audio_context) : audio_context.createJavaScriptNode.bind(audio_context); - audio_node = createjsnode_function(audio_buffer_size, 0, 1); - audio_node.onaudioprocess = audio_onprocess; - audio_node.connect(gainNode); - window.setInterval(audio_flush, audio_flush_interval_ms); - finish(); - } } function initialize_demodulator() { @@ -1772,12 +1550,6 @@ function initialize_demodulator() { var reconnect_timeout = false; function on_ws_closed() { - try { - audio_node.disconnect(); - } - catch (dont_care) { - } - audio_initialized = 0; if (reconnect_timeout) { // max value: roundabout 8 and a half minutes reconnect_timeout = Math.min(reconnect_timeout * 2, 512000); @@ -2168,25 +1940,66 @@ function init_header() { }); } +function audio_buffer_progressbar_update(buffersize) { + var audio_buffer_value = buffersize / audioEngine.getSampleRate(); + var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; + var underrun = audio_buffer_value === 0; + var text = "buffer"; + if (overrun) { + text = "overrun"; + } + if (underrun) { + text = "underrun"; + } + progressbar_set(e("openwebrx-bar-audio-buffer"), audio_buffer_value, "Audio " + text + " [" + (audio_buffer_value).toFixed(1) + " s]", overrun || underrun); +} + +function updateNetworkStats() { + var elapsed = (new Date() - debug_ws_time_start) / 1000; + var network_speed_value = (debug_ws_data_received / 1000) / elapsed; + progressbar_set(e("openwebrx-bar-network-speed"), network_speed_value * 8 / 2000, "Network usage [" + (network_speed_value * 8).toFixed(1) + " kbps]", false); +} + +function audioReporter(stats) { + if (typeof(stats.buffersize) !== 'undefined') { + audio_buffer_progressbar_update(stats.buffersize); + } + + if (typeof(stats.audioByteRate) !== 'undefined') { + var audio_speed_value = stats.audioByteRate * 8; + progressbar_set(e("openwebrx-bar-audio-speed"), audio_speed_value / 500000, "Audio stream [" + (audio_speed_value / 1000).toFixed(0) + " kbps]", false); + } + + if (typeof(stats.audioRate) !== 'undefined') { + var audio_max_rate = audioEngine.getSampleRate() * 1.25; + var audio_min_rate = audioEngine.getSampleRate() * .25; + progressbar_set(e("openwebrx-bar-audio-output"), stats.audioRate / audio_max_rate, "Audio output [" + (stats.audioRate / 1000).toFixed(1) + " ksps]", stats.audioRate > audio_max_rate || stats.audioRate < audio_min_rate); + } +} + var bookmarks; +var audioEngine; function openwebrx_init() { - if (ios || is_chrome) e("openwebrx-big-grey").style.display = "table-cell"; - var opb = e("openwebrx-play-button-text"); - opb.style.marginTop = (window.innerHeight / 2 - opb.clientHeight / 2).toString() + "px"; + audioEngine = new AudioEngine(audio_buffer_maximal_length_sec, audioReporter); + if (!audioEngine.isAllowed()) { + e("openwebrx-big-grey").style.display = "table-cell"; + var opb = e("openwebrx-play-button-text"); + opb.style.marginTop = (window.innerHeight / 2 - opb.clientHeight / 2).toString() + "px"; + } else { + audioEngine.start(onAudioStart); + } init_rx_photo(); open_websocket(); + setInterval(updateNetworkStats, 1000); secondary_demod_init(); digimodes_init(); place_panels(first_show_panel); - window.setTimeout(function () { - window.setInterval(debug_audio, 1000); - }, 1000); window.addEventListener("resize", openwebrx_resize); check_top_bar_congestion(); init_header(); bookmarks = new BookmarkBar(); - + parseHash(); } function digimodes_init() { @@ -2210,14 +2023,13 @@ function update_dmr_timeslot_filtering() { webrx_set_param("dmr_filter", filter); } -function iosPlayButtonClick() { +function playButtonClick() { //On iOS, we can only start audio from a click or touch event. - audio_init(); + audioEngine.start(onAudioStart); e("openwebrx-big-grey").style.opacity = 0; window.setTimeout(function () { e("openwebrx-big-grey").style.display = "none"; }, 1100); - audio_allowed = 1; } var rt = function (s, n) { @@ -2226,37 +2038,6 @@ var rt = function (s, n) { }); }; -var audio_debug_time_start = 0; -var audio_debug_time_last_start = 0; - -function debug_audio() { - if (audio_debug_time_start === 0) return; //audio_init has not been called - var time_now = (new Date()).getTime(); - var audio_debug_time_since_last_call = (time_now - audio_debug_time_last_start) / 1000; - audio_debug_time_last_start = time_now; //now - var audio_debug_time_taken = (time_now - audio_debug_time_start) / 1000; - var kbps_mult = (audio_compression === "adpcm") ? 8 : 16; - - var audio_speed_value = audio_buffer_current_size_debug * kbps_mult / audio_debug_time_since_last_call; - progressbar_set(e("openwebrx-bar-audio-speed"), audio_speed_value / 500000, "Audio stream [" + (audio_speed_value / 1000).toFixed(0) + " kbps]", false); - - var audio_output_value = (audio_buffer_current_count_debug * audio_buffer_size) / audio_debug_time_taken; - var audio_max_rate = audio_context.sampleRate * 1.25; - var audio_min_rate = audio_context.sampleRate * .25; - progressbar_set(e("openwebrx-bar-audio-output"), audio_output_value / audio_max_rate, "Audio output [" + (audio_output_value / 1000).toFixed(1) + " ksps]", audio_output_value > audio_max_rate || audio_output_value < audio_min_rate); - - // disable when audioworklets used - if (audio_node && !audio_node.port) audio_buffer_progressbar_update(); - - var debug_ws_time_taken = (time_now - debug_ws_time_start) / 1000; - var network_speed_value = debug_ws_data_received / debug_ws_time_taken; - progressbar_set(e("openwebrx-bar-network-speed"), network_speed_value * 8 / 2000, "Network usage [" + (network_speed_value * 8).toFixed(1) + " kbps]", false); - - audio_buffer_current_size_debug = 0; - - if (waterfall_measure_minmax) waterfall_measure_minmax_print(); -} - // ======================================================== // ======================= PANELS ======================= // ======================================================== diff --git a/owrx/connection.py b/owrx/connection.py index 4ee8734ac..b4b827bed 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -157,6 +157,8 @@ def setSdr(self, id=None): self.sdr = next + self.startDsp() + # send initial config configProps = ( self.sdr.getProps() From dd7d262bd3dfdba27c5c59a56bf6e5c688a6924b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 20 Oct 2019 23:38:58 +0200 Subject: [PATCH 0499/2616] fixing some issues with the IDE --- htdocs/lib/AudioEngine.js | 26 +++++++++++++------------- htdocs/lib/AudioProcessor.js | 6 +++--- htdocs/openwebrx.js | 8 ++++---- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/htdocs/lib/AudioEngine.js b/htdocs/lib/AudioEngine.js index aa79e7209..0445dd3b5 100644 --- a/htdocs/lib/AudioEngine.js +++ b/htdocs/lib/AudioEngine.js @@ -117,14 +117,14 @@ AudioEngine.prototype.start = function(callback) { setInterval(me.reportStats.bind(me), 1000); }); -} +}; AudioEngine.prototype.isAllowed = function() { return this.allowed; -} +}; AudioEngine.prototype.reportStats = function() { - var stats = {} + var stats = {}; if (this.audioNode.port) { this.audioNode.port.postMessage(JSON.stringify({cmd:'getBuffers'})); } else { @@ -132,12 +132,12 @@ AudioEngine.prototype.reportStats = function() { } stats.audioRate = this.stats.audioSamples; var elapsed = new Date() - this.stats.startTime; - stats.audioByteRate = this.stats.audioBytes * 1000 / elapsed + stats.audioByteRate = this.stats.audioBytes * 1000 / elapsed; this.audioReporter(stats); // sample rate is just measuring the last seconds this.stats.audioSamples = 0; -} +}; AudioEngine.prototype.resetStats = function() { this.stats = { @@ -145,7 +145,7 @@ AudioEngine.prototype.resetStats = function() { audioBytes: 0, audioSamples: 0 }; -} +}; AudioEngine.prototype.setupResampling = function() { //both at the server and the client var output_range_max = 12000; @@ -166,15 +166,15 @@ AudioEngine.prototype.setupResampling = function() { //both at the server and th } i++; } -} +}; AudioEngine.prototype.getOutputRate = function() { return this.outputRate; -} +}; AudioEngine.prototype.getSampleRate = function() { return this.audioContext.sampleRate; -} +}; AudioEngine.prototype.pushAudio = function(data) { if (!this.audioNode) return; @@ -197,18 +197,18 @@ AudioEngine.prototype.pushAudio = function(data) { this.audioBuffers.push(buffer); } } -} +}; AudioEngine.prototype.setCompression = function(compression) { this.compression = compression; -} +}; AudioEngine.prototype.setVolume = function(volume) { this.gainNode.gain.value = volume; -} +}; AudioEngine.prototype.getBuffersize = function() { // only available when using ScriptProcessorNode if (!this.audioBuffers) return 0; return this.audioBuffers.map(function(b){ return b.length; }).reduce(function(a, b){ return a + b; }, 0); -} +}; diff --git a/htdocs/lib/AudioProcessor.js b/htdocs/lib/AudioProcessor.js index 0902c5034..24c7b9ed7 100644 --- a/htdocs/lib/AudioProcessor.js +++ b/htdocs/lib/AudioProcessor.js @@ -2,14 +2,14 @@ class OwrxAudioProcessor extends AudioWorkletProcessor { constructor(options){ super(options); // initialize ringbuffer, make sure it aligns with the expected buffer size of 128 - this.bufferSize = Math.round(options.processorOptions.maxBufferSize / 128) * 128 + this.bufferSize = Math.round(options.processorOptions.maxBufferSize / 128) * 128; this.audioBuffer = new Float32Array(this.bufferSize); this.inPos = 0; this.outPos = 0; this.port.addEventListener('message', (m) => { if (typeof(m.data) === 'string') { const json = JSON.parse(m.data); - if (json.cmd && json.cmd == 'getBuffers') { + if (json.cmd && json.cmd === 'getBuffers') { this.reportBuffers(); } } else { @@ -31,7 +31,7 @@ class OwrxAudioProcessor extends AudioWorkletProcessor { this.port.addEventListener('messageerror', console.error); this.port.start(); } - process(inputs, outputs, parameters) { + process(inputs, outputs) { const samples = Math.min(128, this.remaining()); outputs[0].forEach((output) => { output.set(this.audioBuffer.subarray(this.outPos, this.outPos + samples)); diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 58ff2d538..114cbefdb 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -157,6 +157,10 @@ function updateSquelch() { var waterfall_min_level; var waterfall_max_level; +var waterfall_min_level_default; +var waterfall_max_level_default; +var waterfall_colors; +var waterfall_auto_level_margin; function updateWaterfallColors(which) { var wfmax = e("openwebrx-waterfall-color-max"); @@ -1452,10 +1456,6 @@ function waterfall_measure_minmax_do(what) { waterfall_measure_minmax_max = Math.max(waterfall_measure_minmax_max, Math.max.apply(Math, what)); } -function waterfall_measure_minmax_print() { - console.log("Waterfall | min = " + waterfall_measure_minmax_min.toString() + " dB | max = " + waterfall_measure_minmax_max.toString() + " dB"); -} - function on_ws_opened() { ws.send("SERVER DE CLIENT client=openwebrx.js type=receiver"); divlog("WebSocket opened to " + ws.url); From 7ef0ef0d7c9c3403636942ca12b7f5dbba0f58f3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 20 Oct 2019 23:48:49 +0200 Subject: [PATCH 0500/2616] don't split ringbuffer blocks in the output; this means up to 3ms stay in the buffer. --- htdocs/lib/AudioProcessor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/htdocs/lib/AudioProcessor.js b/htdocs/lib/AudioProcessor.js index 24c7b9ed7..900e56fea 100644 --- a/htdocs/lib/AudioProcessor.js +++ b/htdocs/lib/AudioProcessor.js @@ -32,11 +32,11 @@ class OwrxAudioProcessor extends AudioWorkletProcessor { this.port.start(); } process(inputs, outputs) { - const samples = Math.min(128, this.remaining()); + if (this.remaining() < 128) return true; outputs[0].forEach((output) => { - output.set(this.audioBuffer.subarray(this.outPos, this.outPos + samples)); + output.set(this.audioBuffer.subarray(this.outPos, this.outPos + 128)); }); - this.outPos = (this.outPos + samples) % this.bufferSize; + this.outPos = (this.outPos + 128) % this.bufferSize; return true; } remaining() { From 6cdec05cde769aac753fec829e0d05c741029af8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 21 Oct 2019 01:16:19 +0200 Subject: [PATCH 0501/2616] remove unused variables --- htdocs/openwebrx.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 114cbefdb..d56226aed 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -42,9 +42,6 @@ function e(what) { return document.getElementById(what); } -ios = /iPad|iPod|iPhone|Chrome/.test(navigator.userAgent); -is_chrome = /Chrome/.test(navigator.userAgent); - var rx_photo_height; function init_rx_photo() { From eb29d0ac999f3220db9e29fdd531c7cc9514d64b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 21 Oct 2019 21:51:31 +0200 Subject: [PATCH 0502/2616] protect websocket handling from any exceptions --- owrx/controllers.py | 2 +- owrx/websocket.py | 36 ++++++++++++++++++++---------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/owrx/controllers.py b/owrx/controllers.py index 639b07dda..3553240c0 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -152,4 +152,4 @@ class WebSocketController(Controller): def handle_request(self): conn = WebSocketConnection(self.handler, WebSocketMessageHandler()) # enter read loop - conn.read_loop() + conn.handle() diff --git a/owrx/websocket.py b/owrx/websocket.py index e780ab96f..24ab02b90 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -149,6 +149,26 @@ def chunks(l, n): def interrupt(self): self.interruptPipeSend.send(bytes(0x00)) + def handle(self): + WebSocketConnection.connections.append(self) + try: + self.read_loop() + finally: + logger.debug("websocket loop ended; shutting down") + + self.messageHandler.handleClose() + self.cancelPing() + + logger.debug("websocket loop ended; sending close frame") + + header = self.get_header(0, OPCODE_CLOSE) + self._sendBytes(header) + + try: + WebSocketConnection.connections.remove(self) + except ValueError: + pass + def read_loop(self): def protected_read(num): data = self.handler.rfile.read(num) @@ -158,7 +178,6 @@ def protected_read(num): raise IncompleteRead() return data - WebSocketConnection.connections.append(self) self.open = True while self.open: (read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], [], 15) @@ -203,21 +222,6 @@ def protected_read(num): logger.exception("OSError while reading data; closing connection") self.open = False - logger.debug("websocket loop ended; shutting down") - - self.messageHandler.handleClose() - self.cancelPing() - - logger.debug("websocket loop ended; sending close frame") - - header = self.get_header(0, OPCODE_CLOSE) - self._sendBytes(header) - - try: - WebSocketConnection.connections.remove(self) - except ValueError: - pass - def close(self): self.open = False self.interrupt() From 3b77753829f5211e788cde43a1a7cac1a6140bf1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 21 Oct 2019 22:09:18 +0200 Subject: [PATCH 0503/2616] ignore IDE files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6a211b7e9..f488a907a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc *.swp tags +.idea From ebf2804d639fd0c0be394f28153e8a4070af2395 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 22 Oct 2019 21:30:48 +0200 Subject: [PATCH 0504/2616] rename --- htdocs/index.html | 2 +- htdocs/lib/{bookmarks.js => BookmarkBar.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename htdocs/lib/{bookmarks.js => BookmarkBar.js} (100%) diff --git a/htdocs/index.html b/htdocs/index.html index c5d26c481..5a503165c 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -27,7 +27,7 @@ - + diff --git a/htdocs/lib/bookmarks.js b/htdocs/lib/BookmarkBar.js similarity index 100% rename from htdocs/lib/bookmarks.js rename to htdocs/lib/BookmarkBar.js From 713b6119d0d5c59465d588971e135e7e32ea683a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 22 Oct 2019 22:35:54 +0200 Subject: [PATCH 0505/2616] refactor progressbars into objects --- htdocs/index.html | 1 + htdocs/lib/ProgressBar.js | 113 ++++++++++++++++++++++++++++++++++++++ htdocs/openwebrx.js | 69 ++++++++--------------- 3 files changed, 137 insertions(+), 46 deletions(-) create mode 100644 htdocs/lib/ProgressBar.js diff --git a/htdocs/index.html b/htdocs/index.html index 5a503165c..34530e96a 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -29,6 +29,7 @@ + diff --git a/htdocs/lib/ProgressBar.js b/htdocs/lib/ProgressBar.js new file mode 100644 index 000000000..9762e6538 --- /dev/null +++ b/htdocs/lib/ProgressBar.js @@ -0,0 +1,113 @@ +ProgressBar = function(el) { + this.$el = $(el); + this.$innerText = this.$el.find('.openwebrx-progressbar-text'); + this.$innerBar = this.$el.find('.openwebrx-progressbar-bar'); + this.$innerBar.css('width', '0%'); +}; + +ProgressBar.prototype.set = function(val, text, over) { + this.setValue(val); + this.setText(text); + this.setOver(over); +}; + +ProgressBar.prototype.setValue = function(val) { + if (val < 0) val = 0; + if (val > 1) val = 1; + this.$innerBar.stop().animate({width: val * 100 + '%'}, 700); +}; + +ProgressBar.prototype.setText = function(text) { + this.$innerText.html(text); +}; + +ProgressBar.prototype.setOver = function(over) { + this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6"); +}; + +AudioBufferProgressBar = function(el, sampleRate) { + ProgressBar.call(this, el); + this.sampleRate = sampleRate; +}; + +AudioBufferProgressBar.prototype = new ProgressBar(); + +AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) { + var audio_buffer_value = buffersize / this.sampleRate; + var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; + var underrun = audio_buffer_value === 0; + var text = "buffer"; + if (overrun) { + text = "overrun"; + } + if (underrun) { + text = "underrun"; + } + this.set(audio_buffer_value, "Audio " + text + " [" + (audio_buffer_value).toFixed(1) + " s]", overrun || underrun); +}; + + +NetworkSpeedProgressBar = function(el) { + ProgressBar.call(this, el); +}; + +NetworkSpeedProgressBar.prototype = new ProgressBar(); + +NetworkSpeedProgressBar.prototype.setSpeed = function(speed) { + this.set(speed * 8 / 2000, "Network usage [" + (speed * 8).toFixed(1) + " kbps]", false); +}; + +AudioSpeedProgressBar = function(el) { + ProgressBar.call(this, el); +}; + +AudioSpeedProgressBar.prototype = new ProgressBar(); + +AudioSpeedProgressBar.prototype.setSpeed = function(speed) { + this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false); +}; + +AudioOutputProgressBar = function(el, sampleRate) { + ProgressBar.call(this, el); + this.maxRate = sampleRate * 1.25; + this.minRate = sampleRate * .25; +}; + +AudioOutputProgressBar.prototype = new ProgressBar(); + +AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) { + this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate); +}; + +ClientsProgressBar = function(el) { + ProgressBar.call(this, el); + this.clients = 0; + this.maxClients = 0; +}; + +ClientsProgressBar.prototype = new ProgressBar(); + +ClientsProgressBar.prototype.setClients = function(clients) { + this.clients = clients; + this.render(); +}; + +ClientsProgressBar.prototype.setMaxClients = function(maxClients) { + this.maxClients = maxClients; + this.render(); +}; + +ClientsProgressBar.prototype.render = function() { + console.info(this); + this.set(this.clients / this.maxClients, "Clients [" + this.clients + "]", this.clients > this.maxClients * 0.85); +}; + +CpuProgressBar = function(el) { + ProgressBar.call(this, el); +}; + +CpuProgressBar.prototype = new ProgressBar(); + +CpuProgressBar.prototype.setUsage = function(usage) { + this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85); +}; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index d56226aed..c89ebf9b8 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1063,8 +1063,6 @@ function resize_waterfall_container(check_init) { var debug_ws_data_received = 0; var debug_ws_time_start; -var max_clients_num = 0; -var client_num = 0; var currentprofile; var COMPRESS_FFT_PAD_N = 10; //should be the same as in csdr.c @@ -1099,8 +1097,7 @@ function on_ws_recv(evt) { divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + "."); fft_compression = config['fft_compression']; divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + "."); - max_clients_num = config['max_clients']; - progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num * 0.85); + clientProgressBar.setMaxClients(config['max_clients']); mathbox_waterfall_colors = config['mathbox_waterfall_colors']; mathbox_waterfall_frequency_resolution = config['mathbox_waterfall_frequency_resolution']; mathbox_waterfall_history_length = config['mathbox_waterfall_history_length']; @@ -1134,12 +1131,10 @@ function on_ws_recv(evt) { setSmeterAbsoluteValue(smeter_level); break; case "cpuusage": - var server_cpu_usage = json['value']; - progressbar_set(e("openwebrx-bar-server-cpu"), server_cpu_usage, "Server CPU [" + Math.round(server_cpu_usage * 100) + "%]", server_cpu_usage > 85); + cpuProgressBar.setUsage(json['value']); break; case "clients": - client_num = json['value']; - progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num * 0.85); + clientProgressBar.setClients(json['value']); break; case "profiles": var listbox = e("openwebrx-sdr-profiles-listbox"); @@ -1937,40 +1932,39 @@ function init_header() { }); } -function audio_buffer_progressbar_update(buffersize) { - var audio_buffer_value = buffersize / audioEngine.getSampleRate(); - var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; - var underrun = audio_buffer_value === 0; - var text = "buffer"; - if (overrun) { - text = "overrun"; - } - if (underrun) { - text = "underrun"; - } - progressbar_set(e("openwebrx-bar-audio-buffer"), audio_buffer_value, "Audio " + text + " [" + (audio_buffer_value).toFixed(1) + " s]", overrun || underrun); -} +var audioBufferProgressBar; +var networkSpeedProgressBar; +var audioSpeedProgressBar; +var audioOutputProgressBar; +var clientProgressBar; +var cpuProgressBar; + +function initProgressBars() { + audioBufferProgressBar = new AudioBufferProgressBar($('#openwebrx-bar-audio-buffer'), audioEngine.getSampleRate()); + networkSpeedProgressBar = new NetworkSpeedProgressBar($('#openwebrx-bar-network-speed')); + audioSpeedProgressBar = new AudioSpeedProgressBar($('#openwebrx-bar-audio-speed')); + audioOutputProgressBar = new AudioOutputProgressBar($('#openwebrx-bar-audio-output'), audioEngine.getSampleRate()); + clientProgressBar = new ClientsProgressBar($('#openwebrx-bar-clients')); + cpuProgressBar = new CpuProgressBar($('#openwebrx-bar-server-cpu')); +}; function updateNetworkStats() { var elapsed = (new Date() - debug_ws_time_start) / 1000; var network_speed_value = (debug_ws_data_received / 1000) / elapsed; - progressbar_set(e("openwebrx-bar-network-speed"), network_speed_value * 8 / 2000, "Network usage [" + (network_speed_value * 8).toFixed(1) + " kbps]", false); + networkSpeedProgressBar.setSpeed(network_speed_value); } function audioReporter(stats) { if (typeof(stats.buffersize) !== 'undefined') { - audio_buffer_progressbar_update(stats.buffersize); + audioBufferProgressBar.setBuffersize(stats.buffersize); } if (typeof(stats.audioByteRate) !== 'undefined') { - var audio_speed_value = stats.audioByteRate * 8; - progressbar_set(e("openwebrx-bar-audio-speed"), audio_speed_value / 500000, "Audio stream [" + (audio_speed_value / 1000).toFixed(0) + " kbps]", false); + audioSpeedProgressBar.setSpeed(stats.audioByteRate * 8); } if (typeof(stats.audioRate) !== 'undefined') { - var audio_max_rate = audioEngine.getSampleRate() * 1.25; - var audio_min_rate = audioEngine.getSampleRate() * .25; - progressbar_set(e("openwebrx-bar-audio-output"), stats.audioRate / audio_max_rate, "Audio output [" + (stats.audioRate / 1000).toFixed(1) + " ksps]", stats.audioRate > audio_max_rate || stats.audioRate < audio_min_rate); + audioOutputProgressBar.setAudioRate(stats.audioRate); } } @@ -1986,6 +1980,7 @@ function openwebrx_init() { } else { audioEngine.start(onAudioStart); } + initProgressBars(); init_rx_photo(); open_websocket(); setInterval(updateNetworkStats, 1000); @@ -2162,24 +2157,6 @@ function place_panels(function_apply) { } } -function progressbar_set(obj, val, text, over) { - if (val < 0.05) val = 0; - if (val > 1) val = 1; - var innerBar = null; - var innerText = null; - for (var i = 0; i < obj.children.length; i++) { - if (obj.children[i].className === "openwebrx-progressbar-text") innerText = obj.children[i]; - else if (obj.children[i].className === "openwebrx-progressbar-bar") innerBar = obj.children[i]; - } - if (innerBar == null) return; - //.h: function animate(object,style_name,unit,from,to,accel,time_ms,fps,to_exec) - animate(innerBar, "width", "px", innerBar.clientWidth, val * obj.clientWidth, 0.7, 700, 60); - //innerBar.style.width=(val*100).toFixed(0)+"%"; - innerBar.style.backgroundColor = (over) ? "#ff6262" : "#00aba6"; - if (innerText == null) return; - innerText.innerHTML = text; -} - function demodulator_buttons_update() { $(".openwebrx-demodulator-button").removeClass("highlighted"); if (secondary_demod) { From 58da0e8a60730bfeb42a08fe9820c453c92acf59 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 22 Oct 2019 22:38:08 +0200 Subject: [PATCH 0506/2616] remove debugging code --- htdocs/lib/ProgressBar.js | 1 - 1 file changed, 1 deletion(-) diff --git a/htdocs/lib/ProgressBar.js b/htdocs/lib/ProgressBar.js index 9762e6538..e623f9d92 100644 --- a/htdocs/lib/ProgressBar.js +++ b/htdocs/lib/ProgressBar.js @@ -98,7 +98,6 @@ ClientsProgressBar.prototype.setMaxClients = function(maxClients) { }; ClientsProgressBar.prototype.render = function() { - console.info(this); this.set(this.clients / this.maxClients, "Clients [" + this.clients + "]", this.clients > this.maxClients * 0.85); }; From 5bbee1e1d7719180bf0a44210e90c8305be9b7d1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 23 Oct 2019 11:27:05 +0200 Subject: [PATCH 0507/2616] fix some more minor javascript issues --- htdocs/lib/AprsMarker.js | 4 ++-- htdocs/lib/BookmarkBar.js | 30 +++++++++++++++--------------- htdocs/openwebrx.js | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/htdocs/lib/AprsMarker.js b/htdocs/lib/AprsMarker.js index aa3210ad2..d31daddff 100644 --- a/htdocs/lib/AprsMarker.js +++ b/htdocs/lib/AprsMarker.js @@ -8,7 +8,7 @@ AprsMarker.prototype.draw = function() { if (!div || !overlay) return; if (this.symbol) { - var tableId = this.symbol.table == '/' ? 0 : 1; + var tableId = this.symbol.table === '/' ? 0 : 1; div.style.background = 'url(/aprs-symbols/aprs-symbols-24-' + tableId + '@2x.png)'; div.style['background-size'] = '384px 144px'; div.style['background-position-x'] = -(this.symbol.index % 16) * 24 + 'px'; @@ -25,7 +25,7 @@ AprsMarker.prototype.draw = function() { div.style.transform = null; } - if (this.symbol.table != '/' && this.symbol.table != '\\') { + if (this.symbol.table !== '/' && this.symbol.table !== '\\') { overlay.style.display = 'block'; overlay.style['background-position-x'] = -(this.symbol.tableindex % 16) * 24 + 'px'; overlay.style['background-position-y'] = -Math.floor(this.symbol.tableindex / 16) * 24 + 'px'; diff --git a/htdocs/lib/BookmarkBar.js b/htdocs/lib/BookmarkBar.js index 43f72136e..4edac8e71 100644 --- a/htdocs/lib/BookmarkBar.js +++ b/htdocs/lib/BookmarkBar.js @@ -31,7 +31,7 @@ function BookmarkBar() { me.loadLocalBookmarks(); }); - var $bookmarkButton = $('#openwebrx-panel-receiver .openwebrx-bookmark-button'); + var $bookmarkButton = $('#openwebrx-panel-receiver').find('.openwebrx-bookmark-button'); if (typeof(Storage) !== 'undefined') { $bookmarkButton.show(); } else { @@ -56,10 +56,10 @@ function BookmarkBar() { BookmarkBar.prototype.position = function(){ var range = get_visible_freq_range(); - $('#openwebrx-bookmarks-container .bookmark').each(function(){ + $('#openwebrx-bookmarks-container').find('.bookmark').each(function(){ $(this).css('left', scale_px_from_freq($(this).data('frequency'), range)); }); -} +}; BookmarkBar.prototype.loadLocalBookmarks = function(){ var bwh = bandwidth / 2; @@ -69,7 +69,7 @@ BookmarkBar.prototype.loadLocalBookmarks = function(){ return b.frequency >= start && b.frequency <= end; }); this.replace_bookmarks(bookmarks, 'local', true); -} +}; BookmarkBar.prototype.replace_bookmarks = function(bookmarks, source, editable) { editable = !!editable; @@ -80,13 +80,13 @@ BookmarkBar.prototype.replace_bookmarks = function(bookmarks, source, editable) }); this.bookmarks[source] = bookmarks; this.render(); -} +}; BookmarkBar.prototype.render = function(){ var bookmarks = Object.values(this.bookmarks).reduce(function(l, v){ return l.concat(v); }); bookmarks = bookmarks.sort(function(a, b){ return a.frequency - b.frequency; }); var elements = bookmarks.map(function(b){ - $bookmark = $( + var $bookmark = $( '
    ' + '
    ' + '
    ' + @@ -101,7 +101,7 @@ BookmarkBar.prototype.render = function(){ this.$container.find('.bookmark').remove(); this.$container.append(elements); this.position(); -} +}; BookmarkBar.prototype.showEditDialog = function(bookmark) { var $form = this.$dialog.find("form"); @@ -118,7 +118,7 @@ BookmarkBar.prototype.showEditDialog = function(bookmark) { this.$dialog.data('id', bookmark.id); this.$dialog.show(); this.$dialog.find('#name').focus(); -} +}; BookmarkBar.prototype.storeBookmark = function() { var me = this; @@ -146,31 +146,31 @@ BookmarkBar.prototype.storeBookmark = function() { } } - bookmarks = bookmarks.filter(function(b) { return b.id != bookmark.id; }); + bookmarks = bookmarks.filter(function(b) { return b.id !== bookmark.id; }); bookmarks.push(bookmark); me.localBookmarks.setBookmarks(bookmarks); me.loadLocalBookmarks(); me.$dialog.hide(); -} +}; BookmarkLocalStorage = function(){ -} +}; BookmarkLocalStorage.prototype.getBookmarks = function(){ return JSON.parse(window.localStorage.getItem("bookmarks")) || []; -} +}; BookmarkLocalStorage.prototype.setBookmarks = function(bookmarks){ window.localStorage.setItem("bookmarks", JSON.stringify(bookmarks)); -} +}; BookmarkLocalStorage.prototype.deleteBookmark = function(data) { if (data.id) data = data.id; var bookmarks = this.getBookmarks(); - bookmarks = bookmarks.filter(function(b) { return b.id != data; }); + bookmarks = bookmarks.filter(function(b) { return b.id !== data; }); this.setBookmarks(bookmarks); -} +}; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index c89ebf9b8..317b26113 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1946,7 +1946,7 @@ function initProgressBars() { audioOutputProgressBar = new AudioOutputProgressBar($('#openwebrx-bar-audio-output'), audioEngine.getSampleRate()); clientProgressBar = new ClientsProgressBar($('#openwebrx-bar-clients')); cpuProgressBar = new CpuProgressBar($('#openwebrx-bar-server-cpu')); -}; +} function updateNetworkStats() { var elapsed = (new Date() - debug_ws_time_start) / 1000; From d3ac44c52656acecd397265aa066db59b3510bd3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 24 Oct 2019 19:35:23 +0200 Subject: [PATCH 0508/2616] replace custom animations with jquery --- htdocs/openwebrx.js | 56 +++++---------------------------------------- 1 file changed, 6 insertions(+), 50 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 317b26113..addf79044 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -49,10 +49,10 @@ function init_rx_photo() { rx_photo_height = clip.clientHeight; clip.style.maxHeight = rx_photo_height + "px"; window.setTimeout(function () { - animate(e("webrx-rx-photo-title"), "opacity", "", 1, 0, 1, 500, 30); + $('#webrx-rx-photo-title').animate({opacity: 0}, 500); }, 1000); window.setTimeout(function () { - animate(e("webrx-rx-photo-desc"), "opacity", "", 1, 0, 1, 500, 30); + $('#webrx-rx-photo-desc').animate({opacity: 0}, 500); }, 1500); window.setTimeout(function () { close_rx_photo() @@ -77,9 +77,9 @@ function toggle_rx_photo() { function close_rx_photo() { rx_photo_state = 0; - animate_to(e("webrx-top-photo-clip"), "maxHeight", "px", 67, 0.93, 1000, 60, function () { + $('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, step: function () { resize_waterfall_container(true); - }); + }}); e("openwebrx-rx-details-arrow-down").style.display = "block"; e("openwebrx-rx-details-arrow-up").style.display = "none"; } @@ -88,9 +88,9 @@ function open_rx_photo() { rx_photo_state = 1; e("webrx-rx-photo-desc").style.opacity = 1; e("webrx-rx-photo-title").style.opacity = 1; - animate_to(e("webrx-top-photo-clip"), "maxHeight", "px", rx_photo_height, 0.93, 1000, 60, function () { + $('#webrx-top-photo-clip').animate({maxHeight: rx_photo_height}, {duration: 1000, step: function () { resize_waterfall_container(true); - }); + }}); e("openwebrx-rx-details-arrow-down").style.display = "none"; e("openwebrx-rx-details-arrow-up").style.display = "block"; } @@ -225,50 +225,6 @@ function typeInAnimation(element, timeout, what, onFinish) { } -// ======================================================== -// ================= ANIMATION ROUTINES ================= -// ======================================================== - -function animate(object, style_name, unit, from, to, accel, time_ms, fps, to_exec) { - //console.log(object.className); - if (typeof to_exec === "undefined") to_exec = 0; - object.style[style_name] = from.toString() + unit; - object.anim_i = 0; - var n_of_iters = time_ms / (1000 / fps); - var change = (to - from) / (n_of_iters); - if (typeof object.anim_timer !== "undefined") { - window.clearInterval(object.anim_timer); - } - object.anim_timer = window.setInterval( - function () { - if (object.anim_i++ < n_of_iters) { - if (accel === 1) object.style[style_name] = (parseFloat(object.style[style_name]) + change).toString() + unit; - else { - var remain = parseFloat(object.style[style_name]) - to; - var new_val; - if (Math.abs(remain) > 9 || unit !== "px") new_val = (to + accel * remain); - else { - if (Math.abs(remain) < 2) new_val = to; - else new_val = to + remain - (remain / Math.abs(remain)); - } - object.style[style_name] = new_val.toString() + unit; - } - } - else { - object.style[style_name] = to.toString() + unit; - window.clearInterval(object.anim_timer); - delete object.anim_timer; - } - if (to_exec !== 0) to_exec(); - }, 1000 / fps); -} - -function animate_to(object, style_name, unit, to, accel, time_ms, fps, to_exec) { - var from = parseFloat(style_value(object, style_name)); - animate(object, style_name, unit, from, to, accel, time_ms, fps, to_exec); -} - - // ======================================================== // ================ DEMODULATOR ROUTINES ================ // ======================================================== From afa322a83b26a5525889082cc288c41aa48291a8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 24 Oct 2019 20:00:30 +0200 Subject: [PATCH 0509/2616] mousewheel control for the sliders <3 --- htdocs/openwebrx.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index addf79044..e6fde8890 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1948,6 +1948,22 @@ function openwebrx_init() { init_header(); bookmarks = new BookmarkBar(); parseHash(); + initSliders(); +} + +function initSliders() { + $('#openwebrx-panel-receiver').on('wheel', 'input[type=range]', function(ev){ + var $slider = $(this); + if (!$slider.attr('step')) return; + var val = Number($slider.val()); + var step = Number($slider.attr('step')); + if (ev.originalEvent.wheelDelta < 0) { + step *= -1; + } + val += step; + $slider.val(val + (ev.originalEvent.wheelDelta > 0 ? 1 : -1)) + $slider.trigger('change'); + }); } function digimodes_init() { From 07a8e6bf9217eb050cdd5e99df2b7b5589a2484a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 24 Oct 2019 20:06:24 +0200 Subject: [PATCH 0510/2616] add a title to show what the bookmark button does on hover --- htdocs/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/index.html b/htdocs/index.html index 34530e96a..24caa5d33 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -55,7 +55,7 @@
    ---.--- MHz
    ---.--- MHz
    - From 52b945cd64c8cc0a386d79f059dbf4e2a9042b53 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 25 Oct 2019 16:52:10 +0200 Subject: [PATCH 0511/2616] optimize --- htdocs/openwebrx.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index e6fde8890..f6b4e0d94 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -95,11 +95,6 @@ function open_rx_photo() { e("openwebrx-rx-details-arrow-up").style.display = "block"; } -function style_value(of_what, which) { - if (of_what.currentStyle) return of_what.currentStyle[which]; - else if (window.getComputedStyle) return document.defaultView.getComputedStyle(of_what, null).getPropertyValue(which); -} - function updateVolume() { audioEngine.setVolume(parseFloat(e("openwebrx-panel-volume").value) / 100); } @@ -1961,7 +1956,7 @@ function initSliders() { step *= -1; } val += step; - $slider.val(val + (ev.originalEvent.wheelDelta > 0 ? 1 : -1)) + $slider.val(val + (ev.originalEvent.wheelDelta > 0 ? 1 : -1)); $slider.trigger('change'); }); } From 70e2a99274b65979503c1e4b8262ca98d21ebedf Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 25 Oct 2019 19:21:49 +0200 Subject: [PATCH 0512/2616] custom easing to restore the original fadeout --- htdocs/openwebrx.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index f6b4e0d94..633a7d2eb 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -48,6 +48,13 @@ function init_rx_photo() { var clip = e("webrx-top-photo-clip"); rx_photo_height = clip.clientHeight; clip.style.maxHeight = rx_photo_height + "px"; + + $.extend($.easing, { + easeOutCubic:function(x) { + return 1 - Math.pow( 1 - x, 3 ); + } + }); + window.setTimeout(function () { $('#webrx-rx-photo-title').animate({opacity: 0}, 500); }, 1000); @@ -77,7 +84,7 @@ function toggle_rx_photo() { function close_rx_photo() { rx_photo_state = 0; - $('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, step: function () { + $('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic', step: function () { resize_waterfall_container(true); }}); e("openwebrx-rx-details-arrow-down").style.display = "block"; @@ -88,7 +95,7 @@ function open_rx_photo() { rx_photo_state = 1; e("webrx-rx-photo-desc").style.opacity = 1; e("webrx-rx-photo-title").style.opacity = 1; - $('#webrx-top-photo-clip').animate({maxHeight: rx_photo_height}, {duration: 1000, step: function () { + $('#webrx-top-photo-clip').animate({maxHeight: rx_photo_height}, {duration: 1000, easing: 'easeOutCubic', step: function () { resize_waterfall_container(true); }}); e("openwebrx-rx-details-arrow-down").style.display = "none"; From c7eb5c430c89efae9ca31af72230a1eaed968743 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 25 Oct 2019 21:08:56 +0200 Subject: [PATCH 0513/2616] perform binary decoding on the server side --- htdocs/openwebrx.js | 12 +++--------- owrx/connection.py | 3 ++- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 633a7d2eb..e3d06929e 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -23,11 +23,6 @@ is_firefox = navigator.userAgent.indexOf("Firefox") >= 0; -function arrayBufferToString(buf) { - //http://stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers - return String.fromCharCode.apply(null, new Uint8Array(buf)); -} - var bandwidth; var center_freq; var fft_size; @@ -1136,6 +1131,9 @@ function on_ws_recv(evt) { case "sdr_error": divlog(json['value'], true); break; + case 'secondary_demod': + secondary_demod_push_data(json['value']); + break; default: console.warn('received message of unknown type: ' + json['type']); } @@ -1186,10 +1184,6 @@ function on_ws_recv(evt) { secondary_demod_waterfall_add(waterfall_f32); } break; - case 4: - // secondary demod - secondary_demod_push_data(arrayBufferToString(data)); - break; default: console.warn('unknown type of binary message: ' + type) } diff --git a/owrx/connection.py b/owrx/connection.py index b4b827bed..824d91253 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -243,7 +243,8 @@ def write_secondary_fft(self, data): self.send(bytes([0x03]) + data) def write_secondary_demod(self, data): - self.send(bytes([0x04]) + data) + message = data.decode('ascii') + self.send({"type": "secondary_demod", "value": message}) def write_secondary_dsp_config(self, cfg): self.send({"type": "secondary_config", "value": cfg}) From fe08228204229e7eeb408b942f8b13dd2a359dec Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 26 Oct 2019 21:32:00 +0200 Subject: [PATCH 0514/2616] rework panel code to use less javascript and more css for positioning --- htdocs/css/openwebrx.css | 40 +++++--- htdocs/index.html | 202 ++++++++++++++++++++------------------- htdocs/openwebrx.js | 161 ++++++++----------------------- 3 files changed, 168 insertions(+), 235 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 29ae34a1e..e3f83dfc4 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -146,8 +146,10 @@ input[type=range]:focus::-ms-fill-upper #webrx-page-container { - min-height:100%; - position:relative; + height: 100%; + position: relative; + display: flex; + flex-direction: column; } #openwebrx-scale-container @@ -253,8 +255,8 @@ input[type=range]:focus::-ms-fill-upper #webrx-canvas-container { + flex-grow: 1; position: relative; - height: 2000px; overflow: hidden; background-image: url('../gfx/openwebrx-background-cool-blue.png'); background-repeat: no-repeat; @@ -274,6 +276,7 @@ input[type=range]:focus::-ms-fill-upper #openwebrx-mathbox-container { + flex-grow: 1; overflow: none; display: none; } @@ -288,14 +291,6 @@ input[type=range]:focus::-ms-fill-upper .nano .nano-pane { background: #444; } .nano .nano-slider { background: #eee !important; } -#webrx-main-container -{ - position: relative; - width: 100%; - margin: 0; - padding: 0; -} - .webrx-error { font-weight: bold; @@ -356,18 +351,35 @@ input[type=range]:focus::-ms-fill-upper margin-bottom: 5px; } +#openwebrx-panels-container-left, +#openwebrx-panels-container-right { + position: absolute; + bottom: 0; + display: flex; + flex-direction: column; + justify-content: flex-end; +} + +#openwebrx-panels-container-left { + left: 0; + align-items: flex-start; +} + +#openwebrx-panels-container-right { + right: 0; + align-items: flex-end; +} .openwebrx-panel { transform: perspective( 600px ) rotateX( 90deg ); - visibility: hidden; background-color: #575757; padding: 10px; color: white; - position: fixed; font-size: 10pt; border-radius: 15px; -moz-border-radius: 15px; + margin: 5.9px; } .openwebrx-panel a @@ -525,7 +537,7 @@ img.openwebrx-mirror-img #openwebrx-panel-status { - margin: 0px; + margin: 0 0 0 5.9px; padding: 0px; background-color:rgba(0, 0, 0, 0); } diff --git a/htdocs/index.html b/htdocs/index.html index 24caa5d33..1751cc719 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -37,19 +37,101 @@
    ${header} -
    -
    -
    -
    - -
    +
    +
    +
    +
    -
    -
    - +
    +
    +
    + +
    +
    +
    +
    + Under construction +
    We're working on the code right now, so the application might fail. +
    + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    OpenWebRX client log
    + Author: András Retzler, HA7ILM
    You can support OpenWebRX development via PayPal!
    +
    +
    +
    +
    +
    +
    Audio buffer [0 ms]
    +
    Audio output [0 sps]
    +
    Audio stream [0 kbps]
    +
    Network usage [0 kbps]
    +
    Server CPU [0%]
    +
    Clients [1]
    +
    -
    -
    +
    +
    ---.--- MHz
    @@ -65,29 +147,29 @@
    FM
    + onclick="demodulator_analog_replace('nfm');">FM
    AM
    + onclick="demodulator_analog_replace('am');">AM
    LSB
    + onclick="demodulator_analog_replace('lsb');">LSB
    USB
    + onclick="demodulator_analog_replace('usb');">USB
    CW
    + onclick="demodulator_analog_replace('cw');">CW
    + onclick="demodulator_analog_replace('dmr');">DMR
    + onclick="demodulator_analog_replace('dstar');">DStar
    + onclick="demodulator_analog_replace('nxdn');">NXDN
    + onclick="demodulator_analog_replace('ysf');">YSF
    DIG
    @@ -128,86 +210,6 @@
    -
    -
    -
    -
    OpenWebRX client log
    - Author: András Retzler, HA7ILM
    You can support OpenWebRX development via PayPal!
    -
    -
    -
    -
    -
    -
    Audio buffer [0 ms]
    -
    Audio output [0 sps]
    -
    Audio stream [0 kbps]
    -
    Network usage [0 kbps]
    -
    Server CPU [0%]
    -
    Clients [1]
    -
    -
    - Under construction -
    We're working on the code right now, so the application might fail. -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - -
    UTCdBDTFreqMessage
    - - - - - - - - -
    UTCCallsignCoordComment
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    Timeslot 1
    -
    -
    -
    -
    -
    -
    -
    Timeslot 2
    -
    -
    -
    -
    -
    -
    -
    diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index e3d06929e..379b3b16b 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -79,9 +79,7 @@ function toggle_rx_photo() { function close_rx_photo() { rx_photo_state = 0; - $('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic', step: function () { - resize_waterfall_container(true); - }}); + $('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic'}); e("openwebrx-rx-details-arrow-down").style.display = "block"; e("openwebrx-rx-details-arrow-up").style.display = "none"; } @@ -90,9 +88,7 @@ function open_rx_photo() { rx_photo_state = 1; e("webrx-rx-photo-desc").style.opacity = 1; e("webrx-rx-photo-title").style.opacity = 1; - $('#webrx-top-photo-clip').animate({maxHeight: rx_photo_height}, {duration: 1000, easing: 'easeOutCubic', step: function () { - resize_waterfall_container(true); - }}); + $('#webrx-top-photo-clip').animate({maxHeight: rx_photo_height}, {duration: 1000, easing: 'easeOutCubic'}); e("openwebrx-rx-details-arrow-down").style.display = "none"; e("openwebrx-rx-details-arrow-up").style.display = "block"; } @@ -1001,19 +997,6 @@ function zoom_calc() { //console.log("zoom_calc || zopx:"+zoom_offset_px.toString()+ " maxoff:"+(winsize-canvases_new_width).toString()+" relval:"+(0.5+zoom_center_rel/bandwidth).toString() ); } -function resize_waterfall_container(check_init) { - if (check_init && !waterfall_setup_done) return; - var numHeight; - mathbox_container.style.height = canvas_container.style.height = (numHeight = window.innerHeight - e("webrx-top-container").clientHeight - e("openwebrx-scale-container").clientHeight).toString() + "px"; - if (mathbox) { - //mathbox.three.camera.aspect = document.body.offsetWidth / numHeight; - //mathbox.three.camera.updateProjectionMatrix(); - mathbox.three.renderer.setSize(document.body.offsetWidth, numHeight); - console.log(document.body.offsetWidth, numHeight); - } - -} - var debug_ws_data_received = 0; var debug_ws_time_start; var currentprofile; @@ -1624,8 +1607,6 @@ function resize_canvases(zoom) { function waterfall_init() { init_canvas_container(); - resize_waterfall_container(false); - /* then */ resize_canvases(); scale_setup(); mkzoomlevels(); @@ -1873,7 +1854,6 @@ function waterfall_clear() { function openwebrx_resize() { resize_canvases(); - resize_waterfall_container(true); resize_scale(); check_top_bar_congestion(); } @@ -1938,7 +1918,7 @@ function openwebrx_init() { setInterval(updateNetworkStats, 1000); secondary_demod_init(); digimodes_init(); - place_panels(first_show_panel); + initPanels(); window.addEventListener("resize", openwebrx_resize); check_top_bar_congestion(); init_header(); @@ -1963,10 +1943,6 @@ function initSliders() { } function digimodes_init() { - $(".openwebrx-meta-panel").each(function (_, p) { - p.openwebrxHidden = true; - }); - // initialze DMR timeslot muting $('.openwebrx-dmr-timeslot-panel').click(function (e) { $(e.currentTarget).toggleClass("muted"); @@ -2002,50 +1978,32 @@ var rt = function (s, n) { // ======================= PANELS ======================= // ======================================================== -var panel_margin = 5.9; - -function pop_bottommost_panel(from) { - var min_order = parseInt(from[0].dataset.panelOrder); - var min_index = 0; - for (var i = 0; i < from.length; i++) { - var actual_order = parseInt(from[i].dataset.panelOrder); - if (actual_order < min_order) { - min_index = i; - min_order = actual_order; - } - } - var to_return = from[min_index]; - from.splice(min_index, 1); - return to_return; +function panel_displayed(el){ + return !(el.style && el.style.display && el.style.display === 'none') } function toggle_panel(what, on) { - var item = e(what); + var item = $('#' + what)[0]; if (!item) return; - if (typeof on !== "undefined") { - if (item.openwebrxHidden && !on) return; - if (!item.openwebrxHidden && on) return; + var displayed = panel_displayed(item); + if (typeof on !== "undefined" && displayed === on) { + return; } if (item.openwebrxDisableClick) return; - item.style.transitionDuration = "599ms"; + if (displayed) { + item.movement = 'collapse'; + item.style.transform = "perspective(600px) rotateX(90deg)"; + item.style.transitionProperty = 'transform'; + } else { + item.movement = 'expand'; + item.style.display = 'block'; + setTimeout(function(){ + item.style.transitionProperty = 'transform'; + item.style.transform = 'perspective(600px) rotateX(0deg)'; + }, 20); + } + item.style.transitionDuration = "600ms"; item.style.transitionDelay = "0ms"; - if (!item.openwebrxHidden) { - window.setTimeout(function () { - item.openwebrxHidden = !item.openwebrxHidden; - place_panels(); - item.openwebrxDisableClick = false; - }, 700); - item.style.transform = "perspective( 599px ) rotateX( 90deg )"; - } - else { - item.openwebrxHidden = !item.openwebrxHidden; - place_panels(); - window.setTimeout(function () { - item.openwebrxDisableClick = false; - }, 700); - item.style.transform = "perspective( 599px ) rotateX( 0deg )"; - } - item.style.transitionDuration = "0"; item.openwebrxDisableClick = true; @@ -2062,67 +2020,31 @@ function first_show_panel(panel) { roty = rottemp; } if (rotx !== 0 && Math.random() > 0.5) rotx = 270; - panel.style.transform = "perspective( 599px ) rotateX( %1deg ) rotateY( %2deg )" + panel.style.transform = "perspective(600px) rotateX(%1deg) rotateY(%2deg)" .replace("%1", rotx.toString()).replace("%2", roty.toString()); window.setTimeout(function () { - panel.style.transitionDuration = "599ms"; + panel.style.transitionDuration = "600ms"; panel.style.transitionDelay = (Math.floor(Math.random() * 500)).toString() + "ms"; - panel.style.transform = "perspective( 599px ) rotateX( 0deg ) rotateY( 0deg )"; + panel.style.transform = "perspective(600px) rotateX(0deg) rotateY(0deg)"; }, 1); } -function place_panels(function_apply) { - if (function_apply === undefined) function_apply = function (x) { - }; - var hoffset = 0; //added this because the first panel should not have such great gap below - var left_col = []; - var right_col = []; - var plist = e("openwebrx-panels-container").children; - for (var i = 0; i < plist.length; i++) { - var c = plist[i]; - if (c.className.indexOf("openwebrx-panel") >= 0) { - if (c.openwebrxHidden) { - c.style.display = "none"; - continue; +function initPanels() { + $('#openwebrx-panels-container').find('.openwebrx-panel').each(function(){ + var el = this; + el.openwebrxPanelTransparent = (!!el.dataset.panelTransparent); + el.addEventListener('transitionend', function(ev){ + if (ev.target !== el) return; + el.openwebrxDisableClick = false; + el.style.transitionDuration = null; + el.style.transitionDelay = null; + el.style.transitionProperty = null; + if (el.movement && el.movement == 'collapse') { + el.style.display = 'none'; } - c.style.display = "block"; - c.openwebrxPanelTransparent = (!!c.dataset.panelTransparent); - var newSize = c.dataset.panelSize.split(","); - if (c.dataset.panelPos === "left") { - left_col.push(c); - } - else if (c.dataset.panelPos === "right") { - right_col.push(c); - } - c.style.width = newSize[0] + "px"; - //c.style.height=newSize[1]+"px"; - if (!c.openwebrxPanelTransparent) c.style.margin = panel_margin.toString() + "px"; - else c.style.marginLeft = panel_margin.toString() + "px"; - c.openwebrxPanelWidth = parseInt(newSize[0]); - c.openwebrxPanelHeight = parseInt(newSize[1]); - } - } - - var y = hoffset; //was y=0 before hoffset - var p; - while (left_col.length > 0) { - p = pop_bottommost_panel(left_col); - p.style.left = "0px"; - p.style.bottom = y.toString() + "px"; - p.style.visibility = "visible"; - y += p.openwebrxPanelHeight + ((p.openwebrxPanelTransparent) ? 0 : 3) * panel_margin; - if (function_apply) function_apply(p); - //console.log(p.id, y, p.openwebrxPanelTransparent); - } - y = hoffset; - while (right_col.length > 0) { - p = pop_bottommost_panel(right_col); - p.style.right = (e("webrx-canvas-container").offsetWidth - e("webrx-canvas-container").clientWidth).toString() + "px"; //get scrollbar width - p.style.bottom = y.toString() + "px"; - p.style.visibility = "visible"; - y += p.openwebrxPanelHeight + ((p.openwebrxPanelTransparent) ? 0 : 3) * panel_margin; - if (function_apply) function_apply(p); - } + }); + if (panel_displayed(el)) first_show_panel(el); + }); } function demodulator_buttons_update() { @@ -2261,9 +2183,6 @@ function secondary_demod_swap_canvases() { } function secondary_demod_init() { - $("#openwebrx-panel-digimodes")[0].openwebrxHidden = true; - $("#openwebrx-panel-wsjt-message")[0].openwebrxHidden = true; - $("#openwebrx-panel-packet-message")[0].openwebrxHidden = true; secondary_demod_canvas_container = $("#openwebrx-digimode-canvas-container")[0]; $(secondary_demod_canvas_container) .mousemove(secondary_demod_canvas_container_mousemove) From 39120d94135ae372bfd2f42162e7937ab5b0433f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 26 Oct 2019 22:32:25 +0200 Subject: [PATCH 0515/2616] implement new way of measuring stats that allows arbitrary timeranges --- htdocs/index.html | 1 + htdocs/lib/AudioEngine.js | 60 +++++++++++++++++++++------------- htdocs/lib/AudioProcessor.js | 14 +++++--- htdocs/lib/Measurement.js | 62 ++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 26 deletions(-) create mode 100644 htdocs/lib/Measurement.js diff --git a/htdocs/index.html b/htdocs/index.html index 1751cc719..985196b8a 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -30,6 +30,7 @@ + diff --git a/htdocs/lib/AudioEngine.js b/htdocs/lib/AudioEngine.js index 0445dd3b5..5860b2efc 100644 --- a/htdocs/lib/AudioEngine.js +++ b/htdocs/lib/AudioEngine.js @@ -4,6 +4,7 @@ var useAudioWorklets = true; function AudioEngine(maxBufferLength, audioReporter) { this.audioReporter = audioReporter; + this.initStats(); this.resetStats(); var ctx = window.AudioContext || window.webkitAudioContext; if (!ctx) { @@ -55,7 +56,12 @@ AudioEngine.prototype.start = function(callback) { me.audioNode.port.addEventListener('message', function(m){ var json = JSON.parse(m.data); if (typeof(json.buffersize) !== 'undefined') { - me.audioReporter(json); + me.audioReporter({ + buffersize: json.buffersize + }); + } + if (typeof(json.samplesProcessed) !== 'undefined') { + me.audioSamples.add(json.samplesProcessed); } }); me.audioNode.port.start(); @@ -86,21 +92,23 @@ AudioEngine.prototype.start = function(callback) { var out = new Float32Array(bufferSize); while (me.audioBuffers.length) { var b = me.audioBuffers.shift(); - var newLength = total + b.length; // not enough space to fit all data, so splice and put back in the queue - if (newLength > bufferSize) { - var tokeep = b.subarray(0, bufferSize - total); + if (total + b.length > bufferSize) { + var spaceLeft = bufferSize - total; + var tokeep = b.subarray(0, spaceLeft); out.set(tokeep, total); - var tobuffer = b.subarray(bufferSize - total, b.length); + var tobuffer = b.subarray(spaceLeft, b.length); me.audioBuffers.unshift(tobuffer); + total += spaceLeft; break; } else { out.set(b, total); + total += b.length; } - total = newLength; } e.outputBuffer.copyToChannel(out, 0); + me.audioSamples.add(total); } @@ -124,27 +132,36 @@ AudioEngine.prototype.isAllowed = function() { }; AudioEngine.prototype.reportStats = function() { - var stats = {}; if (this.audioNode.port) { - this.audioNode.port.postMessage(JSON.stringify({cmd:'getBuffers'})); + this.audioNode.port.postMessage(JSON.stringify({cmd:'getStats'})); } else { - stats.buffersize = this.getBuffersize(); + this.audioReporter({ + buffersize: this.getBuffersize() + }); } - stats.audioRate = this.stats.audioSamples; - var elapsed = new Date() - this.stats.startTime; - stats.audioByteRate = this.stats.audioBytes * 1000 / elapsed; - this.audioReporter(stats); +}; + +AudioEngine.prototype.initStats = function() { + var me = this; + var buildReporter = function(key) { + return function(v){ + var report = {}; + report[key] = v; + me.audioReporter(report); + } - // sample rate is just measuring the last seconds - this.stats.audioSamples = 0; + }; + + this.audioBytes = new Measurement(); + this.audioBytes.report(10000, 1000, buildReporter('audioByteRate')); + + this.audioSamples = new Measurement(); + this.audioSamples.report(10000, 1000, buildReporter('audioRate')); }; AudioEngine.prototype.resetStats = function() { - this.stats = { - startTime: new Date(), - audioBytes: 0, - audioSamples: 0 - }; + this.audioBytes.reset(); + this.audioSamples.reset(); }; AudioEngine.prototype.setupResampling = function() { //both at the server and the client @@ -178,7 +195,7 @@ AudioEngine.prototype.getSampleRate = function() { AudioEngine.prototype.pushAudio = function(data) { if (!this.audioNode) return; - this.stats.audioBytes += data.byteLength; + this.audioBytes.add(data.byteLength); var buffer; if (this.compression === "adpcm") { //resampling & ADPCM @@ -187,7 +204,6 @@ AudioEngine.prototype.pushAudio = function(data) { buffer = new Int16Array(data); } buffer = this.resampler.process(sdrjs.ConvertI16_F(buffer)); - this.stats.audioSamples += buffer.length; if (this.audioNode.port) { // AudioWorklets supported this.audioNode.port.postMessage(buffer); diff --git a/htdocs/lib/AudioProcessor.js b/htdocs/lib/AudioProcessor.js index 900e56fea..7ac76c514 100644 --- a/htdocs/lib/AudioProcessor.js +++ b/htdocs/lib/AudioProcessor.js @@ -6,11 +6,12 @@ class OwrxAudioProcessor extends AudioWorkletProcessor { this.audioBuffer = new Float32Array(this.bufferSize); this.inPos = 0; this.outPos = 0; + this.samplesProcessed = 0; this.port.addEventListener('message', (m) => { if (typeof(m.data) === 'string') { const json = JSON.parse(m.data); - if (json.cmd && json.cmd === 'getBuffers') { - this.reportBuffers(); + if (json.cmd && json.cmd === 'getStats') { + this.reportStats(); } } else { // the ringbuffer size is aligned to the output buffer size, which means that the input buffers might @@ -37,6 +38,7 @@ class OwrxAudioProcessor extends AudioWorkletProcessor { output.set(this.audioBuffer.subarray(this.outPos, this.outPos + 128)); }); this.outPos = (this.outPos + 128) % this.bufferSize; + this.samplesProcessed += 128; return true; } remaining() { @@ -44,8 +46,12 @@ class OwrxAudioProcessor extends AudioWorkletProcessor { if (mod >= 0) return mod; return mod + this.bufferSize; } - reportBuffers() { - this.port.postMessage(JSON.stringify({buffersize: this.remaining()})); + reportStats() { + this.port.postMessage(JSON.stringify({ + buffersize: this.remaining(), + samplesProcessed: this.samplesProcessed + })); + this.samplesProcessed = 0; } } diff --git a/htdocs/lib/Measurement.js b/htdocs/lib/Measurement.js new file mode 100644 index 000000000..018739ca8 --- /dev/null +++ b/htdocs/lib/Measurement.js @@ -0,0 +1,62 @@ +function Measurement() { + this.reset(); +}; + +Measurement.prototype.add = function(v) { + this.value += v; +}; + +Measurement.prototype.getValue = function() { + return this.value; +}; + +Measurement.prototype.getElapsed = function() { + return new Date() - this.start; +}; + +Measurement.prototype.getRate = function() { + return this.getValue() / this.getElapsed(); +}; + +Measurement.prototype.reset = function() { + this.value = 0; + this.start = new Date(); +}; + +Measurement.prototype.report = function(range, interval, callback) { + return new Reporter(this, range, interval, callback); +} + +function Reporter(measurement, range, interval, callback) { + this.measurement = measurement; + this.range = range; + this.samples = []; + this.callback = callback; + this.interval = setInterval(this.report.bind(this), interval); +}; + +Reporter.prototype.sample = function(){ + this.samples.push({ + timestamp: new Date(), + value: this.measurement.getValue() + }); +}; + +Reporter.prototype.report = function(){ + this.sample(); + var now = new Date(); + var minDate = now.getTime() - this.range; + this.samples = this.samples.filter(function(s) { + return s.timestamp.getTime() > minDate; + }); + this.samples.sort(function(a, b) { + return a.timestamp - b.timestamp; + }); + var oldest = this.samples[0]; + var newest = this.samples[this.samples.length -1]; + var elapsed = newest.timestamp - oldest.timestamp; + if (elapsed <= 0) return; + var accumulated = newest.value - oldest.value; + // we want rate per second, but our time is in milliseconds... compensate by 1000 + this.callback(accumulated * 1000 / elapsed); +}; \ No newline at end of file From 13f27a76ff3040218dcbc5c9842b45f75ff3d557 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 26 Oct 2019 22:44:54 +0200 Subject: [PATCH 0516/2616] use new way of measuring for network speed, too --- htdocs/lib/ProgressBar.js | 3 ++- htdocs/openwebrx.js | 24 +++++++++++------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/htdocs/lib/ProgressBar.js b/htdocs/lib/ProgressBar.js index e623f9d92..9d0736d97 100644 --- a/htdocs/lib/ProgressBar.js +++ b/htdocs/lib/ProgressBar.js @@ -54,7 +54,8 @@ NetworkSpeedProgressBar = function(el) { NetworkSpeedProgressBar.prototype = new ProgressBar(); NetworkSpeedProgressBar.prototype.setSpeed = function(speed) { - this.set(speed * 8 / 2000, "Network usage [" + (speed * 8).toFixed(1) + " kbps]", false); + var speedInKilobits = speed * 8 / 1000; + this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false); }; AudioSpeedProgressBar = function(el) { diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 379b3b16b..ef3d6a756 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -997,8 +997,7 @@ function zoom_calc() { //console.log("zoom_calc || zopx:"+zoom_offset_px.toString()+ " maxoff:"+(winsize-canvases_new_width).toString()+" relval:"+(0.5+zoom_center_rel/bandwidth).toString() ); } -var debug_ws_data_received = 0; -var debug_ws_time_start; +var networkSpeedMeasurement; var currentprofile; var COMPRESS_FFT_PAD_N = 10; //should be the same as in csdr.c @@ -1006,7 +1005,7 @@ var COMPRESS_FFT_PAD_N = 10; //should be the same as in csdr.c function on_ws_recv(evt) { if (typeof evt.data === 'string') { // text messages - debug_ws_data_received += evt.data.length; + networkSpeedMeasurement.add(evt.data.length); if (evt.data.substr(0, 16) === "CLIENT DE SERVER") { divlog("Server acknowledged WebSocket connection."); @@ -1127,7 +1126,7 @@ function on_ws_recv(evt) { } } else if (evt.data instanceof ArrayBuffer) { // binary messages - debug_ws_data_received += evt.data.byteLength; + networkSpeedMeasurement.add(evt.data.byteLength); var type = new Uint8Array(evt.data, 0, 1)[0]; var data = evt.data.slice(1); @@ -1386,8 +1385,14 @@ function waterfall_measure_minmax_do(what) { function on_ws_opened() { ws.send("SERVER DE CLIENT client=openwebrx.js type=receiver"); divlog("WebSocket opened to " + ws.url); - debug_ws_data_received = 0; - debug_ws_time_start = new Date(); + if (!networkSpeedMeasurement) { + networkSpeedMeasurement = new Measurement(); + networkSpeedMeasurement.report(60000, 1000, function(rate){ + networkSpeedProgressBar.setSpeed(rate); + }); + } else { + networkSpeedMeasurement.reset(); + } reconnect_timeout = false; ws.send(JSON.stringify({ "type": "dspcontrol", @@ -1880,12 +1885,6 @@ function initProgressBars() { cpuProgressBar = new CpuProgressBar($('#openwebrx-bar-server-cpu')); } -function updateNetworkStats() { - var elapsed = (new Date() - debug_ws_time_start) / 1000; - var network_speed_value = (debug_ws_data_received / 1000) / elapsed; - networkSpeedProgressBar.setSpeed(network_speed_value); -} - function audioReporter(stats) { if (typeof(stats.buffersize) !== 'undefined') { audioBufferProgressBar.setBuffersize(stats.buffersize); @@ -1915,7 +1914,6 @@ function openwebrx_init() { initProgressBars(); init_rx_photo(); open_websocket(); - setInterval(updateNetworkStats, 1000); secondary_demod_init(); digimodes_init(); initPanels(); From a24cb3e04a709b3710ae8b308f557e069b4569ed Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 27 Oct 2019 12:16:17 +0100 Subject: [PATCH 0517/2616] shutdown services properly --- openwebrx.py | 1 + owrx/service.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openwebrx.py b/openwebrx.py index d4ebddd76..4d755be97 100755 --- a/openwebrx.py +++ b/openwebrx.py @@ -60,3 +60,4 @@ def main(): main() except KeyboardInterrupt: WebSocketConnection.closeAll() + Services.stop() diff --git a/owrx/service.py b/owrx/service.py index ec86ad890..7a583c367 100644 --- a/owrx/service.py +++ b/owrx/service.py @@ -369,12 +369,19 @@ def write_aprs_data(self, data): class Services(object): + handlers = [] @staticmethod def start(): if not PropertyManager.getSharedInstance()["services_enabled"]: return for source in SdrService.getSources().values(): - ServiceHandler(source) + Services.handlers.append(ServiceHandler(source)) + + @staticmethod + def stop(): + for handler in Services.handlers: + handler.stopServices() + Services.handlers = [] class Service(object): From 1e28fc5018fa0b13891becf761e7897c990ed24a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 27 Oct 2019 13:18:00 +0100 Subject: [PATCH 0518/2616] fix broken widths on digital meta panels --- htdocs/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index 985196b8a..83d21bd5f 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -84,7 +84,7 @@ -