diff --git a/config_webrx.py b/config_webrx.py index 34e480c..0613278 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -36,7 +36,6 @@ config_webrx: configuration options for OpenWebRX # ==== Server settings ==== web_port=8073 -server_hostname="localhost" # If this contains an incorrect value, the web UI may freeze on load (it can't open websocket) max_clients=20 # ==== Web GUI configuration ==== @@ -65,25 +64,24 @@ Website: http://localhost sdrhu_key = "" # 3. Set this setting to True to enable listing: sdrhu_public_listing = False +server_hostname="localhost" # ==== DSP/RX settings ==== fft_fps=9 fft_size=4096 #Should be power of 2 fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. -# samp_rate = 250000 -samp_rate = 2400000 -center_freq = 144250000 -rf_gain = 5 #in dB. For an RTL-SDR, rf_gain=0 will set the tuner to auto gain mode, else it will be in manual gain mode. -ppm = 0 - audio_compression="adpcm" #valid values: "adpcm", "none" fft_compression="adpcm" #valid values: "adpcm", "none" digimodes_enable=True #Decoding digimodes come with higher CPU usage. digimodes_fft_size=1024 -start_rtl_thread=True +# 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: @@ -101,81 +99,107 @@ 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 # ################################################################################################# -# You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... Some examples of configuration are available here (default is RTL-SDR): +# Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf", "airspy" -# >> RTL-SDR via rtl_sdr -start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) -format_conversion="csdr convert_u8_f" - -#lna_gain=8 -#rf_amp=1 -#start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm, rf_amp=rf_amp, lna_gain=lna_gain) -#format_conversion="csdr convert_s8_f" -""" -To use a HackRF, compile the HackRF host tools from its "stdout" branch: - 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 -""" - -# >> 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 -".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)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate) -# format_conversion="csdr convert_u8_f" - -# >> Pre-recorded raw I/Q file as signal source -# You will have to correctly specify: samp_rate, center_freq, format_conversion in order to correctly play an I/Q file. -#start_rtl_command="(while true; do cat my_iq_file.raw; done) | csdr flowcontrol {sr} 20 ".format(sr=samp_rate*2*1.05) -#format_conversion="csdr convert_u8_f" - -#>> The rx_sdr command works with a variety of SDR harware: RTL-SDR, HackRF, SDRplay, UHD, Airspy, Red Pitaya, audio devices, etc. -# It will auto-detect your SDR hardware if the following tools are installed: -# * the vendor provided driver and library, -# * the vendor-specific SoapySDR wrapper library, -# * and SoapySDR itself. -# Check out this article on the OpenWebRX Wiki: https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX/ -#start_rtl_command="rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) -#format_conversion="" - -# >> gr-osmosdr signal source using GNU Radio (follow this guide: https://github.com/simonyiszk/openwebrx/wiki/Using-GrOsmoSDR-as-signal-source) -#start_rtl_command="cat /tmp/osmocom_fifo" -#format_conversion="" +sdrs = { + "rtlsdr": { + "name": "RTL-SDR USB Stick", + "type": "rtl_sdr", + "ppm": 0, + # you can change this if you use an upconverter. formula is: + # shown_center_freq = center_freq + lfo_offset + # "lfo_offset": 0, + "profiles": { + "70cm": { + "name": "70cm Relais", + "center_freq": 438800000, + "rf_gain": 30, + "samp_rate": 2400000, + "start_freq": 439275000, + "start_mod": "nfm" + }, + "2m": { + "name": "2m komplett", + "center_freq": 145000000, + "rf_gain": 30, + "samp_rate": 2400000, + "start_freq": 145725000, + "start_mod": "nfm" + } + } + }, + "sdrplay": { + "name": "SDRPlay RSP2", + "type": "sdrplay", + "ppm": 0, + "profiles": { + "20m": { + "name":"20m", + "center_freq": 14150000, + "rf_gain": 4, + "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, + "samp_rate": 500000, + "start_freq": 7070000, + "start_mod": "usb", + "antenna": "Antenna A" + }, + "80m": { + "name":"80m", + "center_freq": 3650000, + "rf_gain": 4, + "samp_rate": 500000, + "start_freq": 3570000, + "start_mod": "usb", + "antenna": "Antenna A" + }, + "49m": { + "name": "49m Broadcast", + "center_freq": 6000000, + "rf_gain": 4, + "samp_rate": 500000, + "start_freq": 6070000, + "start_mod": "am", + "antenna": "Antenna A" + } + } + }, + # this one is just here to test feature detection + "test": { + "type": "test" + } +} # ==== Misc settings ==== -shown_center_freq = center_freq #you can change this if you use an upconverter - 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" +iq_port_range = [4950, 4960] #TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default. # ==== Color themes ==== #A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels ### 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) @@ -197,7 +221,7 @@ waterfall_auto_level_margin = (5, 40) # 3D view settings mathbox_waterfall_frequency_resolution = 128 #bins mathbox_waterfall_history_length = 10 #seconds -mathbox_waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" +mathbox_waterfall_colors = [0x000000ff,0x2e6893ff,0x69a5d0ff,0x214b69ff,0x9dc4e0ff,0xfff775ff,0xff8a8aff,0xb20000ff] # === Experimental settings === #Warning! The settings below are very experimental. @@ -206,11 +230,3 @@ csdr_print_bufsizes = False # This prints the buffer sizes used for csdr proces csdr_through = False # Setting this True will print out how much data is going into the DSP chains. nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux. - -#Look up external IP address automatically from icanhazip.com, and use it as [server_hostname] -""" -print "[openwebrx-config] Detecting external IP address..." -import urllib2 -server_hostname=urllib2.urlopen("http://icanhazip.com").read()[:-1] -print "[openwebrx-config] External IP address detected:", server_hostname -""" diff --git a/csdr.py b/csdr.py index c35cc3e..0ef0250 100755 --- a/csdr.py +++ b/csdr.py @@ -23,13 +23,22 @@ OpenWebRX csdr plugin: do the signal processing with csdr import subprocess import time import os -import code import signal -import fcntl +import threading +from functools import partial -class dsp: +import logging +logger = logging.getLogger(__name__) - def __init__(self): +class output(object): + def add_output(self, type, read_fn): + pass + def reset(self): + pass + +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 @@ -45,7 +54,6 @@ class dsp: self.fft_compression = "none" self.demodulator = "nfm" self.name = "csdr" - self.format_conversion = "csdr convert_u8_f" self.base_bufsize = 512 self.nc_port = 4951 self.csdr_dynamic_bufsize = False @@ -59,62 +67,72 @@ class dsp: 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 + self.modification_lock = threading.Lock() + 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 | " - any_chain_base+=self.format_conversion+(" | " if self.format_conversion!="" else "") ##"csdr flowcontrol {flowcontrol} auto 1.5 10 | " + 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} | " + chain += "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | " + chain += "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every} | " if self.secondary_demodulator: - chain_begin+="csdr tee {iqtee_pipe} | " - chain_begin+="csdr tee {iqtee2_pipe} | " - chain_end = "" + 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 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 | dc_block | " + chain += last_decimation_block + # 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 -1 | CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f | " + max_gain = 5 + # digiham modes + else: + chain += "rrc_filter | gfsk_demodulator | " + if which == "dmr": + chain += "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | " + elif which == "ysf": + chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | " + max_gain = 0.0005 + chain += "digitalvoice_filter -f | " + chain += "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain} | ".format(max_gain=max_gain) + chain += "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + elif which == "am": + 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 | " + chain += last_decimation_block + chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" + if self.audio_compression=="adpcm": - chain_end = " | csdr encode_ima_adpcm_i16_u8" - if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr convert_f_s16"+chain_end - if which in [ "dstar", "nxdn" ]: - c = chain_begin - c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" - if which == "dstar": - c += " | dsd -fd" - elif which == "nxdn": - c += " | dsd -fi" - c += " -i - -o - -u 2 -g 10" - c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --input-buffer 160 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 220" - c += chain_end - return c - elif which == "dmr": - c = chain_begin - c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" - c += " | rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer" - c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256" - c += chain_end - return c - elif which == "ysf": - c = chain_begin - c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16" - c += " | rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y" - c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256" - c += chain_end - return c - elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end - elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end + chain += " | csdr encode_ima_adpcm_i16_u8" + return chain def secondary_chain(self, which): secondary_chain_base="cat {input_pipe} | " @@ -122,14 +140,17 @@ class dsp: return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "") elif which == "bpsk31": return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \ - "csdr bandpass_fir_fft_cc $(csdr '=-(31.25)/{if_samp_rate}') $(csdr '=(31.25)/{if_samp_rate}') $(csdr '=31.25/{if_samp_rate}') | " + \ + "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " + \ "csdr simple_agc_cc 0.001 0.5 | " + \ "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" 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 @@ -139,12 +160,12 @@ class dsp: def secondary_bpf_cutoff(self): if self.secondary_demodulator == "bpsk31": - return (31.25/2) / self.if_samp_rate() + return 31.25 / self.if_samp_rate() return 0 def secondary_bpf_transition_bw(self): if self.secondary_demodulator == "bpsk31": - return (31.25/2) / self.if_samp_rate() + return 31.25 / self.if_samp_rate() return 0 def secondary_samples_per_bits(self): @@ -157,51 +178,46 @@ class dsp: return 31.25 def start_secondary_demodulator(self): - if(not self.secondary_demodulator): return - print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate() + if not self.secondary_demodulator: return + logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate()) secondary_command_fft=self.secondary_chain("fft") secondary_command_demod=self.secondary_chain(self.secondary_demodulator) self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft) - secondary_command_fft=secondary_command_fft.format( \ - input_pipe=self.iqtee_pipe, \ - secondary_fft_input_size=self.secondary_fft_size, \ - secondary_fft_size=self.secondary_fft_size, \ - secondary_fft_block_size=self.secondary_fft_block_size(), \ + secondary_command_fft=secondary_command_fft.format( + input_pipe=self.iqtee_pipe, + secondary_fft_input_size=self.secondary_fft_size, + secondary_fft_size=self.secondary_fft_size, + secondary_fft_block_size=self.secondary_fft_block_size(), ) - secondary_command_demod=secondary_command_demod.format( \ - input_pipe=self.iqtee2_pipe, \ - secondary_shift_pipe=self.secondary_shift_pipe, \ - secondary_decimation=self.secondary_decimation(), \ - secondary_samples_per_bits=self.secondary_samples_per_bits(), \ - secondary_bpf_cutoff=self.secondary_bpf_cutoff(), \ - secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), \ + secondary_command_demod=secondary_command_demod.format( + input_pipe=self.iqtee2_pipe, + secondary_shift_pipe=self.secondary_shift_pipe, + secondary_decimation=self.secondary_decimation(), + secondary_samples_per_bits=self.secondary_samples_per_bits(), + secondary_bpf_cutoff=self.secondary_bpf_cutoff(), + secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), if_samp_rate=self.if_samp_rate() ) - print "[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft - print "[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod - #code.interact(local=locals()) + logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft) + logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (demod) = %s", secondary_command_demod) my_env=os.environ.copy() #if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) - print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)" + logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)") self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes - print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)" #TODO digimodes + logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") #TODO digimodes self.secondary_processes_running = True - #open control pipes for csdr and send initialization data - # print "==========> 1" - if self.secondary_shift_pipe != None: #TODO digimodes - # print "==========> 2", self.secondary_shift_pipe - self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes - # print "==========> 3" - self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes - # print "==========> 4" + self.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)) - self.set_pipe_nonblocking(self.secondary_process_demod.stdout) - self.set_pipe_nonblocking(self.secondary_process_fft.stdout) + #open control pipes for csdr and send initialization data + if self.secondary_shift_pipe != None: #TODO digimodes + self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes + self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes def set_secondary_offset_freq(self, value): self.secondary_offset_freq=value @@ -212,16 +228,20 @@ class dsp: def stop_secondary_demodulator(self): if self.secondary_processes_running == False: return self.try_delete_pipes(self.secondary_pipe_names) - if self.secondary_process_fft: os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM) - if self.secondary_process_demod: os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM) + if self.secondary_process_fft: + try: + os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM) + except ProcessLookupError: + # been killed by something else, ignore + pass + if self.secondary_process_demod: + try: + os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM) + except ProcessLookupError: + # been killed by something else, ignore + 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 @@ -244,12 +264,20 @@ class dsp: if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2) def set_samp_rate(self,samp_rate): - #to change this, restart is required self.samp_rate=samp_rate - self.decimation=1 - while self.samp_rate/(self.decimation+1)>=self.output_rate: - self.decimation+=1 - self.last_decimation=float(self.if_samp_rate())/self.output_rate + self.calculate_decimation() + if self.running: self.restart() + + def calculate_decimation(self): + (self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate()) + + def get_decimation(self, input_rate, output_rate): + decimation=1 + while input_rate / (decimation+1) >= output_rate: + decimation += 1 + 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 @@ -260,66 +288,86 @@ class dsp: 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.set_samp_rate(self.samp_rate) #as it depends on output_rate + self.calculate_decimation() def set_demodulator(self,demodulator): - #to change this, restart is required + if (self.demodulator == demodulator): return self.demodulator=demodulator + self.calculate_decimation() + self.restart() def get_demodulator(self): return self.demodulator def set_fft_size(self,fft_size): - #to change this, restart is required self.fft_size=fft_size + self.restart() def set_fft_fps(self,fft_fps): - #to change this, restart is required self.fft_fps=fft_fps + self.restart() def set_fft_averages(self,fft_averages): - #to change this, restart is required self.fft_averages=fft_averages + self.restart() def fft_block_size(self): if self.fft_averages == 0: return self.samp_rate/self.fft_fps else: return self.samp_rate/self.fft_fps/self.fft_averages - def set_format_conversion(self,format_conversion): - self.format_conversion=format_conversion - def set_offset_freq(self,offset_freq): self.offset_freq=offset_freq if self.running: + self.modification_lock.acquire() self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate)) self.shift_pipe_file.flush() + self.modification_lock.release() def set_bpf(self,low_cut,high_cut): self.low_cut=low_cut self.high_cut=high_cut if self.running: + self.modification_lock.acquire() self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) ) self.bpf_pipe_file.flush() + self.modification_lock.release() def get_bpf(self): return [self.low_cut, self.high_cut] def set_squelch_level(self, squelch_level): self.squelch_level=squelch_level + #no squelch required on digital voice modes + actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level if self.running: - self.squelch_pipe_file.write( "%g\n"%(float(self.squelch_level)) ) + self.modification_lock.acquire() + self.squelch_pipe_file.write("%g\n"%(float(actual_squelch))) self.squelch_pipe_file.flush() + self.modification_lock.release() - def get_smeter_level(self): - if self.running: - line=self.smeter_pipe_file.readline() - return float(line[:-1]) + def set_unvoiced_quality(self, q): + self.unvoiced_quality = q + self.restart() - def get_metadata(self): - if self.running and self.meta_pipe: - return self.meta_pipe_file.readline() + 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: @@ -332,9 +380,7 @@ class dsp: return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate)) def try_create_pipes(self, pipe_names, command_base): - # print "try_create_pipes" for pipe_name in pipe_names: - # print "\t"+pipe_name if "{"+pipe_name+"}" in command_base: setattr(self, pipe_name, self.pipe_base_path+pipe_name) self.mkfifo(getattr(self, pipe_name)) @@ -346,122 +392,107 @@ 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 - - def set_pipe_nonblocking(self, pipe): - flags = fcntl.fcntl(pipe, fcntl.F_GETFL) - fcntl.fcntl(pipe, fcntl.F_SETFL, flags | os.O_NONBLOCK) + except Exception: + logger.exception("try_delete_pipes()") def start(self): + self.modification_lock.acquire() + if (self.running): + self.modification_lock.release() + return + self.running = True + command_base=self.chain(self.demodulator) #create control pipes for csdr self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self)) - # self.bpf_pipe = self.shift_pipe = self.squelch_pipe = self.smeter_pipe = None self.try_create_pipes(self.pipe_names, command_base) - # if "{bpf_pipe}" in command_base: - # self.bpf_pipe=pipe_base_path+"bpf" - # self.mkfifo(self.bpf_pipe) - # if "{shift_pipe}" in command_base: - # self.shift_pipe=pipe_base_path+"shift" - # self.mkfifo(self.shift_pipe) - # if "{squelch_pipe}" in command_base: - # self.squelch_pipe=pipe_base_path+"squelch" - # self.mkfifo(self.squelch_pipe) - # if "{smeter_pipe}" in command_base: - # self.smeter_pipe=pipe_base_path+"smeter" - # self.mkfifo(self.smeter_pipe) - # if "{iqtee_pipe}" in command_base: - # self.iqtee_pipe=pipe_base_path+"iqtee" - # self.mkfifo(self.iqtee_pipe) - # if "{iqtee2_pipe}" in command_base: - # self.iqtee2_pipe=pipe_base_path+"iqtee2" - # self.mkfifo(self.iqtee2_pipe) - #run the command - command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, \ - last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, \ - bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), \ - flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, \ - squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe ) + command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, + last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, + bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), + flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, + squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, + output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000), + unvoiced_quality = self.get_unvoiced_quality(), dmr_control_pipe = self.dmr_control_pipe) - print "[openwebrx-dsp-plugin:csdr] Command =",command - #code.interact(local=locals()) + logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) my_env=os.environ.copy() if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) - #set stdout to non-blocking to avoid blocking the main loop when no audio was decoded in digital modes - self.set_pipe_nonblocking(self.process.stdout) + def watch_thread(): + rc = self.process.wait() + logger.debug("dsp thread ended with rc=%d", rc) + if (rc == 0 and self.running and not self.modification_lock.locked()): + logger.debug("restarting since rc = 0, self.running = true, and no modification") + self.restart() - self.running = True + threading.Thread(target = watch_thread).start() - #open control pipes for csdr and send initialization data - if self.bpf_pipe != None: - self.bpf_pipe_file=open(self.bpf_pipe,"w") - self.set_bpf(self.low_cut,self.high_cut) - if self.shift_pipe != None: - self.shift_pipe_file=open(self.shift_pipe,"w") - self.set_offset_freq(self.offset_freq) - if self.squelch_pipe != None: - self.squelch_pipe_file=open(self.squelch_pipe,"w") - self.set_squelch_level(self.squelch_level) - if self.smeter_pipe != None: - self.smeter_pipe_file=open(self.smeter_pipe,"r") - self.set_pipe_nonblocking(self.smeter_pipe_file) - if self.meta_pipe != None: - self.meta_pipe_file=open(self.meta_pipe,"r") - self.set_pipe_nonblocking(self.meta_pipe_file) + 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: + self.bpf_pipe_file = open(self.bpf_pipe, "w") + if self.shift_pipe: + self.shift_pipe_file = open(self.shift_pipe, "w") + if self.squelch_pipe: + self.squelch_pipe_file = open(self.squelch_pipe, "w") self.start_secondary_demodulator() - def read(self,size): - return self.process.stdout.read(size) + self.modification_lock.release() - def read_async(self, size): - try: - return self.process.stdout.read(size) - except IOError: - return None + # send initial config through the pipes + if self.squelch_pipe: + self.set_squelch_level(self.squelch_level) + if self.shift_pipe: + self.set_offset_freq(self.offset_freq) + if self.bpf_pipe: + self.set_bpf(self.low_cut, self.high_cut) + if self.smeter_pipe: + self.smeter_pipe_file=open(self.smeter_pipe,"r") + def read_smeter(): + raw = self.smeter_pipe_file.readline() + if len(raw) == 0: + return None + else: + return float(raw.rstrip("\n")) + self.output.add_output("smeter", read_smeter) + if self.meta_pipe != None: + # TODO make digiham output unicode and then change this here + self.meta_pipe_file=open(self.meta_pipe, "r", encoding="cp437") + def read_meta(): + raw = self.meta_pipe_file.readline() + if len(raw) == 0: + return None + else: + return raw.rstrip("\n") + self.output.add_output("meta", read_meta) + + if self.dmr_control_pipe: + self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w") def stop(self): - os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + self.modification_lock.acquire() + self.running = False + if hasattr(self, "process"): + try: + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + except ProcessLookupError: + # been killed by something else, ignore + pass self.stop_secondary_demodulator() - #if(self.process.poll()!=None):return # returns None while subprocess is running - #while(self.process.poll()==None): - # #self.process.kill() - # print "killproc",os.getpgid(self.process.pid),self.process.pid - # os.killpg(self.process.pid, signal.SIGTERM) - # - # time.sleep(0.1) self.try_delete_pipes(self.pipe_names) - # if self.bpf_pipe: - # try: os.unlink(self.bpf_pipe) - # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe - # if self.shift_pipe: - # try: os.unlink(self.shift_pipe) - # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.shift_pipe - # if self.squelch_pipe: - # try: os.unlink(self.squelch_pipe) - # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.squelch_pipe - # if self.smeter_pipe: - # try: os.unlink(self.smeter_pipe) - # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.smeter_pipe - # if self.iqtee_pipe: - # try: os.unlink(self.iqtee_pipe) - # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee_pipe - # if self.iqtee2_pipe: - # try: os.unlink(self.iqtee2_pipe) - # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee2_pipe - - self.running = False + self.modification_lock.release() def restart(self): + if not self.running: return self.stop() self.start() 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/gfx/openwebrx-avatar.png b/htdocs/gfx/openwebrx-avatar.png index 7e9736f..2ae083f 100644 Binary files a/htdocs/gfx/openwebrx-avatar.png and b/htdocs/gfx/openwebrx-avatar.png differ diff --git a/htdocs/gfx/openwebrx-directcall.png b/htdocs/gfx/openwebrx-directcall.png new file mode 100644 index 0000000..2d74713 Binary files /dev/null and b/htdocs/gfx/openwebrx-directcall.png differ diff --git a/htdocs/gfx/openwebrx-groupcall.png b/htdocs/gfx/openwebrx-groupcall.png new file mode 100644 index 0000000..5d61a4c Binary files /dev/null and b/htdocs/gfx/openwebrx-groupcall.png differ diff --git a/htdocs/gfx/openwebrx-mute.png b/htdocs/gfx/openwebrx-mute.png new file mode 100644 index 0000000..23da7bb Binary files /dev/null and b/htdocs/gfx/openwebrx-mute.png differ diff --git a/htdocs/index.wrx b/htdocs/index.html similarity index 62% rename from htdocs/index.wrx rename to htdocs/index.html index 2fcf3be..90ac89c 100644 --- a/htdocs/index.wrx +++ b/htdocs/index.html @@ -22,59 +22,44 @@ OpenWebRX | Open Source SDR Web App for Everyone! - - - - - - - - + + + + + + +
- -
%[RX_PHOTO_TITLE]
-
%[RX_PHOTO_DESC]
-
-
-
- - - - -
%[RX_TITLE]
-
%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, [maps]
-
- - + +
+ + +
+ +
+
+
+
+
+
+ + +
+
+
    +

  • Status
  • +

  • Log
  • +

  • Receiver
  • +
+
-
-
    -

  • Status
  • -

  • Log
  • -

  • Receiver
  • -
-
+
+
@@ -90,6 +75,10 @@
---.--- MHz
---.--- MHz
+
+ +
FM
@@ -102,12 +91,16 @@
CW
@@ -118,23 +111,23 @@
-
+
-
+
-
+
-
+
-
-
-
-
-
+
+
+
+
+
0 dB
@@ -160,7 +153,7 @@
Server CPU [0%]
Clients [1]
-
+
Under construction
We're working on the code right now, so the application might fail.
@@ -175,14 +168,41 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Timeslot 1
+
+
+
+
+
+
+
Timeslot 2
+
+
+
+
+
+
- +

Start OpenWebRX
diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 250c54a..5624d79 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -155,22 +155,12 @@ input[type=range]:focus::-ms-fill-upper .webrx-top-bar-parts { - position: absolute; - top: 0px; - left: 0px; - width:100%; height:67px; } -#webrx-top-bar-background -{ - background-color: #808080; - opacity: 0.15; - filter:alpha(opacity=15); -} - #webrx-top-bar { + background: rgba(128, 128, 128, 0.15); margin:0; padding:0; user-select: none; @@ -179,20 +169,23 @@ input[type=range]:focus::-ms-fill-upper -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; + overflow: hidden; + position: absolute; + left: 0; + top: 0; + right: 0; } #webrx-top-logo { - position: absolute; - top: 12px; - left: 15px; + padding: 12px; + float: left; } #webrx-ha5kfu-top-logo { - position: absolute; - top: 15px; - right: 15px; + float: right; + padding: 15px; } #webrx-top-photo @@ -204,79 +197,37 @@ input[type=range]:focus::-ms-fill-upper #webrx-rx-avatar-background { cursor:pointer; - position: absolute; - left: 285px; - top: 6px; + background-image: url(gfx/openwebrx-avatar-background.png); + background-origin: content-box; + background-repeat: no-repeat; + float: left; + width: 54px; + height: 54px; + padding: 7px; } #webrx-rx-avatar { cursor:pointer; - position: absolute; - left: 289px; - top: 10px; width: 46px; height: 46px; + padding: 4px; } #webrx-top-photo-clip { + min-height: 67px; max-height: 350px; overflow: hidden; position: relative; } -/*#webrx-bottom-bar -{ - position: absolute; - bottom: 0px; - width: 100%; - height: 117px; - background-image:url(gfx/webrx-bottom-bar.png); -}*/ - #webrx-page-container { min-height:100%; position:relative; } -/*#webrx-photo-gradient-left -{ - position: absolute; - bottom: 0px; - left: 0px; - background-image:url(gfx/webrx-photo-gradient-corner.png); - width: 59px; - height: 92px; - -} - -#webrx-photo-gradient-middle -{ - position: absolute; - bottom: 0px; - left: 59px; - right: 59px; - height: 92px; - background-image:url(gfx/webrx-photo-gradient-middle.png); -} - -#webrx-photo-gradient-right -{ - position: absolute; - bottom: 0px; - right: 0px; - background-image:url(gfx/webrx-photo-gradient-corner.png); - width: 59px; - height: 92px; - -webkit-transform:scaleX(-1); - -moz-transform:scaleX(-1); - -ms-transform:scaleX(-1); - -o-transform:scaleX(-1); - transform:scaleX(-1); -}*/ - #webrx-rx-photo-title { position: absolute; @@ -303,10 +254,17 @@ input[type=range]:focus::-ms-fill-upper #webrx-rx-photo-desc a { - /*color: #007df1;*/ color: #5ca8ff; text-shadow: none; - /*text-shadow: 0px 0px 7px #fff;*/ +} + +#webrx-rx-texts { + float: left; + padding: 10px; +} + +#webrx-rx-texts div { + padding: 3px; } #webrx-rx-title @@ -314,9 +272,6 @@ input[type=range]:focus::-ms-fill-upper white-space:nowrap; overflow: hidden; cursor:pointer; - position: absolute; - left: 350px; - top: 13px; font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; color: #909090; font-size: 11pt; @@ -330,15 +285,11 @@ input[type=range]:focus::-ms-fill-upper cursor:pointer; font-size: 10pt; color: #909090; - position: absolute; - left: 350px; - top: 34px; } #webrx-rx-desc a { color: #909090; - /*text-decoration: none;*/ } #openwebrx-rx-details-arrow @@ -718,9 +669,7 @@ img.openwebrx-mirror-img #openwebrx-main-buttons { - position: absolute; - right: 133px; - top: 3px; + float: right; margin:0; color: white; text-shadow: 0px 0px 4px #000000; @@ -841,10 +790,7 @@ img.openwebrx-mirror-img transition: width 500ms, left 500ms; } -#openwebrx-secondary-demod-listbox -{ - width: 201px; - height: 27px; +.openwebrx-panel select { border-radius: 5px; background-color: #373737; color: White; @@ -856,16 +802,27 @@ img.openwebrx-mirror-img border-color: transparent; border-width: 0px; -moz-appearance: none; - padding-left:3px; } -#openwebrx-secondary-demod-listbox option -{ +.openwebrx-panel select option { border-width: 0px; background-color: #373737; color: White; } +#openwebrx-secondary-demod-listbox +{ + width: 201px; + height: 27px; + padding-left:3px; +} + +#openwebrx-sdr-profiles-listbox { + width: 100%; + font-size: 10pt; + height: 27px; +} + #openwebrx-cursor-blink { animation: cursor-blink 1s infinite; @@ -971,3 +928,88 @@ img.openwebrx-mirror-img border-color: Red; } +.openwebrx-meta-slot { + width: 145px; + height: 196px; + float: left; + margin-right: 10px; + + 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; +} + +.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; + cursor: pointer; + + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: rgba(0,0,0,.3); +} + +.openwebrx-meta-slot.active { + 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; +} + +.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"); +} + +.openwebrx-meta-slot.active .openwebrx-meta-user-image.group { + background-image: url("gfx/openwebrx-groupcall.png"); +} + +.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 8adda72..27cc8fc 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -52,6 +52,7 @@ var waterfall_setup_done=0; var waterfall_queue = []; var waterfall_timer; var secondary_fft_size; +var audio_allowed; /*function fade(something,from,to,time_ms,fps) { @@ -79,7 +80,9 @@ 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"); + rx_photo_height = clip.clientHeight + clip.style.maxHeight=rx_photo_height+"px"; window.setTimeout(function() { animate(e("webrx-rx-photo-title"),"opacity","",1,0,1,500,30); },1000); window.setTimeout(function() { animate(e("webrx-rx-photo-desc"),"opacity","",1,0,1,500,30); },1500); window.setTimeout(function() { close_rx_photo() },2500); @@ -133,14 +136,14 @@ function toggleMute() if (mute) { mute = false; e("openwebrx-mute-on").id="openwebrx-mute-off"; - e("openwebrx-mute-img").src="gfx/openwebrx-speaker.png"; + e("openwebrx-mute-img").src="static/gfx/openwebrx-speaker.png"; e("openwebrx-panel-volume").disabled=false; e("openwebrx-panel-volume").style.opacity=1.0; e("openwebrx-panel-volume").value = volumeBeforeMute; } else { mute = true; e("openwebrx-mute-off").id="openwebrx-mute-on"; - e("openwebrx-mute-img").src="gfx/openwebrx-speaker-muted.png"; + e("openwebrx-mute-img").src="static/gfx/openwebrx-speaker-muted.png"; e("openwebrx-panel-volume").disabled=true; e("openwebrx-panel-volume").style.opacity=0.5; volumeBeforeMute = e("openwebrx-panel-volume").value; @@ -160,7 +163,7 @@ function updateSquelch() { var sliderValue=parseInt(e("openwebrx-panel-squelch").value); var outputValue=(sliderValue==parseInt(e("openwebrx-panel-squelch").min))?0:getLinearSmeterValue(sliderValue); - ws.send("SET squelch_level="+outputValue.toString()); + ws.send(JSON.stringify({"type":"dspcontrol","params":{"squelch_level":outputValue}})); } function updateWaterfallColors(which) @@ -433,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") { @@ -470,9 +473,13 @@ function demodulator_default_analog(offset_frequency,subtype) this.doset=function(first_time) { //this function sends demodulator parameters to the server - ws.send("SET"+((first_time)?" mod="+this.server_mod:"")+ - " low_cut="+this.low_cut.toString()+" high_cut="+this.high_cut.toString()+ - " offset_freq="+this.offset_frequency.toString()); + params = { + "low_cut": this.low_cut, + "high_cut": this.high_cut, + "offset_freq": this.offset_frequency + }; + if (first_time) params.mod = this.server_mod; + ws.send(JSON.stringify({"type":"dspcontrol","params":params})); } this.doset(true); //we set parameters on object creation @@ -612,6 +619,8 @@ function demodulator_analog_replace(subtype, for_digital) } demodulator_add(new demodulator_default_analog(temp_offset,subtype)); demodulator_buttons_update(); + hide_digitalvoice_panels(); + toggle_panel("openwebrx-panel-metadata-" + subtype, true); } function demodulator_set_offset_frequency(which,to_what) @@ -1150,176 +1159,232 @@ 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 function on_ws_recv(evt) { - if(!(evt.data instanceof ArrayBuffer)) { divlog("on_ws_recv(): Not ArrayBuffer received...",1); return; } - // - debug_ws_data_received+=evt.data.byteLength/1000; - first4Chars=getFirstChars(evt.data,4); - 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 (typeof evt.data == 'string') { + // text messages + debug_ws_data_received += evt.data.length / 1000; - } - if(first3Chars=="AUD") - { - var audio_data; - if(audio_compression=="adpcm") audio_data=new Uint8Array(evt.data,4) - else audio_data=new Int16Array(evt.data,4); - audio_prepare(audio_data); - audio_buffer_current_size_debug+=audio_data.length; - audio_buffer_all_size_debug+=audio_data.length; - if(!(ios||is_chrome) && (audio_initialized==0 && audio_prepared_buffers.length>audio_buffering_fill_to)) audio_init() - } - else if(first3Chars=="FFT") - { - //alert("Yupee! Doing FFT"); - //if(first4Chars=="FFTS") console.log("FFTS"); - if(fft_compression=="none") waterfall_add_queue(new Float32Array(evt.data,4)); - else if(fft_compression="adpcm") - { - fft_codec.reset(); + 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(); + + window.starting_mod = config.start_mod + window.starting_offset_frequency = config.start_offset_frequency; + window.audio_buffering_fill_to = config.client_audio_buffer_size; + bandwidth = config.samp_rate; + center_freq = config.center_freq + config.lfo_offset; + fft_size = config.fft_size; + fft_fps = config.fft_fps; + audio_compression = config.audio_compression; + divlog( "Audio stream is "+ ((audio_compression=="adpcm")?"compressed":"uncompressed")+"." ) + fft_compression = config.fft_compression; + divlog( "FFT stream is "+ ((fft_compression=="adpcm")?"compressed":"uncompressed")+"." ) + max_clients_num = config.max_clients; + progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num*0.85); + 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; - 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;i85); - break; - case "clients": - var clients_num=parseInt(param[1]); - progressbar_set(e("openwebrx-bar-clients"),clients_num/max_clients_num,"Clients ["+param[1]+"]",clients_num>max_clients_num*0.85); - break; - case "max_clients": - max_clients_num=parseInt(param[1]); - break; - case "s": - smeter_level=parseFloat(param[1]); - setSmeterAbsoluteValue(smeter_level); - break; - } - } - /*} - catch(err) - { - divlog("Received invalid message over WebSocket."); - }*/ - } else if (first3Chars=='MET') - { - var stringData=arrayBufferToString(evt.data); - var metaPanels = Array.prototype.filter.call(document.getElementsByClassName('openwebrx-panel'), function(el) { - return el.dataset.panelName == 'metadata'; - }); + break; + case "receiver_details": + var r = json.value; + e('webrx-rx-title').innerHTML = r.receiver_name; + e('webrx-rx-desc').innerHTML = r.receiver_location + ' | Loc: ' + r.receiver_qra + ', ASL: ' + r.receiver_asl + ' m, [maps]'; + e('webrx-rx-photo-title').innerHTML = r.photo_title; + e('webrx-rx-photo-desc').innerHTML = r.photo_desc; + break; + case "smeter": + smeter_level = json.value; + setSmeterAbsoluteValue(smeter_level); + break; + case "cpuusage": + var server_cpu_usage = json.value; + progressbar_set(e("openwebrx-bar-server-cpu"),server_cpu_usage,"Server CPU [" + Math.round(server_cpu_usage * 100) + "%]",server_cpu_usage>85); + break; + case "clients": + client_num = json.value; + progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num*0.85); + break; + case "profiles": + var listbox = e("openwebrx-sdr-profiles-listbox"); + listbox.innerHTML = json.value.map(function(profile){ + return '"; + }).join(""); + break; + case "features": + for (var feature in json.value) { + $('[data-feature="' + feature + '"')[json.value[feature] ? "show" : "hide"](); + } + break; + case "metadata": + update_metadata(json.value); + break; + default: + console.warn('received message of unknown type: ' + json.type); + } + } catch (e) { + // don't lose exception + console.error(e) + } + } + } else if (evt.data instanceof ArrayBuffer) { + // binary messages + debug_ws_data_received += evt.data.byteLength / 1000; - var meta = {}; - stringData.substr(4).split(";").forEach(function(s) { - var item = s.split(":"); - meta[item[0]] = item[1]; - }); + type = new Uint8Array(evt.data, 0, 1)[0] + data = evt.data.slice(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; - }; + switch (type) { + case 1: + // FFT data + 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;iaudio_buffering_fill_to)) audio_init() + break; + case 3: + // secondary FFT + if (fft_compression == "none") { + secondary_demod_waterfall_add_queue(new Float32Array(data)); + } else if (fft_compression == "adpcm") { + fft_codec.reset(); - metaPanels.forEach(update); + 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" + 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"); + } + $(el).find(".openwebrx-ysf-mode").text(mode); + $(el).find(".openwebrx-ysf-source").html(source); + $(el).find(".openwebrx-ysf-up").text(up); + $(el).find(".openwebrx-ysf-down").text(down); + + break; + } else { + clear_metadata(); } } +function hide_digitalvoice_panels() { + $(".openwebrx-meta-panel").each(function(_, p){ + toggle_panel(p.id, false); + }); + 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) { problems_span=e("openwebrx-problems"); @@ -1630,7 +1695,9 @@ function audio_flush_notused() function webrx_set_param(what, value) { - ws.send("SET "+what+"="+value.toString()); + params = {}; + params[what] = value; + ws.send(JSON.stringify({"type":"dspcontrol","params":params})); } var starting_mute = false; @@ -1645,7 +1712,7 @@ function parsehash() if(harr[0]=="mute") toggleMute(); else if(harr[0]=="mod") starting_mod = harr[1]; else if(harr[0]=="sql") - { + { e("openwebrx-panel-squelch").value=harr[1]; updateSquelch(); } @@ -1680,16 +1747,18 @@ function audio_preinit() else if(audio_context.sampleRate>44100*4) audio_buffer_size = 4096 * 4; - audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED); - audio_last_output_buffer = new Float32Array(audio_buffer_size); + if (!audio_rebuffer) { + audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED); + audio_last_output_buffer = new Float32Array(audio_buffer_size); - //we send our setup packet - parsehash(); + //we send our setup packet + parsehash(); - 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 + audio_calculate_resampling(audio_context.sampleRate); + audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1); + } + ws.send(JSON.stringify({"type":"dspcontrol","action":"start","params":{"output_rate":audio_server_output_rate}})); } function audio_init() @@ -1751,7 +1820,10 @@ function on_ws_closed() audio_node.disconnect(); } catch (dont_care) {} - divlog("WebSocket has closed unexpectedly. Please reload the page.", 1); + audio_initialized = 0; + divlog("WebSocket has closed unexpectedly. Attempting to reconnect in 5 seconds...", 1); + + setTimeout(open_websocket, 5000); } function on_ws_error(event) @@ -1763,14 +1835,15 @@ 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 - //} + 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+client_id); + ws = new WebSocket(ws_url); ws.onopen = on_ws_opened; ws.onmessage = on_ws_recv; ws.onclose = on_ws_closed; @@ -2023,27 +2096,23 @@ function waterfall_add(data) waterfall_image.data[base+x*4+i] = ((color>>>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(); } @@ -2265,6 +2334,7 @@ function openwebrx_init() init_rx_photo(); open_websocket(); secondary_demod_init(); + digimodes_init(); place_panels(first_show_panel); window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); window.addEventListener("resize",openwebrx_resize); @@ -2272,7 +2342,26 @@ function openwebrx_init() //Synchronise volume with slider updateVolume(); - waterfallColorsDefault(); + +} + +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() @@ -2281,6 +2370,7 @@ function iosPlayButtonClick() audio_init(); e("openwebrx-big-grey").style.opacity=0; window.setTimeout(function(){ e("openwebrx-big-grey").style.display="none"; },1100); + audio_allowed = 1; } /* @@ -2361,6 +2451,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; @@ -2424,7 +2515,7 @@ function place_panels(function_apply) for(i=0;i= 0) { if(c.openwebrxHidden) { @@ -2491,29 +2582,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); } @@ -2616,19 +2705,19 @@ function secondary_demod_init() function secondary_demod_start(subtype) { secondary_demod_canvases_initialized = false; - ws.send("SET secondary_mod="+subtype); - secondary_demod = subtype; + ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":subtype}})); + secondary_demod = subtype; } function secondary_demod_set() { - ws.send("SET secondary_offset_freq="+secondary_demod_offset_freq.toString()); + ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_offset_freq":secondary_demod_offset_freq}})); } function secondary_demod_stop() { - ws.send("SET secondary_mod=off"); - secondary_demod = false; + ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":false}})); + secondary_demod = false; secondary_demod_waterfall_queue = []; } @@ -2755,7 +2844,7 @@ function secondary_demod_update_channel_freq_from_event(evt) { secondary_demod_waiting_for_set = true; window.setTimeout(()=>{ - ws.send("SET secondary_offset_freq="+Math.floor(secondary_demod_channel_freq)); + ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_offset_freq":Math.floor(secondary_demod_channel_freq)}})); //console.log("doneset:", secondary_demod_channel_freq); secondary_demod_waiting_for_set = false; }, 50); @@ -2815,3 +2904,8 @@ function secondary_demod_waterfall_set_zoom(low_cut, high_cut) secondary_demod_canvases.map((x)=>{$(x).css("left",secondary_demod_canvas_left+"px").css("width",secondary_demod_canvas_width+"px");}); secondary_demod_update_channel_freq_from_event(); } + +function sdr_profile_changed() { + value = $('#openwebrx-sdr-profiles-listbox').val(); + ws.send(JSON.stringify({ type:"selectprofile", params:{ profile:value }})); +} diff --git a/openwebrx.py b/openwebrx.py old mode 100755 new mode 100644 index ed5af8c..99b1419 --- a/openwebrx.py +++ b/openwebrx.py @@ -1,757 +1,52 @@ -#!/usr/bin/python2 -print "" # python2.7 is required to run OpenWebRX instead of python3. Please run me by: python2 openwebrx.py -""" +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, ClientRegistry +from socketserver import ThreadingMixIn +from owrx.sdrhu import SdrHuUpdater - This file is part of OpenWebRX, - an open-source SDR receiver software with a web UI. - Copyright (c) 2013-2015 by Andras Retzler +import logging +logging.basicConfig(level = logging.DEBUG, format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s") - 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 . - -""" -sw_version="v0.17" -#0.15 (added nmux) - -import os -import code -import importlib -import csdr -import thread -import time -import datetime -import subprocess -import os -from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -from SocketServer import ThreadingMixIn -import fcntl -import time -import md5 -import random -import threading -import sys -import traceback -from collections import namedtuple -import Queue -import ctypes - -#import rtl_mus -import rxws -import uuid -import signal -import socket - -try: import sdrhu -except: sdrhu=False -avatar_ctime="" - -#pypy compatibility -try: import dl -except: pass -try: import __pypy__ -except: pass -pypy="__pypy__" in globals() - -""" -def import_all_plugins(directory): - for subdir in os.listdir(directory): - if os.path.isdir(directory+subdir) and not subdir[0]=="_": - exact_path=directory+subdir+"/plugin.py" - if os.path.isfile(exact_path): - importname=(directory+subdir+"/plugin").replace("/",".") - print "[openwebrx-import] Found plugin:",importname - importlib.import_module(importname) -""" - -class MultiThreadHTTPServer(ThreadingMixIn, HTTPServer): +class ThreadedHttpServer(ThreadingMixIn, HTTPServer): pass -def handle_signal(sig, frame): - global spectrum_dsp - if sig == signal.SIGUSR1: - print "[openwebrx] Verbose status information on USR1 signal" - print - print "time.time() =", time.time() - print "clients_mutex.locked() =", clients_mutex.locked() - print "clients_mutex_locker =", clients_mutex_locker - if server_fail: print "server_fail = ", server_fail - print "spectrum_thread_watchdog_last_tick =", spectrum_thread_watchdog_last_tick - print - print "clients:",len(clients) - for client in clients: - print - for key in client._fields: - print "\t%s = %s"%(key,str(getattr(client,key))) - elif sig == signal.SIGUSR2: - code.interact(local=globals()) - else: - 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, 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 "_________________________________________________________________________________________________" - print - print "Author contact info: Andras Retzler, HA7ILM " - print + 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 +OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package +_________________________________________________________________________________________________ - #Open log files - logs = type("logs_class", (object,), {"access_log":open(cfg.access_log if cfg.access_log else "/dev/null","a"), "error_log":""})() +Author contact info: Andras Retzler, HA7ILM - #Set signal handler - signal.signal(signal.SIGINT, handle_signal) #http://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python - signal.signal(signal.SIGUSR1, handle_signal) - signal.signal(signal.SIGUSR2, handle_signal) + """) - #Pypy - if pypy: print "pypy detected (and now something completely different: c code is expected to run at a speed of 3*10^8 m/s?)" + pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") - #Change process name to "openwebrx" (to be seen in ps) - 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, "openwebrx", 0, 0, 0) - break - except: - pass - - #Start rtl thread - if os.system("csdr 2> /dev/null") == 32512: #check for csdr - print "[openwebrx-main] You need to install \"csdr\" to run OpenWebRX!\n" + featureDetector = FeatureDetector() + if not featureDetector.is_available("core"): + print("you are missing required dependencies to run openwebrx. " + "please check that the following core requirements are installed:") + print(", ".join(featureDetector.get_requirements("core"))) return - if os.system("nmux --help 2> /dev/null") == 32512: #check for nmux - print "[openwebrx-main] You need to install an up-to-date version of \"csdr\" that contains the \"nmux\" tool to run OpenWebRX! Please upgrade \"csdr\"!\n" - return - if cfg.start_rtl_thread: - nmux_bufcnt = nmux_bufsize = 0 - while nmux_bufsize < cfg.samp_rate/4: nmux_bufsize += 4096 - while nmux_bufsize * nmux_bufcnt < cfg.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) - cfg.start_rtl_command += "| nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, 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 - print "[openwebrx-main] Waiting for I/Q server to start..." - while True: - testsock=socket.socket() - try: testsock.connect(("127.0.0.1", cfg.iq_server_port)) - except: - time.sleep(0.1) - continue - testsock.close() - break - print "[openwebrx-main] I/Q server started." - #Initialize clients - clients=[] - clients_mutex=threading.Lock() - lock_try_time=0 + # Get error messages about unknown / unavailable features as soon as possible + SdrService.loadProps() - #Start watchdog thread - print "[openwebrx-main] Starting watchdog threads." - mutex_test_thread=threading.Thread(target = mutex_test_thread_function, args = ()) - mutex_test_thread.start() - mutex_watchdog_thread=threading.Thread(target = mutex_watchdog_thread_function, args = ()) - mutex_watchdog_thread.start() + 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() - #Start spectrum thread - 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 = ()) - bcastmsg_thread.start() - - #threading.Thread(target = measure_thread_function, args = ()).start() - - #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() - - #Start HTTP thread - httpd = MultiThreadHTTPServer(('', cfg.web_port), WebRXHandler) - print('[openwebrx-main] Starting HTTP server.') - access_log("Starting OpenWebRX...") - httpd.serve_forever() - - -# This is a debug function below: -measure_value=0 -def measure_thread_function(): - global measure_value - while True: - print "[openwebrx-measure] value is",measure_value - measure_value=0 - time.sleep(1) - -def bcastmsg_thread_function(): - global clients - while True: - time.sleep(3) - try: cpu_usage=get_cpu_usage() - except: cpu_usage=0 - cma("bcastmsg_thread") - for i in range(0,len(clients)): - clients[i].bcastmsg="MSG cpu_usage={0} clients={1}".format(int(cpu_usage*100),len(clients)) - cmr() - -def mutex_test_thread_function(): - global clients_mutex, lock_try_time - while True: - time.sleep(0.5) - lock_try_time=time.time() - clients_mutex.acquire() - clients_mutex.release() - lock_try_time=0 - -def cma(what): #clients_mutex acquire - global clients_mutex - global clients_mutex_locker - if not clients_mutex.locked(): clients_mutex_locker = what - clients_mutex.acquire() - -def cmr(): - global clients_mutex - global clients_mutex_locker - clients_mutex_locker = None - clients_mutex.release() - -def mutex_watchdog_thread_function(): - global lock_try_time - global clients_mutex_locker - global clients_mutex - while True: - if lock_try_time != 0 and time.time()-lock_try_time > 3.0: - #if 3 seconds pass without unlock - 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, spectrum_dsp, spectrum_thread_watchdog_last_tick - spectrum_dsp=dsp=csdr.dsp() - 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_averages(int(round(1.0 * cfg.samp_rate / cfg.fft_size / cfg.fft_fps / (1.0 - cfg.fft_voverlap_factor))) if cfg.fft_voverlap_factor>0 else 0) - 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_async(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_async(bytes_to_read) - if data is None: - time.sleep(.01) - continue - #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)): - 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 - cmr() - -def get_client_by_id(client_id, use_mutex=True): - global clients - output=-1 - if use_mutex: cma("get_client_by_id") - for i in range(0,len(clients)): - if(clients[i].id==client_id): - output=i - break - if use_mutex: cmr() - if output==-1: - raise ClientNotFoundException - else: - return output - -def log_client(client, what): - print "[openwebrx-httpd] client {0}#{1} :: {2}".format(client.ip,client.id,what) - -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 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() - -def generate_client_id(ip): - #add a client - global clients - new_client=namedtuple("ClientStruct", "id gen_time ws_started sprectum_queue ip closed bcastmsg dsp loopstat") - new_client.id=md5.md5(str(random.random())).hexdigest() - new_client.gen_time=time.time() - new_client.ws_started=False # to check whether client has ever tried to open the websocket - new_client.spectrum_queue=Queue.Queue(1000) - new_client.ip=ip - new_client.bcastmsg="" - new_client.closed=[False] #byref, not exactly sure if required - new_client.dsp=None - cma("generate_client_id") - clients.append(new_client) - log_client(new_client,"client added. Clients now: {0}".format(len(clients))) - cmr() - cleanup_clients() - return new_client.id - -def close_client(i, use_mutex=True): - global clients - log_client(clients[i],"client being closed.") - if use_mutex: cma("close_client") +if __name__ == "__main__": try: - clients[i].dsp.stop() - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - 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() - -# http://www.codeproject.com/Articles/462525/Simple-HTTP-Server-and-Client-in-Python -# some ideas are used from the artice above - -class WebRXHandler(BaseHTTPRequestHandler): - def proc_read_thread(): - pass - - def send_302(self,what): - self.send_response(302) - self.send_header('Content-type','text/html') - self.send_header("Location", "http://{0}:{1}/{2}".format(cfg.server_hostname,cfg.web_port,what)) - self.end_headers() - self.wfile.write("

Object moved

Please click here to continue.".format(what)) - - - def do_GET(self): - self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - 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/": - print "[openwebrx-ws] Client requested WebSocket connection" - if receiver_failed: self.send_error(500,"Internal server error") - try: - # ========= WebSocket handshake ========= - ws_success=True - try: - rxws.handshake(self) - cma("do_GET /ws/") - client_i=get_client_by_id(self.path[4:], False) - myclient=clients[client_i] - except rxws.WebSocketException: ws_success=False - except ClientNotFoundException: ws_success=False - finally: - if clients_mutex.locked(): cmr() - if not ws_success: - self.send_error(400, 'Bad request.') - return - - # ========= Client handshake ========= - if myclient.ws_started: - print "[openwebrx-httpd] error: second WS connection with the same client id, throwing it." - self.send_error(400, 'Bad request.') #client already started - return - rxws.send(self, "CLIENT DE SERVER openwebrx.py") - client_ans=rxws.recv(self, True) - if client_ans[:16]!="SERVER DE CLIENT": - rxws.send("ERR Bad answer.") - return - myclient.ws_started=True - #send default parameters - rxws.send(self, "MSG center_freq={0} bandwidth={1} fft_size={2} fft_fps={3} audio_compression={4} fft_compression={5} max_clients={6} setup".format(str(cfg.shown_center_freq),str(cfg.samp_rate),cfg.fft_size,cfg.fft_fps,cfg.audio_compression,cfg.fft_compression,cfg.max_clients)) - - # ========= Initialize DSP ========= - dsp=csdr.dsp() - dsp_initialized=False - dsp.set_audio_compression(cfg.audio_compression) - dsp.set_fft_compression(cfg.fft_compression) #used by secondary chains - dsp.set_format_conversion(cfg.format_conversion) - dsp.set_offset_freq(0) - dsp.set_bpf(-4000,4000) - dsp.set_secondary_fft_size(cfg.digimodes_fft_size) - dsp.nc_port=cfg.iq_server_port - apply_csdr_cfg_to_dsp(dsp) - myclient.dsp=dsp - do_secondary_demod=False - access_log("Started streaming to client: "+self.client_address[0]+"#"+myclient.id+" (users now: "+str(len(clients))+")") - - while True: - myclient.loopstat=0 - if myclient.closed[0]: - print "[openwebrx-httpd:ws] client closed by other thread" - break - - # ========= send audio ========= - if dsp_initialized: - myclient.loopstat=10 - temp_audio_data=dsp.read_async(256) - if (temp_audio_data is not None): - myclient.loopstat=11 - rxws.send(self, temp_audio_data, "AUD ") - else: - #time.sleep((256.0 * 32) / 11025) - time.sleep(.01) - - # ========= send spectrum ========= - while not myclient.spectrum_queue.empty(): - myclient.loopstat=20 - spectrum_data=myclient.spectrum_queue.get() - #spectrum_data_mid=len(spectrum_data[0])/2 - #rxws.send(self, spectrum_data[0][spectrum_data_mid:]+spectrum_data[0][:spectrum_data_mid], "FFT ") - # (it seems GNU Radio exchanges the first and second part of the FFT output, we correct it) - myclient.loopstat=21 - rxws.send(self, spectrum_data[0],"FFT ") - - # ========= send smeter_level ========= - smeter_level=None - while True: - try: - myclient.loopstat=30 - smeter_level=dsp.get_smeter_level() - if smeter_level == None: break - except: - break - if smeter_level!=None: - myclient.loopstat=31 - rxws.send(self, "MSG s={0}".format(smeter_level)) - - # ========= send metadata ========= - metadata = None - while True: - try: - myclient.loopstat=35 - metadata = dsp.get_metadata(); - if metadata == None: break - rxws.send(self, "MET {0}".format(metadata.rstrip("\n"))) - except: - break - - # ========= send bcastmsg ========= - if myclient.bcastmsg!="": - myclient.loopstat=40 - rxws.send(self,myclient.bcastmsg) - myclient.bcastmsg="" - - # ========= send secondary ========= - if do_secondary_demod: - myclient.loopstat=41 - while True: - try: - secondary_spectrum_data=dsp.read_secondary_fft(dsp.get_secondary_fft_bytes_to_read()) - if len(secondary_spectrum_data) == 0: break - # print "len(secondary_spectrum_data)", len(secondary_spectrum_data) #TODO digimodes - rxws.send(self, secondary_spectrum_data, "FFTS") - except: break - myclient.loopstat=42 - while True: - try: - myclient.loopstat=422 - secondary_demod_data=dsp.read_secondary_demod(1) - myclient.loopstat=423 - if len(secondary_demod_data) == 0: break - # print "len(secondary_demod_data)", len(secondary_demod_data), secondary_demod_data #TODO digimodes - rxws.send(self, secondary_demod_data, "DAT ") - except: break - - # ========= process commands ========= - while True: - myclient.loopstat=50 - rdata=rxws.recv(self, False) - myclient.loopstat=51 - #try: - if not rdata: break - elif rdata[:3]=="SET": - print "[openwebrx-httpd:ws,%d] command: %s"%(client_i,rdata) - pairs=rdata[4:].split(" ") - bpf_set=False - new_bpf=dsp.get_bpf() - filter_limit=dsp.get_output_rate()/2 - for pair in pairs: - param_name, param_value = pair.split("=") - if param_name == "low_cut" and -filter_limit <= int(param_value) <= filter_limit: - bpf_set=True - new_bpf[0]=int(param_value) - elif param_name == "high_cut" and -filter_limit <= int(param_value) <= filter_limit: - bpf_set=True - new_bpf[1]=int(param_value) - elif param_name == "offset_freq" and -cfg.samp_rate/2 <= int(param_value) <= cfg.samp_rate/2: - myclient.loopstat=510 - dsp.set_offset_freq(int(param_value)) - elif param_name == "squelch_level" and float(param_value) >= 0: - myclient.loopstat=520 - dsp.set_squelch_level(float(param_value)) - elif param_name=="mod": - if (dsp.get_demodulator()!=param_value): - myclient.loopstat=530 - if dsp_initialized: dsp.stop() - dsp.set_demodulator(param_value) - if dsp_initialized: dsp.start() - elif param_name == "output_rate": - if not dsp_initialized: - myclient.loopstat=540 - dsp.set_output_rate(int(param_value)) - myclient.loopstat=541 - dsp.set_samp_rate(cfg.samp_rate) - elif param_name=="action" and param_value=="start": - if not dsp_initialized: - myclient.loopstat=550 - dsp.start() - dsp_initialized=True - elif param_name=="secondary_mod" and cfg.digimodes_enable: - if (dsp.get_secondary_demodulator() != param_value): - if dsp_initialized: dsp.stop() - if param_value == "off": - dsp.set_secondary_demodulator(None) - do_secondary_demod = False - else: - dsp.set_secondary_demodulator(param_value) - do_secondary_demod = True - rxws.send(self, "MSG secondary_fft_size={0} if_samp_rate={1} secondary_bw={2} secondary_setup".format(cfg.digimodes_fft_size, dsp.if_samp_rate(), dsp.secondary_bw())) - if dsp_initialized: dsp.start() - elif param_name=="secondary_offset_freq" and 0 <= int(param_value) <= dsp.if_samp_rate()/2 and cfg.digimodes_enable: - dsp.set_secondary_offset_freq(int(param_value)) - else: - print "[openwebrx-httpd:ws] invalid parameter" - if bpf_set: - myclient.loopstat=560 - dsp.set_bpf(*new_bpf) - #code.interact(local=locals()) - except: - myclient.loopstat=990 - exc_type, exc_value, exc_traceback = sys.exc_info() - print "[openwebrx-httpd:ws] exception: ",exc_type,exc_value - traceback.print_tb(exc_traceback) #TODO digimodes - #if exc_value[0]==32: #"broken pipe", client disconnected - # pass - #elif exc_value[0]==11: #"resource unavailable" on recv, client disconnected - # pass - #else: - # print "[openwebrx-httpd] error in /ws/ handler: ",exc_type,exc_value - # traceback.print_tb(exc_traceback) - - #stop dsp for the disconnected client - myclient.loopstat=991 - try: - dsp.stop() - del dsp - except: - print "[openwebrx-httpd] error in dsp.stop()" - - #delete disconnected client - myclient.loopstat=992 - try: - cma("do_GET /ws/ delete disconnected") - id_to_close=get_client_by_id(myclient.id,False) - close_client(id_to_close,False) - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - print "[openwebrx-httpd] client cannot be closed: ",exc_type,exc_value - traceback.print_tb(exc_traceback) - finally: - cmr() - myclient.loopstat=1000 - return - elif self.path in ("/status", "/status/"): - #self.send_header('Content-type','text/plain') - self.send_response(200) - self.end_headers() - 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="+("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 - anyStringsPresentInUserAgent=lambda a: reduce(lambda x,y:x or y, map(lambda b:self.headers['user-agent'].count(b), a), False) - if extension == "wrx" and ( (not anyStringsPresentInUserAgent(("Chrome","Firefox","Googlebot","iPhone","iPad","iPod"))) if 'user-agent' in self.headers.keys() else True ) and (not request_param.count("unsupported")): - self.send_302("upgrade.html") - return - if extension == "wrx": - cleanup_clients(False) - if cfg.max_clients<=len(clients): - self.send_302("retry.html") - return - self.send_response(200) - if(("wrx","html","htm").count(extension)): - self.send_header('Content-type','text/html') - elif(extension=="js"): - self.send_header('Content-type','text/javascript') - elif(extension=="css"): - self.send_header('Content-type','text/css') - self.end_headers() - if extension == "wrx": - replace_dictionary=( - ("%[RX_PHOTO_DESC]",cfg.photo_desc), - ("%[CLIENT_ID]", generate_client_id(self.client_address[0])) if "%[CLIENT_ID]" in data else "", - ("%[WS_URL]","ws://"+cfg.server_hostname+":"+str(cfg.web_port)+"/ws/"), - ("%[RX_TITLE]",cfg.receiver_name), - ("%[RX_LOC]",cfg.receiver_location), - ("%[RX_QRA]",cfg.receiver_qra), - ("%[RX_ASL]",str(cfg.receiver_asl)), - ("%[RX_GPS]",str(cfg.receiver_gps[0])+","+str(cfg.receiver_gps[1])), - ("%[RX_PHOTO_HEIGHT]",str(cfg.photo_height)),("%[RX_PHOTO_TITLE]",cfg.photo_title), - ("%[RX_ADMIN]",cfg.receiver_admin), - ("%[RX_ANT]",cfg.receiver_ant), - ("%[RX_DEVICE]",cfg.receiver_device), - ("%[AUDIO_BUFSIZE]",str(cfg.client_audio_buffer_size)), - ("%[START_OFFSET_FREQ]",str(cfg.start_freq-cfg.center_freq)), - ("%[START_MOD]",cfg.start_mod), - ("%[WATERFALL_COLORS]",cfg.waterfall_colors), - ("%[WATERFALL_MIN_LEVEL]",str(cfg.waterfall_min_level)), - ("%[WATERFALL_MAX_LEVEL]",str(cfg.waterfall_max_level)), - ("%[WATERFALL_AUTO_LEVEL_MARGIN]","[%d,%d]"%cfg.waterfall_auto_level_margin), - ("%[DIGIMODES_ENABLE]",("true" if cfg.digimodes_enable else "false")), - ("%[MATHBOX_WATERFALL_FRES]",str(cfg.mathbox_waterfall_frequency_resolution)), - ("%[MATHBOX_WATERFALL_THIST]",str(cfg.mathbox_waterfall_history_length)), - ("%[MATHBOX_WATERFALL_COLORS]",cfg.mathbox_waterfall_colors) - ) - for rule in replace_dictionary: - while data.find(rule[0])!=-1: - data=data.replace(rule[0],rule[1]) - self.wfile.write(data) - f.close() - return - except IOError: - self.send_error(404, 'Invalid path.') - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - print "[openwebrx-httpd] error (@outside):", exc_type, exc_value - traceback.print_tb(exc_traceback) - - -class ClientNotFoundException(Exception): - pass - -last_worktime=0 -last_idletime=0 - -def get_cpu_usage(): - global last_worktime, last_idletime - try: - f=open("/proc/stat","r") - except: - return 0 #Workaround, possibly we're on a Mac - line="" - while not "cpu " in line: line=f.readline() - f.close() - spl=line.split(" ") - worktime=int(spl[2])+int(spl[3])+int(spl[4]) - idletime=int(spl[5]) - dworktime=(worktime-last_worktime) - didletime=(idletime-last_idletime) - rate=float(dworktime)/(didletime+dworktime) - last_worktime=worktime - last_idletime=idletime - if(last_worktime==0): return 0 - return rate - - -if __name__=="__main__": - main() + main() + except KeyboardInterrupt: + for c in ClientRegistry.getSharedInstance().clients: + c.close() diff --git a/owrx/config.py b/owrx/config.py new file mode 100644 index 0000000..6db151e --- /dev/null +++ b/owrx/config.py @@ -0,0 +1,129 @@ +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.subscribers = [] + + def getValue(self): + return self.value + + def setValue(self, value): + if (self.value == value): + return self + self.value = value + for c in self.subscribers: + try: + c.call(self.value) + except Exception as e: + logger.exception(e) + return self + + def wire(self, callback): + 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): + sharedInstance = None + @staticmethod + def getSharedInstance(): + if PropertyManager.sharedInstance is None: + PropertyManager.sharedInstance = PropertyManager() + return PropertyManager.sharedInstance + + def collect(self, *props): + return PropertyManager({name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props}) + + def __init__(self, properties = None): + self.properties = {} + self.subscribers = [] + if properties is not None: + for (name, prop) in properties.items(): + self.add(name, prop) + + def add(self, name, prop): + self.properties[name] = prop + def fireCallbacks(value): + for c in self.subscribers: + try: + c.call(name, value) + except Exception as e: + logger.exception(e) + prop.wire(fireCallbacks) + return self + + def __contains__(self, name): + return self.hasProperty(name) + + def __getitem__(self, name): + return self.getPropertyValue(name) + + def __setitem__(self, name, value): + if not self.hasProperty(name): + 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 + + def getProperty(self, name): + if not self.hasProperty(name): + self.add(name, Property()) + return self.properties[name] + + def getPropertyValue(self, name): + return self.getProperty(name).getValue() + + def wire(self, callback): + sub = Subscription(self, callback) + self.subscribers.append(sub) + return sub + + def unwire(self, sub): + try: + self.subscribers.remove(sub) + except ValueError: + # happens when already removed before + pass + return self + + def defaults(self, other_pm): + for (key, p) in self.properties.items(): + if p.getValue() is None: + 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 diff --git a/owrx/connection.py b/owrx/connection.py new file mode 100644 index 0000000..67ee96b --- /dev/null +++ b/owrx/connection.py @@ -0,0 +1,191 @@ +from owrx.config import PropertyManager +from owrx.source import DspManager, CpuUsageThread, SdrService, ClientRegistry +from owrx.feature import FeatureDetector +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 + + self.dsp = None + self.sdr = None + self.configSub = None + + ClientRegistry.getSharedInstance().addClient(self) + + 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) + + features = FeatureDetector().feature_availability() + self.write_features(features) + + CpuUsageThread.getSharedInstance().add_client(self) + + def setSdr(self, id = None): + next = SdrService.getSource(id) + if (next == self.sdr): + return + + self.stopDsp() + + if self.configSub is not None: + self.configSub.cancel() + self.configSub = None + + self.sdr = next + + # send initial config + configProps = self.sdr.getProps().collect(*OpenWebRxClient.config_keys).defaults(PropertyManager.getSharedInstance()) + + 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) + + 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) + ClientRegistry.getSharedInstance().removeClient(self) + if self.configSub is not None: + self.configSub.cancel() + self.configSub = None + self.conn.close() + 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", "if_gain") \ + .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}) + 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): + 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 new file mode 100644 index 0000000..774ba9b --- /dev/null +++ b/owrx/controllers.py @@ -0,0 +1,87 @@ +import os +import mimetypes +from datetime import datetime +from owrx.websocket import WebSocketConnection +from owrx.config import PropertyManager +from owrx.source import ClientRegistry +from owrx.connection import WebSocketMessageHandler +from owrx.version import openwebrx_version + +import logging +logger = logging.getLogger(__name__) + +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", last_modified: datetime = None, max_age = None): + self.handler.send_response(code) + if content_type is not None: + self.handler.send_header("Content-Type", content_type) + if last_modified is not None: + self.handler.send_header("Last-Modified", last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT")) + if max_age is not None: + self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age)) + self.handler.end_headers() + if (type(content) == str): + content = content.encode() + self.handler.wfile.write(content) + def render_template(self, template, **variables): + f = open('htdocs/' + template) + data = f.read() + f.close() + + self.send_response(data) + +class StatusController(Controller): + def handle_request(self): + pm = PropertyManager.getSharedInstance() + # TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna + vars = { + "status": "active", + "name": pm["receiver_name"], + "op_email": pm["receiver_admin"], + "users": ClientRegistry.getSharedInstance().clientCount(), + "users_max": pm["max_clients"], + "gps": pm["receiver_gps"], + "asl": pm["receiver_asl"], + "loc": pm["receiver_location"], + "sw_version": openwebrx_version, + "avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png") + } + self.send_response("\n".join(["{key}={value}".format(key = key, value = value) for key, value in vars.items()])) + +class AssetsController(Controller): + def serve_file(self, file, content_type = None): + try: + modified = datetime.fromtimestamp(os.path.getmtime('htdocs/' + file)) + + if "If-Modified-Since" in self.handler.headers: + client_modified = datetime.strptime(self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z") + if modified <= client_modified: + self.send_response("", code = 304) + return + + f = open('htdocs/' + file, 'rb') + data = f.read() + f.close() + + if content_type is None: + (content_type, encoding) = mimetypes.MimeTypes().guess_type(file) + self.send_response(data, content_type = content_type, last_modified = modified, max_age = 3600) + except FileNotFoundError: + self.send_response("file not found", code = 404) + def handle_request(self): + filename = self.matches.group(1) + self.serve_file(filename) + +class IndexController(AssetsController): + def handle_request(self): + self.serve_file("index.html", content_type = "text/html") + +class WebSocketController(Controller): + def handle_request(self): + conn = WebSocketConnection(self.handler, WebSocketMessageHandler()) + conn.send("CLIENT DE SERVER openwebrx.py") + # enter read loop + conn.read_loop() diff --git a/owrx/feature.py b/owrx/feature.py new file mode 100644 index 0000000..38b8e27 --- /dev/null +++ b/owrx/feature.py @@ -0,0 +1,122 @@ +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__) + + +class UnknownFeatureException(Exception): + pass + +class FeatureDetector(object): + features = { + "core": [ "csdr", "nmux", "nc" ], + "rtl_sdr": [ "rtl_sdr" ], + "sdrplay": [ "rx_tools" ], + "hackrf": [ "hackrf_transfer" ], + "airspy": [ "airspy_rx" ], + "digital_voice_digiham": [ "digiham", "sox" ], + "digital_voice_dsd": [ "dsd", "sox", "digiham" ] + } + + 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)) + + 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 command_is_runnable(self, command): + return os.system("{0} 2>/dev/null >/dev/null".format(command)) != 32512 + + def has_csdr(self): + return self.command_is_runnable("csdr") + + 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") + + def has_rx_tools(self): + return self.command_is_runnable("rx_sdr --help") + + """ + To use a HackRF, compile the HackRF host tools from its "stdout" branch: + git clone https://github.com/mossmann/hackrf/ + cd hackrf + git fetch + git checkout origin/stdout + cd host + mkdir build + cd build + cmake .. -DINSTALL_UDEV_RULES=ON + make + sudo make install + """ + def has_hackrf_transfer(self): + # TODO i don't have a hackrf, so somebody doublecheck this. + # TODO also check if it has the stdout feature + return self.command_is_runnable("hackrf_transfer --help") + + 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): + required_version = LooseVersion("0.2") + + digiham_version_regex = re.compile('^digiham version (.*)$') + def check_digiham_version(command): + try: + process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE) + version = LooseVersion(digiham_version_regex.match(process.stdout.readline().decode()).group(1)) + process.wait(1) + return version >= required_version + except FileNotFoundError: + return False + return reduce(and_, + map( + check_digiham_version, + ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator", + "digitalvoice_filter"] + ), + True) + + def has_dsd(self): + return self.command_is_runnable("dsd") + + def has_sox(self): + return self.command_is_runnable("sox") + + def has_airspy_rx(self): + return self.command_is_runnable("airspy_rx --help 2> /dev/null") diff --git a/owrx/http.py b/owrx/http.py new file mode 100644 index 0000000..ca7d357 --- /dev/null +++ b/owrx/http.py @@ -0,0 +1,42 @@ +from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController +from http.server import BaseHTTPRequestHandler +import re + +import logging +logger = logging.getLogger(__name__) + +class RequestHandler(BaseHTTPRequestHandler): + def __init__(self, request, client_address, server): + self.router = Router() + super().__init__(request, client_address, server) + def do_GET(self): + self.router.route(self) + +class Router(object): + mappings = [ + {"route": "/", "controller": IndexController}, + {"route": "/status", "controller": StatusController}, + {"regex": "/static/(.+)", "controller": AssetsController}, + {"route": "/ws/", "controller": WebSocketController}, + {"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: + 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) + if res is not None: + (controller, matches) = res + logger.debug("path: {0}, controller: {1}, matches: {2}".format(handler.path, controller, matches)) + controller(handler, matches).handle_request() + else: + handler.send_error(404, "Not Found", "The page you requested could not be found.") diff --git a/owrx/meta.py b/owrx/meta.py new file mode 100644 index 0000000..ec4966a --- /dev/null +++ b/owrx/meta.py @@ -0,0 +1,81 @@ +from owrx.config import PropertyManager +from urllib import request +import json +from datetime import datetime, timedelta +import logging +import threading + +logger = logging.getLogger(__name__) + +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.cacheTimeout = timedelta(seconds = 86400) + def isValid(self, key): + if not key in self.cache: return False + entry = self.cache[key] + return entry["timestamp"] + self.cacheTimeout > datetime.now() + def put(self, key, value): + self.cache[key] = { + "timestamp": datetime.now(), + "data": value + } + def get(self, key): + if not self.isValid(key): return None + return self.cache[key]["data"] + + +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=30).read() + data = json.loads(res.decode("utf-8")) + cache.put(id, data) + except json.JSONDecodeError: + cache.put(id, None) + del self.threads[id] + def enrich(self, meta): + if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: return None + if not "source" in meta: return None + id = meta["source"] + 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 = cache.get(id) + 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(";") + 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"] + 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) + diff --git a/owrx/sdrhu.py b/owrx/sdrhu.py new file mode 100644 index 0000000..5f0d7fb --- /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__(daemon = True) + + def update(self): + pm = PropertyManager.getSharedInstance() + cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}\" 2>&1".format(**pm.__dict__()) + 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/owrx/source.py b/owrx/source.py new file mode 100644 index 0000000..7d62388 --- /dev/null +++ b/owrx/source.py @@ -0,0 +1,552 @@ +import subprocess +from owrx.config import PropertyManager +from owrx.feature import FeatureDetector, UnknownFeatureException +from owrx.meta import MetaParser +import threading +import csdr +import time +import os +import signal +import sys +import socket +import logging + +logger = logging.getLogger(__name__) + +class SdrService(object): + sdrProps = None + sources = {} + lastPort = None + @staticmethod + def getNextPort(): + pm = PropertyManager.getSharedInstance() + (start, end) = pm["iq_port_range"] + if SdrService.lastPort is None: + SdrService.lastPort = start + else: + SdrService.lastPort += 1 + if SdrService.lastPort > end: + raise IndexError("no more available ports to start more sdrs") + return SdrService.lastPort + @staticmethod + def loadProps(): + if SdrService.sdrProps is None: + pm = PropertyManager.getSharedInstance() + featureDetector = FeatureDetector() + def loadIntoPropertyManager(dict: dict): + propertyManager = PropertyManager() + for (name, value) in dict.items(): + propertyManager[name] = value + return propertyManager + def sdrTypeAvailable(value): + try: + if not featureDetector.is_available(value["type"]): + logger.error("The RTL source type \"{0}\" is not available. please check requirements.".format(value["type"])) + return False + return True + except UnknownFeatureException: + logger.error("The RTL source type \"{0}\" is invalid. Please check your configuration".format(value["type"])) + return False + # transform all dictionary items into PropertyManager object, filtering out unavailable ones + SdrService.sdrProps = { + name: loadIntoPropertyManager(value) for (name, value) in pm["sdrs"].items() if sdrTypeAvailable(value) + } + logger.info("SDR sources loaded. Availables SDRs: {0}".format(", ".join(map(lambda x: x["name"], SdrService.sdrProps.values())))) + @staticmethod + def getSource(id = None): + SdrService.loadProps() + if id is None: + # TODO: configure default sdr in config? right now it will pick the first one off the list. + id = list(SdrService.sdrProps.keys())[0] + sources = SdrService.getSources() + return sources[id] + @staticmethod + def getSources(): + SdrService.loadProps() + for id in SdrService.sdrProps.keys(): + if not id in SdrService.sources: + props = SdrService.sdrProps[id] + className = ''.join(x for x in props["type"].title() if x.isalnum()) + "Source" + cls = getattr(sys.modules[__name__], className) + SdrService.sources[id] = cls(props, SdrService.getNextPort()) + return SdrService.sources + + +class SdrSource(object): + def __init__(self, props, port): + self.props = props + self.activateProfile() + self.rtlProps = self.props.collect( + "samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain" + ).defaults(PropertyManager.getSharedInstance()) + + def restart(name, value): + logger.debug("restarting sdr source due to property change: {0} changed to {1}".format(name, value)) + self.stop() + self.start() + self.rtlProps.wire(restart) + self.port = port + self.monitor = None + self.clients = [] + self.spectrumClients = [] + self.spectrumThread = None + self.process = None + self.modificationLock = threading.Lock() + + # 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"] + if id is None: + id = list(profiles.keys())[0] + logger.debug("activating profile {0}".format(id)) + profile = profiles[id] + for (key, value) in profile.items(): + # skip the name, that would overwrite the source name. + if key == "name": continue + self.props[key] = value + + def getProfiles(self): + return self.props["profiles"] + + def getName(self): + return self.props["name"] + + def getProps(self): + return self.props + + def getPort(self): + return self.port + + def start(self): + self.modificationLock.acquire() + if self.monitor: + self.modificationLock.release() + return + + props = self.rtlProps + + start_sdr_command = self.getCommand().format( + **props.collect("samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain").__dict__() + ) + + 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 + while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6: nmux_bufcnt += 1 + if nmux_bufcnt == 0 or nmux_bufsize == 0: + logger.error("Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py") + self.modificationLock.release() + return + logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)) + cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, self.port) + self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp) + logger.info("Started rtl source: " + cmd) + + 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: + testsock.connect(("127.0.0.1", self.getPort())) + testsock.close() + break + except: + time.sleep(0.1) + + self.modificationLock.release() + + for c in self.clients: + c.onSdrAvailable() + + def isAvailable(self): + return self.monitor is not None + + def stop(self): + for c in self.clients: + c.onSdrUnavailable() + + self.modificationLock.acquire() + + if self.process is not None: + try: + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + except ProcessLookupError: + # been killed by something else, ignore + pass + if self.monitor: + self.monitor.join() + self.sleepOnRestart() + self.modificationLock.release() + + def sleepOnRestart(self): + pass + + def addClient(self, c): + self.clients.append(c) + self.start() + def removeClient(self, c): + try: + self.clients.remove(c) + except ValueError: + pass + if not self.clients: + self.stop() + + 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: + c.write_spectrum_data(data) + + +class RtlSdrSource(SdrSource): + 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 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 getCommand(self): + command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}" + gainMap = { "rf_gain" : "RFGR", "if_gain" : "IFGR"} + gains = [ "{0}={{{1}}}".format(gainMap[name], name) for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items() if value is not None ] + if gains: + command += " -g {gains}".format(gains = ",".join(gains)) + if self.rtlProps["antenna"] is not None: + command += " -a \"{antenna}\"" + command += " -" + return command + + def sleepOnRestart(self): + time.sleep(1) + +class AirspySource(SdrSource): + def getCommand(self): + frequency = self.props['center_freq'] / 1e6 + command = "airspy_rx" + command += " -f{0}".format(frequency) + command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}" + return command + def getFormatConversion(self): + return "csdr convert_s16_f" + +class SpectrumThread(csdr.output): + def __init__(self, sdrSource): + self.sdrSource = sdrSource + super().__init__() + + self.props = props = self.sdrSource.props.collect( + "samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression", + "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through" + ).defaults(PropertyManager.getSharedInstance()) + + self.dsp = dsp = csdr.dsp(self) + dsp.nc_port = self.sdrSource.getPort() + dsp.set_demodulator("fft") + + def set_fft_averages(key, value): + samp_rate = props["samp_rate"] + fft_size = props["fft_size"] + fft_fps = props["fft_fps"] + fft_voverlap_factor = props["fft_voverlap_factor"] + + dsp.set_fft_averages(int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor))) if fft_voverlap_factor>0 else 0) + + 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"] + dsp.csdr_print_bufsizes = props["csdr_print_bufsizes"] + dsp.csdr_through = props["csdr_through"] + logger.debug("Spectrum thread initialized successfully.") + + def start(self): + self.sdrSource.addClient(self) + if self.sdrSource.isAvailable(): + self.dsp.start() + + 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: + data = read_fn() + if len(data) == 0: + run = False + else: + self.sdrSource.writeSpectrumData(data) + + threading.Thread(target = pipe).start() + + 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() + def onSdrUnavailable(self): + self.dsp.stop() + +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", + "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality", + "dmr_filter" + ).defaults(PropertyManager.getSharedInstance()) + + self.dsp = csdr.dsp(self) + self.dsp.nc_port = self.sdrSource.getPort() + + def set_low_cut(cut): + bpf = self.dsp.get_bpf() + bpf[0] = cut + self.dsp.set_bpf(*bpf) + + def set_high_cut(cut): + bpf = self.dsp.get_bpf() + bpf[1] = cut + self.dsp.set_bpf(*bpf) + + 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.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) + 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 + self.dsp.set_secondary_demodulator(mod) + if mod is not None: + self.handler.write_secondary_dsp_config({ + "secondary_fft_size":self.localProps["digimodes_fft_size"], + "if_samp_rate":self.dsp.if_samp_rate(), + "secondary_bw":self.dsp.secondary_bw() + }) + self.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) + + super().__init__() + + def start(self): + if self.sdrSource.isAvailable(): + self.dsp.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.metaParser.parse + } + write = writers[t] + + def pump(read, write): + def copy(): + run = True + while run: + data = read() + if data is None or (isinstance(data, bytes) and len(data) == 0): + logger.warning("zero read on {0}".format(t)) + run = False + else: + write(data) + return copy + + threading.Thread(target=pump(read_fn, write)).start() + + def stop(self): + self.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) + + def onSdrAvailable(self): + logger.debug("received onSdrAvailable, attempting DspSource restart") + self.dsp.start() + + def onSdrUnavailable(self): + logger.debug("received onSdrUnavailable, shutting down DspSource") + self.dsp.stop() + +class CpuUsageThread(threading.Thread): + sharedInstance = None + @staticmethod + def getSharedInstance(): + if CpuUsageThread.sharedInstance is None: + CpuUsageThread.sharedInstance = CpuUsageThread() + CpuUsageThread.sharedInstance.start() + return CpuUsageThread.sharedInstance + + def __init__(self): + self.clients = [] + self.doRun = True + self.last_worktime = 0 + self.last_idletime = 0 + super().__init__() + + def run(self): + while self.doRun: + try: + cpu_usage = self.get_cpu_usage() + except: + cpu_usage = 0 + for c in self.clients: + c.write_cpu_usage(cpu_usage) + time.sleep(3) + logger.debug("cpu usage thread shut down") + + def get_cpu_usage(self): + try: + f = open("/proc/stat","r") + except: + return 0 #Workaround, possibly we're on a Mac + line = "" + while not "cpu " in line: line=f.readline() + f.close() + spl = line.split(" ") + worktime = int(spl[2]) + int(spl[3]) + int(spl[4]) + idletime = int(spl[5]) + dworktime = (worktime - self.last_worktime) + didletime = (idletime - self.last_idletime) + rate = float(dworktime) / (didletime+dworktime) + self.last_worktime = worktime + self.last_idletime = idletime + if (self.last_worktime==0): return 0 + return rate + + def add_client(self, c): + self.clients.append(c) + + def remove_client(self, c): + try: + self.clients.remove(c) + except ValueError: + pass + if not self.clients: + self.shutdown() + + def shutdown(self): + CpuUsageThread.sharedInstance = None + self.doRun = False + +class TooManyClientsException(Exception): + pass + +class ClientRegistry(object): + sharedInstance = None + @staticmethod + def getSharedInstance(): + if ClientRegistry.sharedInstance is None: + ClientRegistry.sharedInstance = ClientRegistry() + return ClientRegistry.sharedInstance + + def __init__(self): + self.clients = [] + super().__init__() + + 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) + self.broadcast() + + def clientCount(self): + return len(self.clients) + + def removeClient(self, client): + try: + self.clients.remove(client) + except ValueError: + pass + self.broadcast() \ No newline at end of file diff --git a/owrx/version.py b/owrx/version.py new file mode 100644 index 0000000..7437eda --- /dev/null +++ b/owrx/version.py @@ -0,0 +1 @@ +openwebrx_version = "v0.18" \ No newline at end of file diff --git a/owrx/websocket.py b/owrx/websocket.py new file mode 100644 index 0000000..a247b2a --- /dev/null +++ b/owrx/websocket.py @@ -0,0 +1,97 @@ +import base64 +import hashlib +import json + +import logging +logger = logging.getLogger(__name__) + +class WebSocketConnection(object): + def __init__(self, handler, messageHandler): + self.handler = handler + self.messageHandler = messageHandler + my_headers = self.handler.headers.items() + my_header_keys = list(map(lambda x:x[0],my_headers)) + h_key_exists = lambda x:my_header_keys.count(x) + h_value = lambda x:my_headers[my_header_keys.index(x)][1] + if (not h_key_exists("Upgrade")) or not (h_value("Upgrade")=="websocket") or (not h_key_exists("Sec-WebSocket-Key")): + 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): + # allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway. + data = json.dumps(data, allow_nan = False) + + # string-type messages are sent as text frames + if (type(data) == str): + header = self.get_header(len(data), 1) + data_to_send = header + data.encode('utf-8') + # anything else as binary + else: + header = self.get_header(len(data), 2) + data_to_send = header + data + written = self.handler.wfile.write(data_to_send) + if (written != len(data_to_send)): + logger.error("incomplete write! closing socket!") + self.close() + else: + 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) + if (mask): + data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) + if (opcode == 1): + message = data.decode('utf-8') + self.messageHandler.handleTextMessage(self, message) + elif (opcode == 2): + self.messageHandler.handleBinaryMessage(self, data) + elif (opcode == 8): + open = False + self.messageHandler.handleClose(self) + else: + logger.warning("unsupported opcode: {0}".format(opcode)) + + def close(self): + try: + header = self.get_header(0, 8) + self.handler.wfile.write(header) + self.handler.wfile.flush() + except ValueError: + logger.exception("ValueError while writing close frame:") + except OSError: + logger.exception("OSError while writing close frame:") + + try: + self.handler.finish() + self.handler.connection.close() + except Exception: + logger.exception("while closing connection:") + + +class WebSocketException(Exception): + pass 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 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()