Added some features.

This commit is contained in:
ha7ilm 2015-09-30 14:06:30 +00:00
parent c9bf26f1ac
commit 4b3cc10924
6 changed files with 199 additions and 57 deletions

View File

@ -14,7 +14,7 @@ It has the following features:
- it works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28),
- currently only supports RTL-SDR, but other SDR hardware may be easily added.
**News:**
**News (2015-08-18)**
- My BSc. thesis written on OpenWebRX is <a href="http://openwebrx.org/bsc-thesis.pdf">available here.</a>
- 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`.)
@ -22,6 +22,13 @@ It has the following features:
- Receivers can now be listed on <a href="http://sdr.hu/">sdr.hu</a>.
- License for OpenWebRX is now Affero GPL v3.
**News (2015-09-01)**
- The DDC in *csdr* has been hand-optimized for ARM NEON, so it runs 3× faster on the Raspberry Pi than before.
- Also we use *ncat* instead of *rtl_mus*, and it is also 3× faster.
- OpenWebRX now supports URLs like: http://localhost:8073/#freq=145555000,mod=usb
- When upgrading OpenWebRX, please make sure that you upgrade *csdr*, and install the new (optional) dependency *ncat*!
## Setup
OpenWebRX currently requires Linux and python 2.7 to run.
@ -30,6 +37,7 @@ First you will need to install the dependencies:
- <a href="https://github.com/simonyiszk/csdr">libcsdr</a>
- <a href="http://sdr.osmocom.org/trac/wiki/rtl-sdr">rtl-sdr</a>
- ncat (on Debian/Ubuntu, it is in the *nmap* package). *(It is optional, but highly advised.)*
After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server:
@ -57,14 +65,10 @@ However, if you hold down the shift key, you can drag the center line (BFO) or t
## Configuration tips
If you want to run OpenWebRX on a remote server instead of localhost, do not forget to set *server_hostname* in `config_webrx.py`, or you may get a WebSocket error.
Now we have a %[Wiki](https://github.com/simonyiszk/openwebrx/wiki) with some how-tos. However, some quick tips:
If you want to run OpenWebRX on a remote server instead of localhost, do not forget to set *server_hostname* in `config_webrx.py`.
DSP CPU usage can be fine-tuned in `plugins/dsp/csdr/plugin.py`: you can set transition bandwidths higher (thus degrade filter performance by decreasing the length of the kernel, but also decrease CPU usage), and also set `fft_size` lower.
If you constantly get *audio overrun* errors, you may change `audio_buffer_maximal_length_sec` in `openwebrx.js` from the default 1.7 to 3.
If you want a chat-box to the top of the page, <a href="https://gist.github.com/ha7ilm/15c4c5e4c80cef9b3144">here is a snippet</a> for you to include in `config_webrx.py`.
## Todo
Currently, clients use up a lot of bandwidth. This will be improved later.

View File

@ -66,6 +66,7 @@ sdrhu_public_listing = False
dsp_plugin="csdr"
fft_fps=9
fft_size=4096
#samp_rate = 2048000
samp_rate = 250000
center_freq = 145525000
@ -103,7 +104,13 @@ format_conversion="csdr convert_u8_f"
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
# (if ncat is not available on your system, rtl_mus will be used, thus you will have to set the same port as "my_listening_port" in config_rtl.py as well)

View File

@ -23,11 +23,13 @@
<head>
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
<script type="text/javascript">
//Local variables
client_id="%[CLIENT_ID]";
ws_url="%[WS_URL]";
rx_photo_height=%[RX_PHOTO_HEIGHT];
//Global variables
var client_id="%[CLIENT_ID]";
var ws_url="%[WS_URL]";
var rx_photo_height=%[RX_PHOTO_HEIGHT];
var audio_buffering_fill_to=%[AUDIO_BUFSIZE];
var starting_mod = "%[START_MOD]";
var starting_offset_frequency = %[START_OFFSET_FREQ];
</script>
<script src="sdr.js"></script>
<script src="openwebrx.js"></script>

View File

@ -959,6 +959,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.<br />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;
@ -974,6 +1000,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")
{
@ -1010,6 +1037,7 @@ function on_ws_recv(evt)
{
case "setup":
waterfall_init();
audio_preinit();
break;
case "bandwidth":
bandwidth=parseInt(param[1]);
@ -1122,7 +1150,7 @@ var audio_initialized=0;
var audio_received = Array();
var audio_buffer_index = 0;
var audio_resampler=new sdrjs.RationalResamplerFF(4,1);
var audio_resampler;
var audio_codec=new sdrjs.ImaAdpcm();
var audio_compression="unknown";
var audio_node;
@ -1167,7 +1195,7 @@ function audio_prepare(data)
audio_prepared_buffers.push(audio_rebuffer.take());
audio_buffer_current_count_debug++;
}
if(audio_buffering && audio_prepared_buffers.length>audio_buffering_fill_to) audio_buffering=false;
if(audio_buffering && audio_prepared_buffers.length>audio_buffering_fill_to) { console.log("buffers now: "+audio_prepared_buffers.length.toString()); audio_buffering=false; }
}
@ -1246,6 +1274,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()
{
@ -1255,8 +1285,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;
@ -1345,13 +1375,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;
@ -1362,6 +1405,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);
@ -1379,7 +1444,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)

View File

@ -47,7 +47,6 @@ import ctypes
#import rtl_mus
import rxws
import uuid
import config_webrx as cfg
import signal
import socket
@ -75,11 +74,17 @@ 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
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
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 +92,10 @@ def main():
print "Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>"
print
no_arguments=len(sys.argv)==1
if no_arguments: print "[openwebrx] Configuration script not specified. I will use: \"config_webrx.py\""
cfg=__import__("config_webrx" if no_arguments else sys.argv[1])
#Set signal handler
signal.signal(signal.SIGINT, handle_signal) #http://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python
@ -108,13 +117,18 @@ def main():
#Start rtl thread
if cfg.start_rtl_thread:
rtl_thread=threading.Thread(target = lambda:subprocess.Popen(cfg.start_rtl_command, shell=True), args=())
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
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_cmd = python_command+" rtl_mus.py config_rtl"
if os.system("ncat --version > /dev/null") != 32512:
print "[openwebrx-main] ncat detected, using it instead of rtl_mus:"
rtl_mus_cmd = "ncat localhost 8888 | ncat -4l %d -k --send-only --allow 127.0.0.1 " % cfg.iq_server_port
print rtl_mus_cmd
rtl_mus_thread=threading.Thread(target = lambda:subprocess.Popen(rtl_mus_cmd, 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."
time.sleep(1) #wait until it really starts
@ -208,9 +222,19 @@ def mutex_watchdog_thread_function():
clients_mutex.release()
time.sleep(0.5)
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 spectrum_thread_function():
global clients
dsp=getattr(plugins.dsp,cfg.dsp_plugin).plugin.dsp_plugin()
global clients, spectrum_dsp
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)
@ -220,6 +244,7 @@ def spectrum_thread_function():
sleep_sec=0.87/cfg.fft_fps
print "[openwebrx-spectrum] Spectrum thread initialized successfully."
dsp.start()
dsp.read(8) #dummy read to skip bufsize & preamble
print "[openwebrx-spectrum] Spectrum thread started."
bytes_to_read=int(dsp.get_fft_bytes_to_read())
while True:
@ -255,16 +280,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()
@ -363,13 +389,12 @@ 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
myclient.dsp=dsp
while True:
@ -378,8 +403,9 @@ class WebRXHandler(BaseHTTPRequestHandler):
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 +443,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:
@ -464,6 +498,13 @@ class WebRXHandler(BaseHTTPRequestHandler):
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:
self.send_response(500)
self.send_header('Content-type','text/html')
self.end_headers()
self.wfile.write("<html><body><h1>OpenWebRX Internal Server Error</h1>Please check the server log for details.</body></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 +533,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:

View File

@ -44,13 +44,15 @@ class dsp_plugin:
self.demodulator = "nfm"
self.name = "csdr"
self.format_conversion = "csdr convert_u8_f"
self.base_bufsize = 512
self.nc_port = 4951
try:
subprocess.Popen("nc",stdout=subprocess.PIPE,stderr=subprocess.PIPE).kill()
except:
print "[openwebrx-plugin:csdr] error: netcat not found, please install netcat!"
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="nc -v localhost {nc_port} | csdr setbuf {start_bufsize} | csdr through | "+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 +63,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 +94,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 +159,16 @@ 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()
my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="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 +191,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):