diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..22cca21 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,5 @@ +This is a list of the great people who contributed code to the OpenWebRX repository. (Names are sorted alphabetically.) + +Gnoxter +John Seamons, ZL/KF6VO + diff --git a/README.md b/README.md index 006d020..b9505b7 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,28 @@ It has the following features: - it works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28), - currently supports RTL-SDR and HackRF; other SDR hardware may be easily added. -**News:** +**News (2015-08-18)** - My BSc. thesis written on OpenWebRX is available here. - Several bugs were fixed to improve reliability and stability. - OpenWebRX now supports compression of audio and waterfall stream, so the required network uplink bandwidth has been decreased from 2 Mbit/s to about 200 kbit/s per client! (Measured with the default settings. It is also dependent on `fft_size`.) - OpenWebRX now uses sdr.js (*libcsdr* compiled to JavaScript) for some client-side DSP tasks. -- Receivers can now be listed on sdr.hu. +- Receivers can now be listed on SDR.hu. - License for OpenWebRX is now Affero GPL v3. +**News (2015-02-14)** +- The DDC in *csdr* has been [manually optimized](https://github.com/simonyiszk/csdr/blob/2b54054a9f5de9a908ee075b488a5ee74f41ba18/libcsdr.c#L300) for ARM NEON, so it runs around 3 times faster on the Raspberry Pi 2 than before. +- Also we use *ncat* instead of *rtl_mus*, and it is 3 times faster. +- OpenWebRX now supports URLs like: `http://localhost:8073/#freq=145555000,mod=usb` +- UI improvements were made, thanks to John Seamons and Gnoxter. + +> When upgrading OpenWebRX, please make sure that you upgrade *csdr*, and install the new (optional) dependency *ncat*! + +## OpenWebRX servers on SDR.hu + +[SDR.hu](http://sdr.hu) is a site which lists the active, public OpenWebRX servers. Your receiver [can also be part of it](http://sdr.hu/openwebrx), if you want. + +![sdr.hu](/screenshot-sdrhu.png?raw=true) + ## Setup OpenWebRX currently requires Linux and python 2.7 to run. @@ -30,6 +44,11 @@ First you will need to install the dependencies: - libcsdr - rtl-sdr +- ncat (On Debian/Ubuntu, it is in the *nmap* package). + +> By the way, *nmap* is a tool commonly used for auditing network security, and it is not used by OpenWebRX in any way. We need to install it, because the *ncat* command is packaged with it. +> +> *ncat* is a better *netcat* alternative, which is used by OpenWebRX for internally distributing the I/Q data stream. It also solves the problem of having different versions of *netcat* on different Linux distributions, which are not compatible by their command-line arguments. After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server: @@ -39,7 +58,6 @@ You can now open the GUI at http://localhost:807 Please note that the server is also listening on the following ports (on localhost only): -- port 8888 for the I/Q source, - port 4951 for the multi-user I/Q server. Now the next step is to customize the parameters of your server in `config_webrx.py`. @@ -61,7 +79,7 @@ If you have any problems installing OpenWebRX, you should check out the - - 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 . - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - In addition, as a special exception, the copyright holders - state that config_rtl.py and config_webrx.py are not part of the - Corresponding Source defined in GNU AGPL version 3 section 1. - - (It means that you do not have to redistribute config_rtl.py and - config_webrx.py if you make any changes to these two configuration files, - and use them for running your own web service with OpenWebRX.) -''' - -my_ip='127.0.0.1' # leave blank for listening on all interfaces -my_listening_port = 4951 - -rtl_tcp_host,rtl_tcp_port='localhost',8888 - -send_first="" -#send_first=chr(9)+chr(0)+chr(0)+chr(0)+chr(1) # set direct sampling - -setuid_on_start = 0 # we normally start with root privileges and setuid() to another user -uid = 999 # determine by issuing: $ id -u username -ignore_clients_without_commands = 1 # we won't serve data to telnet sessions and things like that - # we'll start to serve data after getting the first valid command - -freq_allowed_ranges = [[0,2200000000]] - -client_cant_set_until=0 -first_client_can_set=True # openwebrx - spectrum thread will set things on start # no good, clients set parameters and things -buffer_size=25000000 # per client -log_file_path = "/dev/null" # Might be set to /dev/null to turn off logging - -''' -Allow any host to connect: - use_ip_access_control=0 - -Allow from specific ranges: - use_ip_access_control=1 - order_allow_deny=0 # deny and then allow - denied_ip_ranges=() # deny from all - allowed_ip_ranges=('192.168.','44.','127.0.0.1') # allow only from ... - -Deny from specific ranges: - use_ip_access_control=1 - order_allow_deny=0 # allow and then deny - allowed_ip_ranges=() # allow from all - denied_ip_ranges=('192.168.') # deny any hosts from ... -''' -use_ip_access_control=1 #You may want to open up the I/Q server to the public, then set this to zero. -order_allow_deny=0 -denied_ip_ranges=() # deny from all -allowed_ip_ranges=('127.0.0.1') # allow only local connections (from openwebrx). -allow_gain_set=1 - -use_dsp_command=False # you can process raw I/Q data with a custom command that starts a process that we can pipe the data into, and also pipe out of. -debug_dsp_command=False # show sample rate before and after the dsp command -dsp_command="" - -''' -Example DSP commands: - * Compress I/Q data with FLAC: - flac --force-raw-format --channels 2 --sample-rate=250000 --sign=unsigned --bps=8 --endian=little -o - - - * Decompress FLAC-coded I/Q data: - flac --force-raw-format --decode --endian=little --sign=unsigned - - -''' -watchdog_interval=0 -reconnect_interval=10 -''' -If there's no input I/Q data after N seconds, input will be filled with zero samples, -so that GNU Radio won't fail in OpenWebRX. It may reconnect rtl_tcp_thread. -If watchdog_interval is 0, then watchdog thread is not started. - -''' -cache_full_behaviour=2 -''' - 0 = drop samples - 1 = close client - 2 = openwebrx: don't care about that client until it wants samples again (gr-osmosdr bug workaround) -''' - -rtl_tcp_password=None -''' -This one applies to a special version of rtl_tcp that has authentication. -# You can find more info here: https://github.com/ha7ilm/rtl-sdr -# If it is set to a string (e.g. rtl_tcp_password="changeme"), rtl_mus will try to authenticate against the rtl_tcp server. -''' diff --git a/config_webrx.py b/config_webrx.py index 5161edd..445de4b 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -81,7 +81,7 @@ start_rtl_thread=True # >> RTL-SDR via rtl_sdr -start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} - | nc -vvl 127.0.0.1 8888".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) +start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) format_conversion="csdr convert_u8_f" #start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l16 -a0 -r hackrf_pipe & cat hackrf_pipe | nc -vvl 127.0.0.1 8888".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) @@ -94,23 +94,31 @@ format_conversion="csdr convert_u8_f" # >> Sound card SDR (needs ALSA) #I did not have the chance to properly test it. #samp_rate = 96000 -#start_rtl_command="arecord -f S16_LE -r {samp_rate} -c2 - | nc -vvl 127.0.0.1 8888".format(samp_rate=samp_rate) -#format_conversion="csdr convert_i16_f | csdr gain_ff 30" - -# >> RTL_SDR via rtl_tcp -#start_rtl_command="rtl_tcp -s {samp_rate} -f {center_freq} -g {rf_gain} -P {ppm} -p 8888".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) -#format_conversion="csdr convert_u8_f" +#start_rtl_command="arecord -f S16_LE -r {samp_rate} -c2 -".format(samp_rate=samp_rate) +#format_conversion="csdr convert_s16_f | csdr gain_ff 30" # >> /dev/urandom test signal source #samp_rate = 2400000 -#start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1) | nc -vvl 127.0.0.1 8888".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate) +#start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate) #format_conversion="csdr convert_u8_f" #You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... shown_center_freq = center_freq #you can change this if you use an upconverter -client_audio_buffer_size = 4 +client_audio_buffer_size = 5 #increasing client_audio_buffer_size will: # - also increase the latency # - decrease the chance of audio underruns + +start_freq = center_freq +start_mod = "nfm" #nfm, am, lsb, usb, cw + +iq_server_port = 4951 #TCP port for ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default. + +#access_log = "~/openwebrx_access.log" + +#Warning! The settings below are very experimental. +csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr. +csdr_print_bufsizes = False # This prints the buffer sizes used for csdr processes. +csdr_through = False # Setting this True will print out how much data is going into the DSP chains. diff --git a/htdocs/inactive.html b/htdocs/inactive.html new file mode 100644 index 0000000..c7214c5 --- /dev/null +++ b/htdocs/inactive.html @@ -0,0 +1,85 @@ + + +OpenWebRX + + + + +
+ +
+ Sorry, the receiver is inactive due to internal error. +
+
+ + + diff --git a/htdocs/index.wrx b/htdocs/index.wrx index fa64d08..73e6d25 100644 --- a/htdocs/index.wrx +++ b/htdocs/index.wrx @@ -23,11 +23,13 @@ OpenWebRX | Open Source SDR Web App for Everyone! diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index a4bc72c..4cde11c 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -988,6 +988,32 @@ function resize_waterfall_container(check_init) canvas_container.style.height=(window.innerHeight-e("webrx-top-container").clientHeight-e("openwebrx-scale-container").clientHeight).toString()+"px"; } + +audio_server_output_rate=11025; +audio_client_resampling_factor=4; + + +function audio_calculate_resampling(targetRate) +{ //both at the server and the client + output_range_max = 12000; + output_range_min = 8000; + i = 1; + while(true) + { + audio_server_output_rate = Math.floor(targetRate / i); + if(audio_server_output_rate < output_range_min) + { + audio_client_resampling_factor = audio_server_output_rate = 0; + divlog("Your audio card sampling rate ("+targetRate.toString()+") is not supported.
Please change your operating system default settings in order to fix this.",1); + } + if(audio_server_output_rate >= output_range_min && audio_server_output_rate <= output_range_max) break; //okay, we're done + i++; + } + audio_client_resampling_factor=i; + console.log("audio_calculate_resampling() :: "+audio_client_resampling_factor.toString()+", "+audio_server_output_rate.toString()); +} + + debug_ws_data_received=0; max_clients_num=0; @@ -1003,6 +1029,7 @@ function on_ws_recv(evt) { var stringData=arrayBufferToString(evt.data); if(stringData.substring(0,16)=="CLIENT DE SERVER") divlog("Acknowledged WebSocket connection: "+stringData); + } if(firstChars=="AUD") { @@ -1021,6 +1048,7 @@ function on_ws_recv(evt) else if(fft_compression="adpcm") { fft_codec.reset(); + var waterfall_i16=fft_codec.decode(new Uint8Array(evt.data,4)); var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N); for(var i=0;iaudio_buffering_fill_to) audio_buffering=false; + if(audio_buffering && audio_prepared_buffers.length>audio_buffering_fill_to) { console.log("buffers now: "+audio_prepared_buffers.length.toString()); audio_buffering=false; } } @@ -1278,6 +1307,8 @@ var audio_buffer_progressbar_update_disabled=false; var audio_buffer_total_average_level=0; var audio_buffer_total_average_level_length=0; +var audio_overrun_cnt = 0; +var audio_underrun_cnt = 0; function audio_buffer_progressbar_update() { @@ -1287,8 +1318,8 @@ function audio_buffer_progressbar_update() var overrun=audio_buffer_value>audio_buffer_maximal_length_sec; var underrun=audio_prepared_buffers.length==0; var text="buffer"; - if(overrun) text="overrun"; - if(underrun) text="underrun"; + if(overrun) { text="overrun"; console.log("audio overrun, "+(++audio_overrun_cnt).toString()); } + if(underrun) { text="underrun"; console.log("audio underrun, "+(++audio_underrun_cnt).toString()); } if(overrun||underrun) { audio_buffer_progressbar_update_disabled=true; @@ -1377,13 +1408,26 @@ function webrx_set_param(what, value) ws.send("SET "+what+"="+value.toString()); } -function audio_init() +function parsehash() { - audio_debug_time_start=(new Date()).getTime(); - audio_debug_time_last_start=audio_debug_time_start; + if(h=window.location.hash) + { + h.substring(1).split(",").forEach(function(x){ + harr=x.split("="); + console.log(harr); + if(harr[0]=="mod") starting_mod = harr[1]; + if(harr[0]=="freq") { + console.log(parseInt(harr[1])); + console.log(center_freq); + starting_offset_frequency = parseInt(harr[1])-center_freq; + } + }); + + } +} - //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js - audio_initialized=1; // only tell on_ws_recv() not to call it again +function audio_preinit() +{ try { window.AudioContext = window.AudioContext||window.webkitAudioContext; @@ -1394,6 +1438,28 @@ function audio_init() divlog('Your browser does not support Web Audio API, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser.', 1); } + //we send our setup packet + + parsehash(); + //needs audio_context.sampleRate to exist + + audio_calculate_resampling(audio_context.sampleRate); + audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1); + ws.send("SET output_rate="+audio_server_output_rate.toString()+" action=start"); //now we'll get AUD packets as well + +} + +function audio_init() +{ + if(audio_client_resampling_factor==0) return; //if failed to find a valid resampling factor... + + audio_debug_time_start=(new Date()).getTime(); + audio_debug_time_last_start=audio_debug_time_start; + + //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js + audio_initialized=1; // only tell on_ws_recv() not to call it again + + //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor createjsnode_function = (audio_context.createJavaScriptNode == undefined)?audio_context.createScriptProcessor.bind(audio_context):audio_context.createJavaScriptNode.bind(audio_context); audio_node = createjsnode_function(audio_buffer_size, 0, 1); @@ -1411,7 +1477,14 @@ function audio_init() audio_buffer = audio_context.createBuffer(xhr.response, false); audio_source.buffer = buffer; audio_source.noteOn(0);*/ - demodulator_analog_replace('nfm'); //needs audio_context.sampleRate to exist + demodulator_analog_replace(starting_mod); + if(starting_offset_frequency) + { + demodulators[0].offset_frequency = starting_offset_frequency; + demodulators[0].set(); + mkscale(); + } + //hide log panel in a second (if user has not hidden it yet) window.setTimeout(function(){ if(typeof e("openwebrx-panel-log").openwebrxHidden == "undefined" && !was_error) diff --git a/openwebrx.py b/openwebrx.py index 62ff800..6c78ee3 100755 --- a/openwebrx.py +++ b/openwebrx.py @@ -29,6 +29,7 @@ import plugins import plugins.dsp import thread import time +import datetime import subprocess import os from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer @@ -47,7 +48,6 @@ import ctypes #import rtl_mus import rxws import uuid -import config_webrx as cfg import signal import socket @@ -75,11 +75,22 @@ class MultiThreadHTTPServer(ThreadingMixIn, HTTPServer): pass def handle_signal(signal, frame): + global spectrum_dsp print "[openwebrx] Ctrl+C: aborting." + cleanup_clients(True) + spectrum_dsp.stop() os._exit(1) #not too graceful exit +def access_log(data): + global logs + logs.access_log.write("["+datetime.datetime.now().isoformat()+"] "+data+"\n") + logs.access_log.flush() + +receiver_failed=spectrum_thread_watchdog_last_tick=rtl_thread=spectrum_dsp=server_fail=None + def main(): - global clients, clients_mutex, pypy, lock_try_time, avatar_ctime + global clients, clients_mutex, pypy, lock_try_time, avatar_ctime, cfg, logs + global serverfail, rtl_thread print print "OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package" print "_________________________________________________________________________________________________" @@ -87,6 +98,15 @@ def main(): print "Author contact info: Andras Retzler, HA7ILM " print + no_arguments=len(sys.argv)==1 + if no_arguments: print "[openwebrx-main] Configuration script not specified. I will use: \"config_webrx.py\"" + cfg=__import__("config_webrx" if no_arguments else sys.argv[1]) + for option in ("access_log","csdr_dynamic_bufsize","csdr_print_bufsizes","csdr_through"): + if not option in dir(cfg): setattr(cfg, option, False) #initialize optional config parameters + + #Open log files + logs = type("logs_class", (object,), {"access_log":open(cfg.access_log if cfg.access_log else "/dev/null","a"), "error_log":""})() + #Set signal handler signal.signal(signal.SIGINT, handle_signal) #http://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python @@ -107,16 +127,14 @@ def main(): pass #Start rtl thread + if os.system("ncat --version > /dev/null") == 32512: #check for ncat + print "[openwebrx-main] Error: ncat not detected, please install it! The ncat tool is a netcat alternative, used for distributing the I/Q data stream. It is usually available in the nmap package (sudo apt-get install nmap). For more explanation, look into the README.md" + return if cfg.start_rtl_thread: - rtl_thread=threading.Thread(target = lambda:subprocess.Popen(cfg.start_rtl_command, shell=True), args=()) + cfg.start_rtl_command += "| ncat -4l %d -k --send-only --allow 127.0.0.1" % cfg.iq_server_port + rtl_thread=threading.Thread(target = lambda:subprocess.Popen(cfg.start_rtl_command, shell=True), args=()) rtl_thread.start() - print "[openwebrx-main] Started rtl thread: "+cfg.start_rtl_command - - #Run rtl_mus.py in a different OS thread - python_command="pypy" if pypy else "python2" - rtl_mus_thread=threading.Thread(target = lambda:subprocess.Popen(python_command+" rtl_mus.py config_rtl", shell=True), args=()) - rtl_mus_thread.start() # The new feature in GNU Radio 3.7: top_block() locks up ALL python threads until it gets the TCP connection. - print "[openwebrx-main] Started rtl_mus." + print "[openwebrx-main] Started rtl_thread: "+cfg.start_rtl_command time.sleep(1) #wait until it really starts #Initialize clients @@ -136,6 +154,8 @@ def main(): print "[openwebrx-main] Starting spectrum thread." spectrum_thread=threading.Thread(target = spectrum_thread_function, args = ()) spectrum_thread.start() + spectrum_watchdog_thread=threading.Thread(target = spectrum_watchdog_thread_function, args = ()) + spectrum_watchdog_thread.start() get_cpu_usage() bcastmsg_thread=threading.Thread(target = bcastmsg_thread_function, args = ()) @@ -146,13 +166,14 @@ def main(): #Start sdr.hu update thread if sdrhu and cfg.sdrhu_key and cfg.sdrhu_public_listing: print "[openwebrx-main] Starting sdr.hu update thread..." + avatar_ctime=str(os.path.getctime("htdocs/gfx/openwebrx-avatar.png")) sdrhu_thread=threading.Thread(target = sdrhu.run, args = ()) sdrhu_thread.start() - avatar_ctime=str(os.path.getctime("htdocs/gfx/openwebrx-avatar.png")) #Start HTTP thread httpd = MultiThreadHTTPServer(('', cfg.web_port), WebRXHandler) print('[openwebrx-main] Starting HTTP server.') + access_log("Starting OpenWebRX...") httpd.serve_forever() @@ -204,27 +225,61 @@ def mutex_watchdog_thread_function(): while True: if lock_try_time != 0 and time.time()-lock_try_time > 3.0: #if 3 seconds pass without unlock - print "[openwebrx-watchdog] Mutex unlock timeout. Locker: \""+str(clients_mutex_locker)+"\" Now unlocking..." + print "[openwebrx-mutex-watchdog] Mutex unlock timeout. Locker: \""+str(clients_mutex_locker)+"\" Now unlocking..." clients_mutex.release() time.sleep(0.5) - + +def spectrum_watchdog_thread_function(): + global spectrum_thread_watchdog_last_tick, receiver_failed + while True: + time.sleep(60) + if spectrum_thread_watchdog_last_tick and time.time()-spectrum_thread_watchdog_last_tick > 60.0: + print "[openwebrx-spectrum-watchdog] Spectrum timeout. Seems like no I/Q data is coming from the receiver.\nIf you're using RTL-SDR, the receiver hardware may randomly fail under some circumstances:\n1) high temperature,\n2) insufficient current available from the USB port." + print "[openwebrx-spectrum-watchdog] Deactivating receiver." + receiver_failed="spectrum" + return + +def check_server(): + global spectrum_dsp, server_fail, rtl_thread + if server_fail: return server_fail + #print spectrum_dsp.process.poll() + if spectrum_dsp and spectrum_dsp.process.poll()!=None: server_fail = "spectrum_thread dsp subprocess failed" + #if rtl_thread and not rtl_thread.is_alive(): server_fail = "rtl_thread failed" + if server_fail: print "[openwebrx-check_server] >>>>>>> ERROR:", server_fail + return server_fail + +def apply_csdr_cfg_to_dsp(dsp): + dsp.csdr_dynamic_bufsize = cfg.csdr_dynamic_bufsize + dsp.csdr_print_bufsizes = cfg.csdr_print_bufsizes + dsp.csdr_through = cfg.csdr_through + def spectrum_thread_function(): - global clients - dsp=getattr(plugins.dsp,cfg.dsp_plugin).plugin.dsp_plugin() + global clients, spectrum_dsp, spectrum_thread_watchdog_last_tick + spectrum_dsp=dsp=getattr(plugins.dsp,cfg.dsp_plugin).plugin.dsp_plugin() + dsp.nc_port=cfg.iq_server_port dsp.set_demodulator("fft") dsp.set_samp_rate(cfg.samp_rate) dsp.set_fft_size(cfg.fft_size) dsp.set_fft_fps(cfg.fft_fps) dsp.set_fft_compression(cfg.fft_compression) dsp.set_format_conversion(cfg.format_conversion) + apply_csdr_cfg_to_dsp(dsp) sleep_sec=0.87/cfg.fft_fps print "[openwebrx-spectrum] Spectrum thread initialized successfully." dsp.start() + if cfg.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 True: data=dsp.read(bytes_to_read) #print "gotcha",len(data),"bytes of spectrum data via spectrum_thread_function()" + if spectrum_thread_counter >= cfg.fft_fps: + spectrum_thread_counter=0 + spectrum_thread_watchdog_last_tick = time.time() #once every second + else: spectrum_thread_counter+=1 cma("spectrum_thread") correction=0 for i in range(0,len(clients)): @@ -255,16 +310,17 @@ def get_client_by_id(client_id, use_mutex=True): def log_client(client, what): print "[openwebrx-httpd] client {0}#{1} :: {2}".format(client.ip,client.id,what) -def cleanup_clients(): - # if client doesn't open websocket for too long time, we drop it +def cleanup_clients(end_all=False): + # - if a client doesn't open websocket for too long time, we drop it + # - or if end_all is true, we drop all clients global clients cma("cleanup_clients") correction=0 for i in range(0,len(clients)): i-=correction #print "cleanup_clients:: len(clients)=", len(clients), "i=", i - if (not clients[i].ws_started) and (time.time()-clients[i].gen_time)>45: - print "[openwebrx] cleanup_clients :: client timeout to open WebSocket" + if end_all or ((not clients[i].ws_started) and (time.time()-clients[i].gen_time)>45): + if not end_all: print "[openwebrx] cleanup_clients :: client timeout to open WebSocket" close_client(i, False) correction+=1 cmr() @@ -299,6 +355,7 @@ def close_client(i, use_mutex=True): print "[openwebrx] close_client dsp.stop() :: error -",exc_type,exc_value traceback.print_tb(exc_traceback) clients[i].closed[0]=True + access_log("Stopped streaming to client: "+clients[i].ip+"#"+str(clients[i].id)+" (users now: "+str(len(clients)-1)+")") del clients[i] if use_mutex: cmr() @@ -319,18 +376,20 @@ class WebRXHandler(BaseHTTPRequestHandler): def do_GET(self): self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - global dsp_plugin, clients_mutex, clients, avatar_ctime, sw_version + global dsp_plugin, clients_mutex, clients, avatar_ctime, sw_version, receiver_failed rootdir = 'htdocs' self.path=self.path.replace("..","") path_temp_parts=self.path.split("?") self.path=path_temp_parts[0] request_param=path_temp_parts[1] if(len(path_temp_parts)>1) else "" + access_log("GET "+self.path+" from "+self.client_address[0]) try: if self.path=="/": self.path="/index.wrx" # there's even another cool tip at http://stackoverflow.com/questions/4419650/how-to-implement-timeout-in-basehttpserver-basehttprequesthandler-python #if self.path[:5]=="/lock": cma("do_GET /lock/") # to test mutex_watchdog_thread. Do not uncomment in production environment! if self.path[:4]=="/ws/": + if receiver_failed: self.send_error(500,"Internal server error") try: # ========= WebSocket handshake ========= ws_success=True @@ -363,23 +422,26 @@ class WebRXHandler(BaseHTTPRequestHandler): # ========= Initialize DSP ========= dsp=getattr(plugins.dsp,cfg.dsp_plugin).plugin.dsp_plugin() - dsp.set_samp_rate(cfg.samp_rate) - dsp.set_demodulator("nfm") - dsp.set_offset_freq(0) - dsp.set_bpf(-4000,4000) + dsp_initialized=False dsp.set_audio_compression(cfg.audio_compression) dsp.set_format_conversion(cfg.format_conversion) - dsp.start() + dsp.set_offset_freq(0) + dsp.set_bpf(-4000,4000) + dsp.nc_port=cfg.iq_server_port + apply_csdr_cfg_to_dsp(dsp) myclient.dsp=dsp + access_log("Started streaming to client: "+self.client_address[0]+"#"+myclient.id+" (users now: "+str(len(clients))+")") + while True: if myclient.closed[0]: print "[openwebrx-httpd:ws] client closed by other thread" break # ========= send audio ========= - temp_audio_data=dsp.read(256) - rxws.send(self, temp_audio_data, "AUD ") + if dsp_initialized: + temp_audio_data=dsp.read(256) + rxws.send(self, temp_audio_data, "AUD ") # ========= send spectrum ========= while not myclient.spectrum_queue.empty(): @@ -417,9 +479,17 @@ class WebRXHandler(BaseHTTPRequestHandler): dsp.set_offset_freq(int(param_value)) elif param_name=="mod": if (dsp.get_demodulator()!=param_value): - dsp.stop() + if dsp_initialized: dsp.stop() dsp.set_demodulator(param_value) + if dsp_initialized: dsp.start() + elif param_name == "output_rate": + if not dsp_initialized: + dsp.set_output_rate(int(param_value)) + dsp.set_samp_rate(cfg.samp_rate) + elif param_name=="action" and param_value=="start": + if not dsp_initialized: dsp.start() + dsp_initialized=True else: print "[openwebrx-httpd:ws] invalid parameter" if bpf_set: @@ -457,13 +527,17 @@ class WebRXHandler(BaseHTTPRequestHandler): elif self.path in ("/status", "/status/"): #self.send_header('Content-type','text/plain') getbands=lambda: str(int(cfg.shown_center_freq-cfg.samp_rate/2))+"-"+str(int(cfg.shown_center_freq+cfg.samp_rate/2)) - self.wfile.write("status=active\nname="+cfg.receiver_name+"\nsdr_hw="+cfg.receiver_device+"\nop_email="+cfg.receiver_admin+"\nbands="+getbands()+"\nusers="+str(len(clients))+"\navatar_ctime="+avatar_ctime+"\ngps="+str(cfg.receiver_gps)+"\nasl="+str(cfg.receiver_asl)+"\nloc="+cfg.receiver_location+"\nsw_version="+sw_version+"\nantenna="+cfg.receiver_ant+"\n") + self.wfile.write("status="+("inactive" if receiver_failed else "active")+"\nname="+cfg.receiver_name+"\nsdr_hw="+cfg.receiver_device+"\nop_email="+cfg.receiver_admin+"\nbands="+getbands()+"\nusers="+str(len(clients))+"\nusers_max="+str(cfg.max_clients)+"\navatar_ctime="+avatar_ctime+"\ngps="+str(cfg.receiver_gps)+"\nasl="+str(cfg.receiver_asl)+"\nloc="+cfg.receiver_location+"\nsw_version="+sw_version+"\nantenna="+cfg.receiver_ant+"\n") print "[openwebrx-httpd] GET /status/ from",self.client_address[0] else: f=open(rootdir+self.path) data=f.read() extension=self.path[(len(self.path)-4):len(self.path)] extension=extension[2:] if extension[1]=='.' else extension[1:] + checkresult=check_server() + if extension == "wrx" and (checkresult or receiver_failed): + self.send_302("inactive.html") + return if extension == "wrx" and ((self.headers['user-agent'].count("Chrome")==0 and self.headers['user-agent'].count("Firefox")==0 and (not "Googlebot" in self.headers['user-agent'])) if 'user-agent' in self.headers.keys() else True) and (not request_param.count("unsupported")): self.send_302("upgrade.html") return @@ -492,7 +566,9 @@ class WebRXHandler(BaseHTTPRequestHandler): ("%[RX_ADMIN]",cfg.receiver_admin), ("%[RX_ANT]",cfg.receiver_ant), ("%[RX_DEVICE]",cfg.receiver_device), - ("%[AUDIO_BUFSIZE]",str(cfg.client_audio_buffer_size)) + ("%[AUDIO_BUFSIZE]",str(cfg.client_audio_buffer_size)), + ("%[START_OFFSET_FREQ]",str(cfg.start_freq-cfg.center_freq)), + ("%[START_MOD]",cfg.start_mod) ) for rule in replace_dictionary: while data.find(rule[0])!=-1: diff --git a/plugins/dsp/csdr/plugin.py b/plugins/dsp/csdr/plugin.py index 1fcda24..ea5912e 100644 --- a/plugins/dsp/csdr/plugin.py +++ b/plugins/dsp/csdr/plugin.py @@ -44,13 +44,17 @@ class dsp_plugin: self.demodulator = "nfm" self.name = "csdr" self.format_conversion = "csdr convert_u8_f" - try: - subprocess.Popen("nc",stdout=subprocess.PIPE,stderr=subprocess.PIPE).kill() - except: - print "[openwebrx-plugin:csdr] error: netcat not found, please install netcat!" + self.base_bufsize = 512 + self.nc_port = 4951 + self.csdr_dynamic_bufsize = False + self.csdr_print_bufsizes = False + self.csdr_through = False def chain(self,which): - any_chain_base="nc -v localhost 4951 | "+self.format_conversion+(" | " if self.format_conversion!="" else "")+"csdr flowcontrol {flowcontrol} 10 | " + any_chain_base="ncat -v 127.0.0.1 {nc_port} | " + if self.csdr_dynamic_bufsize: any_chain_base+="csdr setbuf {start_bufsize} | " + if self.csdr_through: any_chain_base+="csdr through | " + any_chain_base+=self.format_conversion+(" | " if self.format_conversion!="" else "") ##"csdr flowcontrol {flowcontrol} auto 1.5 10 | " if which == "fft": fft_chain_base = "sleep 1; "+any_chain_base+"csdr fft_cc {fft_size} {fft_block_size} | csdr logpower_cf -70 | csdr fft_exchange_sides_ff {fft_size}" if self.fft_compression=="adpcm": @@ -61,9 +65,9 @@ class dsp_plugin: chain_end = "" if self.audio_compression=="adpcm": chain_end = " | csdr encode_ima_adpcm_i16_u8" - if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr fastagc_ff | csdr convert_f_i16"+chain_end + if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr fastagc_ff 1024 | csdr convert_f_i16"+chain_end elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_i16"+chain_end - elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_i16"+chain_end + elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr clipdetect_ff | csdr limit_ff | csdr convert_f_i16"+chain_end def set_audio_compression(self,what): self.audio_compression = what @@ -92,6 +96,10 @@ class dsp_plugin: def get_output_rate(self): return self.output_rate + def set_output_rate(self,output_rate): + self.output_rate=output_rate + self.set_samp_rate(self.samp_rate) #as it depends on output_rate + def set_demodulator(self,demodulator): #to change this, restart is required self.demodulator=demodulator @@ -153,10 +161,17 @@ class dsp_plugin: self.mkfifo(self.shift_pipe) #run the command - command=command_base.format(bpf_pipe=self.bpf_pipe,shift_pipe=self.shift_pipe,decimation=self.decimation,last_decimation=self.last_decimation,fft_size=self.fft_size,fft_block_size=self.fft_block_size(),bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(),ddc_transition_bw=self.ddc_transition_bw(),flowcontrol=int(self.samp_rate*4*2*1.5)) + command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, \ + last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), \ + bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), \ + flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port) + print "[openwebrx-dsp-plugin:csdr] Command =",command #code.interact(local=locals()) - self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp) + my_env=os.environ.copy() + if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; + if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; + self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) self.running = True #open control pipes for csdr and send initialization data @@ -179,14 +194,16 @@ class dsp_plugin: # os.killpg(self.process.pid, signal.SIGTERM) # # time.sleep(0.1) - try: - os.unlink(self.bpf_pipe) - except: - print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe - try: - os.unlink(self.shift_pipe) - except: - print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe + if self.bpf_pipe: + try: + os.unlink(self.bpf_pipe) + except: + print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe + if self.shift_pipe: + try: + os.unlink(self.shift_pipe) + except: + print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe self.running = False def restart(self): diff --git a/rtl_mus.py b/rtl_mus.py deleted file mode 100644 index 9f5e230..0000000 --- a/rtl_mus.py +++ /dev/null @@ -1,539 +0,0 @@ -''' - This file is part of RTL Multi-User Server, - that makes multi-user access to your DVB-T dongle used as an SDR. - 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 . - ------ - -2013-11? Asyncore version -2014-03 Fill with null on no data - -''' - -import socket -import sys -import array -import time -import logging -import os -import time -import subprocess -import fcntl -import thread -import pdb -import asyncore -import multiprocessing -import signal -#pypy compatiblity -try: import dl -except: pass - -import code -import traceback - -def handle_signal(signal, frame): - log.info("Ctrl+C: aborting.") - os._exit(1) #not too graceful exit - -def ip_match(this,ip_ranges,for_allow): - if not len(ip_ranges): - return 1 #empty list matches all ip addresses - for ip_range in ip_ranges: - #print this[0:len(ip_range)], ip_range - if this[0:len(ip_range)]==ip_range: - return 1 - return 0 - -def ip_access_control(ip): - if(not cfg.use_ip_access_control): return 1 - allowed=0 - if(cfg.order_allow_deny): - if ip_match(ip,cfg.allowed_ip_ranges,1): allowed=1 - if ip_match(ip,cfg.denied_ip_ranges,0): allowed=0 - else: - if ip_match(ip,cfg.denied_ip_ranges,0): - allowed=0 - if ip_match(ip,cfg.allowed_ip_ranges,1): - allowed=1 - return allowed - -def add_data_to_clients(new_data): - # might be called from: - # -> dsp_read - # -> rtl_tcp_asyncore.handle_read - global clients - global clients_mutex - clients_mutex.acquire() - for client in clients: - #print "client %d size: %d"%(client[0].ident,client[0].waiting_data.qsize()) - if(client[0].waiting_data.full()): - if cfg.cache_full_behaviour == 0: - log.error("client cache full, dropping samples: "+str(client[0].ident)+"@"+client[0].socket[1][0]) - while not client[0].waiting_data.empty(): # clear queue - client[0].waiting_data.get(False, None) - elif cfg.cache_full_behaviour == 1: - #rather closing client: - log.error("client cache full, dropping client: "+str(client[0].ident)+"@"+client[0].socket[1][0]) - client[0].close(False) - elif cfg.cache_full_behaviour == 2: - pass #client cache full, just not taking care - else: log.error("invalid value for cfg.cache_full_behaviour") - else: - client[0].waiting_data.put(new_data) - clients_mutex.release() - -def dsp_read_thread(): - global proc - global dsp_data_count - while True: - try: - my_buffer=proc.stdout.read(1024) - except IOError: - log.error("DSP subprocess is not ready for reading.") - time.sleep(1) - continue - add_data_to_clients(my_buffer) - if cfg.debug_dsp_command: - dsp_data_count+=len(my_buffer) - -def dsp_write_thread(): - global proc - global dsp_input_queue - global original_data_count - while True: - try: - my_buffer=dsp_input_queue.get(timeout=0.3) - except: - continue - proc.stdin.write(my_buffer) - proc.stdin.flush() - if cfg.debug_dsp_command: - original_data_count+=len(my_buffer) - -class client_handler(asyncore.dispatcher): - - def __init__(self,client_param): - self.client=client_param - self.client[0].asyncore=self - self.sent_dongle_id=False - self.last_waiting_buffer="" - asyncore.dispatcher.__init__(self, self.client[0].socket[0]) - self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - - def handle_read(self): - global commands - new_command = self.recv(5) - if len(new_command)>=5: - if handle_command(new_command, self.client): - commands.put(new_command) - - def handle_error(self): - exc_type, exc_value, exc_traceback = sys.exc_info() - log.info("client error: "+str(self.client[0].ident)+"@"+self.client[0].socket[1][0]) - traceback.print_tb(exc_traceback) - self.close() - - def handle_close(self): - self.client[0].close() - log.info("client disconnected: "+str(self.client[0].ident)+"@"+self.client[0].socket[1][0]) - - def writable(self): - #print "queryWritable",not self.client[0].waiting_data.empty() - return not self.client[0].waiting_data.empty() - - def handle_write(self): - global last_waiting - global rtl_dongle_identifier - global sample_rate - if not self.sent_dongle_id: - self.send(rtl_dongle_identifier) - self.sent_dongle_id=True - return - #print "write2client",self.client[0].waiting_data.qsize() - next=self.last_waiting_buffer+self.client[0].waiting_data.get() - sent=asyncore.dispatcher.send(self, next) - self.last_waiting_buffer=next[sent:] - -class server_asyncore(asyncore.dispatcher): - - def __init__(self): - asyncore.dispatcher.__init__(self) - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - self.set_reuse_addr() - self.bind((cfg.my_ip, cfg.my_listening_port)) - self.listen(5) - self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - log.info("Server listening on port: "+str(cfg.my_listening_port)) - - def handle_accept(self): - global max_client_id - global clients_mutex - global clients - my_client=[client()] - my_client[0].socket=self.accept() - if (my_client[0].socket is None): # not sure if required - return - if (ip_access_control(my_client[0].socket[1][0])): - my_client[0].ident=max_client_id - max_client_id+=1 - my_client[0].start_time=time.time() - my_client[0].waiting_data=multiprocessing.Queue(500) - clients_mutex.acquire() - clients.append(my_client) - clients_mutex.release() - handler = client_handler(my_client) - log.info("client accepted: "+str(len(clients)-1)+"@"+my_client[0].socket[1][0]+":"+str(my_client[0].socket[1][1])+" users now: "+str(len(clients))) - else: - log.info("client denied: "+str(len(clients)-1)+"@"+my_client[0].socket[1][0]+":"+str(my_client[0].socket[1][1])+" blocked by ip") - my_client.socket.close() - -rtl_tcp_resetting=False #put me away - -def rtl_tcp_asyncore_reset(timeout): - global rtl_tcp_core - global rtl_tcp_resetting - if rtl_tcp_resetting: return - #print "rtl_tcp_asyncore_reset" - rtl_tcp_resetting=True - time.sleep(timeout) - try: - rtl_tcp_core.close() - except: - pass - try: - del rtl_tcp_core - except: - pass - rtl_tcp_core=rtl_tcp_asyncore() - #print asyncore.socket_map - rtl_tcp_resetting=False - -class rtl_tcp_asyncore(asyncore.dispatcher): - def __init__(self): - global server_missing_logged - asyncore.dispatcher.__init__(self) - self.password_sent = False - self.ok=True - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - try: - self.connect((cfg.rtl_tcp_host, cfg.rtl_tcp_port)) - self.socket.settimeout(0.1) - except: - log.error("rtl_tcp connection refused. Retrying.") - thread.start_new_thread(rtl_tcp_asyncore_reset, (1,)) - self.close() - return - - def handle_error(self): - global server_missing_logged - global rtl_tcp_connected - rtl_tcp_connected=False - exc_type, exc_value, exc_traceback = sys.exc_info() - self.ok=False - server_is_missing=hasattr(exc_value,"errno") and exc_value.errno==111 - if (not server_is_missing) or (not server_missing_logged): - log.error("with rtl_tcp host connection: "+str(exc_value)) - #traceback.print_tb(exc_traceback) - server_missing_logged|=server_is_missing - try: - self.close() - except: - pass - thread.start_new_thread(rtl_tcp_asyncore_reset, (2,)) - - def handle_connect(self): - global server_missing_logged - global rtl_tcp_connected - self.socket.settimeout(0.1) - self.password_sent = False - rtl_tcp_connected=True - if self.ok: - log.info("rtl_tcp host connection estabilished") - server_missing_logged=False - - def handle_close(self): - global rtl_tcp_connected - global rtl_tcp_core - rtl_tcp_connected=False - log.error("rtl_tcp host connection has closed, now trying to reopen") - try: - self.close() - except: - pass - thread.start_new_thread(rtl_tcp_asyncore_reset, (2,)) - - def handle_read(self): - global rtl_dongle_identifier - global dsp_input_queue - global watchdog_data_count - if(len(rtl_dongle_identifier)==0): - rtl_dongle_identifier=self.recv(12) - return - new_data_buffer=self.recv(1024*16) - if cfg.watchdog_interval: - watchdog_data_count+=1024*16 - if cfg.use_dsp_command: - dsp_input_queue.put(new_data_buffer) - #print "did put anyway" - else: - add_data_to_clients(new_data_buffer) - - def writable(self): - - #check if any new commands to write - global commands - return (not self.password_sent and cfg.rtl_tcp_password != None) or not commands.empty() - - def handle_write(self): - if(not self.password_sent and cfg.rtl_tcp_password != None): - log.info("Sending rtl_tcp_password...") - self.send(cfg.rtl_tcp_password) - self.password_sent = True - global commands - while not commands.empty(): - mcmd=commands.get() - self.send(mcmd) - -def xxd(data): - #diagnostic purposes only - output="" - for d in data: - output+=hex(ord(d))[2:].zfill(2)+" " - return output - -def handle_command(command, client_param): - global sample_rate - client=client_param[0] - param=array.array("I", command[1:5])[0] - param=socket.ntohl(param) - command_id=ord(command[0]) - client_info=str(client.ident)+"@"+client.socket[1][0]+":"+str(client.socket[1][1]) - if(time.time()-client.start_time client can't set anything until "+str(cfg.client_cant_set_until)+" seconds") - return 0 - if command_id == 1: - if max(map((lambda r: param>=r[0] and param<=r[1]),cfg.freq_allowed_ranges)): - log.debug("allow: "+client_info+" -> set freq "+str(param)) - return 1 - else: - log.debug("deny: "+client_info+" -> set freq - out of range: "+str(param)) - elif command_id == 2: - log.debug("deny: "+client_info+" -> set sample rate: "+str(param)) - sample_rate=param - return 0 # ordinary clients are not allowed to do this - elif command_id == 3: - log.debug("deny/allow: "+client_info+" -> set gain mode: "+str(param)) - return cfg.allow_gain_set - elif command_id == 4: - log.debug("deny/allow: "+client_info+" -> set gain: "+str(param)) - return cfg.allow_gain_set - elif command_id == 5: - log.debug("deny: "+client_info+" -> set freq correction: "+str(param)) - return 0 - elif command_id == 6: - log.debug("deny/allow: set if stage gain") - return cfg.allow_gain_set - elif command_id == 7: - log.debug("deny: set test mode") - return 0 - elif command_id == 8: - log.debug("deny/allow: set agc mode") - return cfg.allow_gain_set - elif command_id == 9: - log.debug("deny: set direct sampling") - return 0 - elif command_id == 10: - log.debug("deny: set offset tuning") - return 0 - elif command_id == 11: - log.debug("deny: set rtl xtal") - return 0 - elif command_id == 12: - log.debug("deny: set tuner xtal") - return 0 - elif command_id == 13: - log.debug("deny/allow: set tuner gain by index") - return cfg.allow_gain_set - else: - log.debug("deny: "+client_info+" sent an ivalid command: "+str(param)) - return 0 - -def watchdog_thread(): - global rtl_tcp_connected - global rtl_tcp_core - global watchdog_data_count - global sample_rate - zero_buffer_size=16348 - second_frac=10 - zero_buffer='\x7f'*zero_buffer_size - watchdog_data_count=0 - rtl_tcp_connected=False - null_fill=False - time.sleep(4) # wait before activating this thread - log.info("watchdog started") - first_start=True - n=0 - while True: - wait_altogether=cfg.watchdog_interval if rtl_tcp_connected or first_start else cfg.reconnect_interval - first_start=False - if null_fill: - log.error("watchdog: filling buffer with zeros.") - while wait_altogether>0: - wait_altogether-=1.0/second_frac - for i in range(0,((2*sample_rate)/second_frac)/zero_buffer_size): - add_data_to_clients(zero_buffer) - n+=len(zero_buffer) - time.sleep(0) #yield - if watchdog_data_count: break - if watchdog_data_count: break - time.sleep(1.0/second_frac) - #print "sent altogether",n - else: - time.sleep(wait_altogether) - null_fill=not watchdog_data_count - if not watchdog_data_count: - log.error("watchdog: restarting rtl_tcp_asyncore() now.") - rtl_tcp_asyncore_reset(0) - watchdog_data_count=0 - - - -def dsp_debug_thread(): - global dsp_data_count - global original_data_count - while 1: - time.sleep(1) - print "[rtl-mus] DSP | Original data: "+str(int(original_data_count/1000))+"kB/sec | Processed data: "+str(int(dsp_data_count/1000))+"kB/sec" - dsp_data_count = original_data_count=0 - -class client: - ident=None #id - to_close=False - waiting_data=None - start_time=None - socket=None - asyncore=None - - def close(self, use_mutex=True): - global clients_mutex - global clients - if use_mutex: clients_mutex.acquire() - correction=0 - for i in range(0,len(clients)): - i-=correction - if clients[i][0].ident==self.ident: - try: - self.socket.close() - except: - pass - try: - self.asyncore.close() - del self.asyncore - except: - pass - del clients[i] - correction+=1 - if use_mutex: clients_mutex.release() - - -def main(): - global server_missing_logged - global rtl_dongle_identifier - global log - global clients - global clients_mutex - global original_data_count - global dsp_input_queue - global dsp_data_count - global proc - global commands - global max_client_id - global rtl_tcp_core - global sample_rate - - #Set signal handler - signal.signal(signal.SIGINT, handle_signal) #http://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python - - # set up logging - log = logging.getLogger("rtl_mus") - log.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') - stream_handler = logging.StreamHandler() - stream_handler.setLevel(logging.DEBUG) - stream_handler.setFormatter(formatter) - log.addHandler(stream_handler) - file_handler = logging.FileHandler(cfg.log_file_path) - file_handler.setLevel(logging.INFO) - file_handler.setFormatter(formatter) - log.addHandler(file_handler) - log.info("Server is UP") - - server_missing_logged=0 # Not to flood the screen with messages related to rtl_tcp disconnect - rtl_dongle_identifier='' # rtl_tcp sends some identifier on dongle type and gain values in the first few bytes right after connection - clients=[] - dsp_data_count=original_data_count=0 - commands=multiprocessing.Queue() - dsp_input_queue=multiprocessing.Queue() - clients_mutex=multiprocessing.Lock() - max_client_id=0 - sample_rate=250000 # so far only watchdog thread uses it to fill buffer up with zeros on missing input - - # start dsp threads - if cfg.use_dsp_command: - print "[rtl_mus] Opening DSP process..." - proc = subprocess.Popen (cfg.dsp_command.split(" "), stdin = subprocess.PIPE, stdout = subprocess.PIPE) #!! should fix the split :-S - dsp_read_thread_v=thread.start_new_thread(dsp_read_thread, ()) - dsp_write_thread_v=thread.start_new_thread(dsp_write_thread, ()) - if cfg.debug_dsp_command: - dsp_debug_thread_v=thread.start_new_thread(dsp_debug_thread,()) - - # start watchdog thread - if cfg.watchdog_interval != 0: - watchdog_thread_v=thread.start_new_thread(watchdog_thread,()) - - # start asyncores - rtl_tcp_core = rtl_tcp_asyncore() - server_core = server_asyncore() - - asyncore.loop(0.1) - - -if __name__=="__main__": - print - print "rtl_mus: Multi-User I/Q Data Server for RTL-SDR v0.22, made at HA5KFU Amateur Radio Club (http://ha5kfu.hu)" - print " code by Andras Retzler, HA7ILM " - print " distributed under GNU GPL v3" - print - - try: - for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]: - if os.path.exists(libcpath): - libc = dl.open(libcpath) - libc.call("prctl", 15, "rtl_mus", 0, 0, 0) - break - except: - pass - - # === Load configuration script === - if len(sys.argv)==1: - print "[rtl_mus] Warning! Configuration script not specified. I will use: \"config_rtl.py\"" - config_script="config_rtl" - else: - config_script=sys.argv[1] - cfg=__import__(config_script) - if cfg.setuid_on_start: - os.setuid(cfg.uid) - main() diff --git a/screenshot-sdrhu.png b/screenshot-sdrhu.png new file mode 100644 index 0000000..3d56bd0 Binary files /dev/null and b/screenshot-sdrhu.png differ