From bd8e66519886e39bfa942841e4ab9838a1b01316 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 3 May 2019 22:59:24 +0200 Subject: [PATCH 001/137] add new webserver infrastructure --- htdocs/index.wrx | 54 ++++++++++++++++++++++----------------------- owrx/controllers.py | 43 ++++++++++++++++++++++++++++++++++++ owrx/http.py | 35 +++++++++++++++++++++++++++++ server.py | 6 +++++ 4 files changed, 111 insertions(+), 27 deletions(-) create mode 100644 owrx/controllers.py create mode 100644 owrx/http.py create mode 100644 server.py diff --git a/htdocs/index.wrx b/htdocs/index.wrx index 5e00506..2cd64da 100644 --- a/htdocs/index.wrx +++ b/htdocs/index.wrx @@ -39,40 +39,40 @@ var mathbox_waterfall_history_length=%[MATHBOX_WATERFALL_THIST]; var mathbox_waterfall_colors=%[MATHBOX_WATERFALL_COLORS]; - - - - - - - + + + + + + +
- +
%[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 0000000..c7989c1 --- /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 0000000..b5ac0ae --- /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 0000000..2b14a52 --- /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 002/137] 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 2cd64da..885e3ef 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 c7fd7c0..6efc93d 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 0000000..7e4e7e5 --- /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 c7989c1..12a4aa2 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 @@ class Controller(object): 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 @@ class IndexController(Controller): 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 b5ac0ae..ab4399b 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 0000000..8bd305e --- /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 2b14a52..32850a5 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 003/137] we got fft --- config_webrx.py | 2 +- csdr.py | 14 +++--- htdocs/openwebrx.js | 51 ++++++++++++++-------- owrx/config.py | 3 ++ owrx/controllers.py | 74 ++++++++++++++++++++++++++------ 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 34e480c..a4f63ef 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -175,7 +175,7 @@ iq_server_port = 4951 #TCP port for ncat to listen on. It will send I/Q data ove #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 a2fb490..a7f1f89 100755 --- a/csdr.py +++ b/csdr.py @@ -129,7 +129,7 @@ class dsp: 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 @@ class dsp: 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 @@ class dsp: 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 @@ class dsp: 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 6efc93d..de12eea 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; - } + } 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(); + //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 7e4e7e5..9866c98 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -21,3 +21,6 @@ class PropertyManager(object): 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 12a4aa2..f1c6ccd 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 @@ class AssetsController(Controller): 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 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() + + conn.send({"type":"config","value":config}) + print("client connection intitialized") + + 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 + + self.forwarder = SpectrumForwarder(conn) + SpectrumThread.getSharedInstance().add_client(self.forwarder) + + 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) + conn = WebSocketConnection(self.handler, WebSocketMessageHandler()) 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}) + # enter read loop + conn.read_loop() diff --git a/owrx/source.py b/owrx/source.py new file mode 100644 index 0000000..0f18014 --- /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 8bd305e..7c96e34 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -3,8 +3,9 @@ import hashlib 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 @@ class WebSocketConnection(object): 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 32850a5..c267f9e 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 004/137] 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 885e3ef..f091933 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 068/137] 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 07df451..b89d7e5 100644 --- a/openwebrx.py +++ b/openwebrx.py @@ -3,6 +3,7 @@ from owrx.http import RequestHandler 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 @@ Author contact info: Andras Retzler, HA7ILM """) - 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 @@ Author contact info: Andras Retzler, HA7ILM # 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 cc84c26..a117228 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -53,6 +53,9 @@ class PropertyManager(object): 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 @@ class PropertyManager(object): 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 @@ class PropertyManager(object): 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 0000000..b2c6b0f --- /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 d06ae05..3060789 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 069/137] 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 b89d7e5..e41d6c7 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 a117228..8fb6513 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 @@ class PropertyManager(object): 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 0000000..a0442b8 --- /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 366eb3a..a41015a 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 0000000..83f9232 --- /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 f918e54..c45d12f 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 070/137] 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 cf9f678..23f17a2 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 071/137] 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 a1f210c..0000000 --- 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 072/137] 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 a41015a..9100de4 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -77,7 +77,7 @@ class AssetsController(Controller): 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 073/137] 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 e41d6c7..99b1419 100644 --- a/openwebrx.py +++ b/openwebrx.py @@ -2,7 +2,7 @@ from http.server import HTTPServer 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 @@ Author contact info: Andras Retzler, HA7ILM 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 a0442b8..95ce84f 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 @@ class OpenWebRxClient(object): 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 9100de4..774ba9b 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -3,7 +3,7 @@ import mimetypes 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 @@ class StatusController(Controller): "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 b2c6b0f..5f0d7fb 100644 --- a/owrx/sdrhu.py +++ b/owrx/sdrhu.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) 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 c45d12f..3efc7d4 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -467,6 +467,8 @@ class CpuUsageThread(threading.Thread): 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 @@ class CpuUsageThread(threading.Thread): 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 @@ class ClientReporterThread(threading.Thread): 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 5387782..d0385b8 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -72,5 +72,20 @@ class WebSocketConnection(object): 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 074/137] 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 a223eec..0fa04c4 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 f5b7da8..8610778 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 95ce84f..346f56d 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 @@ class OpenWebRxClient(object): 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 @@ class OpenWebRxClient(object): 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 83f9232..457588b 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 @@ class FeatureDetector(object): 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 @@ class FeatureDetector(object): 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 075/137] 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 457588b..74b5970 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 076/137] 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 0fa04c4..4a24382 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 74b5970..dfc7901 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 077/137] refactor dsp outputs add digimode metadata --- csdr.py | 54 +++++++++++---------- htdocs/openwebrx.js | 52 ++++++++++++++++++++ owrx/connection.py | 2 + owrx/source.py | 114 +++++++++++++++++++------------------------- 4 files changed, 131 insertions(+), 91 deletions(-) diff --git a/csdr.py b/csdr.py index 51d5034..c0ea54e 100755 --- a/csdr.py +++ b/csdr.py @@ -25,13 +25,20 @@ import time 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 @@ class dsp: 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 @@ class dsp: 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 @@ class dsp: 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 @@ class dsp: 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 @@ class dsp: 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 @@ class dsp: 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 8610778..0ef67bb 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 346f56d..76a93a4 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -127,6 +127,8 @@ class OpenWebRxClient(object): 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 3efc7d4..f2be0af 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -169,9 +169,6 @@ class SdrSource(object): 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 @@ class SdrSource(object): 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 @@ class SdrSource(object): 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 @@ class SdrplaySource(SdrSource): 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 @@ class SpectrumThread(threading.Thread): 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 @@ class DspManager(object): "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 @@ class DspManager(object): 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 @@ class DspManager(object): }) 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 @@ class DspManager(object): 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 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 stopSecondaryThreads(self): - self.runSecondary = False - self.secondaryDemodThread = None - self.secondaryFftThread = None + 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 - 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) + 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 @@ class DspManager(object): 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 078/137] 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 0ef67bb..dfdfafc 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 079/137] 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 dfdfafc..979edf5 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 080/137] 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 f2be0af..4aa9bba 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -151,6 +151,14 @@ class SdrSource(object): 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 @@ class SdrSource(object): 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 @@ class SpectrumThread(csdr.output): 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 @@ class SpectrumThread(csdr.output): 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 @@ class SpectrumThread(csdr.output): 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 @@ class DspManager(csdr.output): 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 @@ class DspManager(csdr.output): 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 @@ class DspManager(csdr.output): 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 081/137] 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 979edf5..e71d0fe 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 4aa9bba..b8e7d12 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -452,8 +452,6 @@ class CpuUsageThread(threading.Thread): 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 @@ class CpuUsageThread(threading.Thread): 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 082/137] 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 e71d0fe..fa68859 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 b8e7d12..0cbc33f 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -487,18 +487,6 @@ class CpuUsageThread(threading.Thread): 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 @@ class ClientRegistry(object): def __init__(self): self.clients = [] - self.reporter = None super().__init__() def broadcast(self): @@ -525,9 +512,7 @@ class ClientRegistry(object): 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 @@ class ClientRegistry(object): 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 083/137] clean up chains --- csdr.py | 94 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/csdr.py b/csdr.py index c0ea54e..969693b 100755 --- a/csdr.py +++ b/csdr.py @@ -74,56 +74,51 @@ class dsp(object): 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 @@ class dsp(object): 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 @@ class dsp(object): 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 084/137] demodulator chain optimizations --- csdr.py | 80 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/csdr.py b/csdr.py index 969693b..915e19f 100755 --- a/csdr.py +++ b/csdr.py @@ -68,7 +68,7 @@ class dsp(object): 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 @@ class dsp(object): 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 @@ class dsp(object): 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 @@ class dsp(object): 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 @@ class dsp(object): 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 @@ class dsp(object): 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 085/137] remove stray character --- csdr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csdr.py b/csdr.py index 915e19f..e9c45fa 100755 --- a/csdr.py +++ b/csdr.py @@ -68,7 +68,7 @@ class dsp(object): 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 086/137] add 30m --- config_webrx.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config_webrx.py b/config_webrx.py index 3bd92c6..f156d0f 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -135,6 +135,14 @@ sdrs = { "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 087/137] 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 e9c45fa..981cb8e 100755 --- a/csdr.py +++ b/csdr.py @@ -106,13 +106,11 @@ class dsp(object): 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 @@ class dsp(object): 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 088/137] resolve todo --- owrx/source.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/owrx/source.py b/owrx/source.py index 0cbc33f..c5003d4 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -253,7 +253,7 @@ class SpectrumThread(csdr.output): 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 @@ class SpectrumThread(csdr.output): 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 089/137] 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 4a24382..5a8ed94 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -168,7 +168,7 @@
-
+
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index fa68859..38f098e 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 090/137] 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 5a8ed94..da3c800 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 091/137] 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 092/137] 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 981cb8e..77b017b 100755 --- a/csdr.py +++ b/csdr.py @@ -86,7 +86,7 @@ class dsp(object): 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 @@ class dsp(object): 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 093/137] introduce subscription concept to simplify unsubscribing from events --- csdr.py | 3 ++ owrx/config.py | 53 ++++++++++++++++++++++++++++-------- owrx/connection.py | 27 ++++++++++-------- owrx/source.py | 68 +++++++++++++++++++++++++--------------------- 4 files changed, 97 insertions(+), 54 deletions(-) diff --git a/csdr.py b/csdr.py index 77b017b..6ce0ea8 100755 --- a/csdr.py +++ b/csdr.py @@ -137,7 +137,10 @@ class dsp(object): "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 8fb6513..f3608ce 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -2,25 +2,49 @@ import logging 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 @@ class PropertyManager(object): 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 @@ class PropertyManager(object): 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 @@ class PropertyManager(object): return self.getProperty(name).getValue() def wire(self, callback): - self.callbacks.append(callback) - return self + sub = Subscription(self, callback) + self.subscribers.append(sub) + return sub - def unwire(self, callback): - self.callbacks.remove(callback) + 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 76a93a4..2a301d7 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -19,7 +19,7 @@ class OpenWebRxClient(object): self.dsp = None self.sdr = None - self.configProps = None + self.configSub = None pm = PropertyManager.getSharedInstance() @@ -39,11 +39,6 @@ class OpenWebRxClient(object): 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 @@ class OpenWebRxClient(object): 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 @@ class OpenWebRxClient(object): 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 c5003d4..b5264b0 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -261,10 +261,6 @@ class SpectrumThread(csdr.output): 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 @@ class SpectrumThread(csdr.output): 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 @@ class SpectrumThread(csdr.output): 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 @@ class DspManager(csdr.output): ).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 @@ class DspManager(csdr.output): "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 @@ class DspManager(csdr.output): 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 094/137] 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 f156d0f..47ebe0d 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -77,6 +77,10 @@ fft_compression="adpcm" #valid values: "adpcm", "none" 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 6ce0ea8..f2aa9df 100755 --- a/csdr.py +++ b/csdr.py @@ -70,6 +70,7 @@ class dsp(object): 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 @@ class dsp(object): 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 @@ class dsp(object): 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 @@ class dsp(object): 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 b5264b0..d75e650 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -355,7 +355,8 @@ class DspManager(csdr.output): 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 095/137] 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 d75e650..f101f5b 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -329,7 +329,7 @@ class DspManager(csdr.output): 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 096/137] 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 f2aa9df..6081157 100755 --- a/csdr.py +++ b/csdr.py @@ -450,7 +450,8 @@ class dsp(object): 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 097/137] 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 3bd92c6..d16b948 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -133,7 +133,8 @@ sdrs = { "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 @@ sdrs = { "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 @@ sdrs = { "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 @@ sdrs = { "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 8fb6513..558e343 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -32,7 +32,7 @@ class PropertyManager(object): 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 3efc7d4..9c41318 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -76,7 +76,7 @@ class SdrSource(object): 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 @@ class SdrSource(object): 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 @@ class HackrfSource(SdrSource): 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 098/137] 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 9c41318..f5a0f5f 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -92,9 +92,13 @@ class SdrSource(object): 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 @@ class SdrSource(object): 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 @@ class SdrSource(object): 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 099/137] 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 dfc7901..e119ffa 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -90,7 +90,7 @@ class FeatureDetector(object): 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 100/137] 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 d16b948..25053eb 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -130,7 +130,8 @@ sdrs = { "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 @@ sdrs = { "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 @@ sdrs = { "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 @@ sdrs = { "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 95ce84f..029b8d4 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -81,7 +81,7 @@ class OpenWebRxClient(object): 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 f5a0f5f..2820abc 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -76,7 +76,7 @@ class SdrSource(object): 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 @@ class SdrSource(object): 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 @@ class HackrfSource(SdrSource): 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 101/137] 30m fix --- config_webrx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config_webrx.py b/config_webrx.py index aece843..75f61b1 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -144,7 +144,8 @@ sdrs = { "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 102/137] 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 25053eb..e97ed63 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -131,17 +131,23 @@ sdrs = { "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 @@ sdrs = { "name":"80m", "center_freq": 3650000, "rf_gain": 4, - "if_gain": 40, "samp_rate": 500000, "start_freq": 3570000, "start_mod": "usb", @@ -161,7 +166,6 @@ sdrs = { "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 2820abc..eeed1a7 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 103/137] 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 6081157..ec2ac12 100755 --- a/csdr.py +++ b/csdr.py @@ -92,13 +92,13 @@ class dsp(object): 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 104/137] 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 38f098e..01aafe7 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 105/137] 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 ec2ac12..5fd2a20 100755 --- a/csdr.py +++ b/csdr.py @@ -98,20 +98,24 @@ class dsp(object): 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 106/137] 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 01aafe7..7c9d237 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 0000000..b99f646 --- /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 66691cc..c390dbf 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 @@ class DspManager(csdr.output): "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 107/137] 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 0a76aad..98bcb0a 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -80,6 +80,8 @@ 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 +# 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 7c9d237..3808f92 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 b99f646..bdb1eab 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 108/137] 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 bdb1eab..d448b55 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -3,36 +3,44 @@ from urllib import request 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 109/137] 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 5fd2a20..6e78adb 100755 --- a/csdr.py +++ b/csdr.py @@ -110,12 +110,12 @@ class dsp(object): 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 110/137] 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 d448b55..8613e64 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -7,40 +7,54 @@ import threading 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 111/137] 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 abd61ab..67ee96b 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 112/137] 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 e119ffa..75ef97d 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 @@ class FeatureDetector(object): 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 113/137] 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 8613e64..e215d89 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -38,7 +38,7 @@ class DmrMetaEnricher(object): 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 114/137] 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 6e78adb..28443f3 100755 --- a/csdr.py +++ b/csdr.py @@ -108,14 +108,15 @@ class dsp(object): 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 115/137] 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 d0385b8..b8ea3a7 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -78,7 +78,9 @@ class WebSocketConnection(object): 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 e422ca4d9b09d5300cc1c9a03131b8bdd6c4c287 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 7 Jun 2019 15:44:11 +0200 Subject: [PATCH 116/137] 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 3bd92c6..a3ea8c0 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -93,7 +93,7 @@ Note: if you experience audio underruns while CPU usage is 100%, you can: # 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 83f9232..bdfcee2 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 @@ class FeatureDetector(object): # 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 3efc7d4..d331c71 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -130,6 +130,7 @@ class SdrSource(object): 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 @@ class SdrplaySource(SdrSource): 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 117/137] 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 b8ea3a7..a247b2a 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -38,12 +38,16 @@ class WebSocketConnection(object): # 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 118/137] 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 7012f0e..ca7d357 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 119/137] 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 28443f3..f04bc6e 100755 --- a/csdr.py +++ b/csdr.py @@ -116,7 +116,7 @@ class dsp(object): 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 120/137] 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 f04bc6e..01c43ec 100755 --- a/csdr.py +++ b/csdr.py @@ -107,8 +107,8 @@ class dsp(object): 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 @@ class dsp(object): 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 121/137] 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 01c43ec..a4c932a 100755 --- a/csdr.py +++ b/csdr.py @@ -119,8 +119,7 @@ class dsp(object): 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 122/137] 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 a4c932a..b7d1ebd 100755 --- a/csdr.py +++ b/csdr.py @@ -111,7 +111,7 @@ class dsp(object): 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 123/137] 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 3808f92..40f95c2 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 124/137] 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 da3c800..255d325 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 4736bf3..9c4e0d8 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 40f95c2..b1e47be 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 125/137] nicer user display panel for YSF, too --- htdocs/index.html | 39 +++++++++++++++++------------ htdocs/openwebrx.css | 14 +++++------ htdocs/openwebrx.js | 58 ++++++++++++++++++++++++++------------------ 3 files changed, 65 insertions(+), 46 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index 255d325..44b414d 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -168,23 +168,32 @@ -
-
-
-
-
-
Timeslot 1
-
-
-
-
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
Timeslot 1
+
+
+
+
+
+
Timeslot 2
-
-
-
-
+
+
+
+
diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 9c4e0d8..0654e52 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 b1e47be..83b2bc1 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 126/137] fix typo --- htdocs/openwebrx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 83b2bc1..67893a2 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 127/137] 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 67893a2..e304950 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 e215d89..ec4966a 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 128/137] 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 0654e52..38ecf2b 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 e304950..f61b845 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 129/137] 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 f61b845..486e3ce 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 130/137] 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 44b414d..8bf1001 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 38ecf2b..92b9f37 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 486e3ce..8a8345b 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 131/137] 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 a430c0d..38d3fa7 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -2,6 +2,8 @@ import os 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 @@ class FeatureDetector(object): 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 132/137] 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 8bf1001..90ac89c 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -168,7 +168,7 @@
    -
    +
    diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 92b9f37..ac90bc6 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 133/137] 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 b7d1ebd..0ef0250 100755 --- a/csdr.py +++ b/csdr.py @@ -67,7 +67,8 @@ class dsp(object): 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 @@ class dsp(object): 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 @@ class dsp(object): 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 @@ class dsp(object): 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 @@ class dsp(object): 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 @@ class dsp(object): 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 @@ class dsp(object): 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 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 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 ac90bc6..3ed6deb 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 8a8345b..debd351 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 134/137] 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 38d3fa7..38b8e27 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -99,7 +99,7 @@ class FeatureDetector(object): 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 135/137] 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 3ed6deb..13a1059 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 136/137] 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 b02ef28..7d62388 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -349,7 +349,8 @@ class DspManager(csdr.output): 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 @@ class DspManager(csdr.output): 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 137/137] 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 0000000..2c54fe1 --- /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 13a1059..5624d79 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 debd351..27cc8fc 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);