From e15dc1ce11c92aa45592e17096566867f33bc982 Mon Sep 17 00:00:00 2001 From: D0han Date: Sun, 21 Jul 2019 19:40:28 +0200 Subject: [PATCH] Reformatted with black -l 120 -t py35 . --- config_webrx.py | 124 ++++++++------- csdr.py | 366 ++++++++++++++++++++++++-------------------- openwebrx.py | 19 ++- owrx/bands.py | 8 +- owrx/config.py | 20 ++- owrx/connection.py | 86 +++++++---- owrx/controllers.py | 45 ++++-- owrx/feature.py | 50 +++--- owrx/http.py | 25 ++- owrx/map.py | 56 +++---- owrx/meta.py | 30 ++-- owrx/sdrhu.py | 15 +- owrx/source.py | 175 +++++++++++++++------ owrx/version.py | 2 +- owrx/websocket.py | 85 +++++----- owrx/wsjt.py | 34 ++-- sdrhu.py | 3 +- 17 files changed, 681 insertions(+), 462 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index eaba803..15569de 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -35,21 +35,21 @@ config_webrx: configuration options for OpenWebRX # https://github.com/simonyiszk/openwebrx/wiki # ==== Server settings ==== -web_port=8073 -max_clients=20 +web_port = 8073 +max_clients = 20 # ==== Web GUI configuration ==== -receiver_name="[Callsign]" -receiver_location="Budapest, Hungary" -receiver_qra="JN97ML" -receiver_asl=200 -receiver_ant="Longwire" -receiver_device="RTL-SDR" -receiver_admin="example@example.com" -receiver_gps=(47.000000,19.000000) -photo_height=350 -photo_title="Panorama of Budapest from Schönherz Zoltán Dormitory" -photo_desc=""" +receiver_name = "[Callsign]" +receiver_location = "Budapest, Hungary" +receiver_qra = "JN97ML" +receiver_asl = 200 +receiver_ant = "Longwire" +receiver_device = "RTL-SDR" +receiver_admin = "example@example.com" +receiver_gps = (47.000000, 19.000000) +photo_height = 350 +photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory" +photo_desc = """ You can add your own background photo and receiver information.
Receiver is operated by: %[RX_ADMIN]
Device: %[RX_DEVICE]
@@ -64,18 +64,20 @@ Website: http://localhost sdrhu_key = "" # 3. Set this setting to True to enable listing: sdrhu_public_listing = False -server_hostname="localhost" +server_hostname = "localhost" # ==== DSP/RX settings ==== -fft_fps=9 -fft_size=4096 #Should be power of 2 -fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. +fft_fps = 9 +fft_size = 4096 # Should be power of 2 +fft_voverlap_factor = ( + 0.3 +) # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. -audio_compression="adpcm" #valid values: "adpcm", "none" -fft_compression="adpcm" #valid values: "adpcm", "none" +audio_compression = "adpcm" # valid values: "adpcm", "none" +fft_compression = "adpcm" # valid values: "adpcm", "none" -digimodes_enable=True #Decoding digimodes come with higher CPU usage. -digimodes_fft_size=1024 +digimodes_enable = True # Decoding digimodes come with higher CPU usage. +digimodes_fft_size = 1024 # determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes # if you're running on a Raspi (up to 3B+) you'll want to leave this on 1 @@ -116,7 +118,7 @@ sdrs = { "rf_gain": 30, "samp_rate": 2400000, "start_freq": 439275000, - "start_mod": "nfm" + "start_mod": "nfm", }, "2m": { "name": "2m komplett", @@ -124,9 +126,9 @@ sdrs = { "rf_gain": 30, "samp_rate": 2400000, "start_freq": 145725000, - "start_mod": "nfm" - } - } + "start_mod": "nfm", + }, + }, }, "sdrplay": { "name": "SDRPlay RSP2", @@ -134,39 +136,39 @@ sdrs = { "ppm": 0, "profiles": { "20m": { - "name":"20m", + "name": "20m", "center_freq": 14150000, "rf_gain": 4, "samp_rate": 500000, "start_freq": 14070000, "start_mod": "usb", - "antenna": "Antenna A" + "antenna": "Antenna A", }, "30m": { - "name":"30m", + "name": "30m", "center_freq": 10125000, "rf_gain": 4, "samp_rate": 250000, "start_freq": 10142000, - "start_mod": "usb" + "start_mod": "usb", }, "40m": { - "name":"40m", + "name": "40m", "center_freq": 7100000, "rf_gain": 4, "samp_rate": 500000, "start_freq": 7070000, "start_mod": "usb", - "antenna": "Antenna A" + "antenna": "Antenna A", }, "80m": { - "name":"80m", + "name": "80m", "center_freq": 3650000, "rf_gain": 4, "samp_rate": 500000, "start_freq": 3570000, "start_mod": "usb", - "antenna": "Antenna A" + "antenna": "Antenna A", }, "49m": { "name": "49m Broadcast", @@ -175,42 +177,43 @@ sdrs = { "samp_rate": 500000, "start_freq": 6070000, "start_mod": "am", - "antenna": "Antenna A" - } - } + "antenna": "Antenna A", + }, + }, }, # this one is just here to test feature detection - "test": { - "type": "test" - } + "test": {"type": "test"}, } # ==== Misc settings ==== client_audio_buffer_size = 5 -#increasing client_audio_buffer_size will: +# increasing client_audio_buffer_size will: # - also increase the latency # - decrease the chance of audio underruns -iq_port_range = [4950, 4960] #TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default. +iq_port_range = [ + 4950, + 4960, +] # TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default. # ==== Color themes ==== -#A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels +# A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels ### default theme by teejez: -waterfall_colors = [0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff] -waterfall_min_level = -88 #in dB +waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF] +waterfall_min_level = -88 # in dB waterfall_max_level = -20 waterfall_auto_level_margin = (5, 40) ### old theme by HA7ILM: -#waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" -#waterfall_min_level = -115 #in dB -#waterfall_max_level = 0 -#waterfall_auto_level_margin = (20, 30) +# waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" +# waterfall_min_level = -115 #in dB +# waterfall_max_level = 0 +# waterfall_auto_level_margin = (20, 30) ##For the old colors, you might also want to set [fft_voverlap_factor] to 0. -#Note: When the auto waterfall level button is clicked, the following happens: +# Note: When the auto waterfall level button is clicked, the following happens: # [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]] # [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]] # @@ -219,17 +222,26 @@ waterfall_auto_level_margin = (5, 40) # current_max_power_level __| # 3D view settings -mathbox_waterfall_frequency_resolution = 128 #bins -mathbox_waterfall_history_length = 10 #seconds -mathbox_waterfall_colors = [0x000000ff,0x2e6893ff,0x69a5d0ff,0x214b69ff,0x9dc4e0ff,0xfff775ff,0xff8a8aff,0xb20000ff] +mathbox_waterfall_frequency_resolution = 128 # bins +mathbox_waterfall_history_length = 10 # seconds +mathbox_waterfall_colors = [ + 0x000000FF, + 0x2E6893FF, + 0x69A5D0FF, + 0x214B69FF, + 0x9DC4E0FF, + 0xFFF775FF, + 0xFF8A8AFF, + 0xB20000FF, +] # === Experimental settings === -#Warning! The settings below are very experimental. -csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr. +# Warning! The settings below are very experimental. +csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr. csdr_print_bufsizes = False # This prints the buffer sizes used for csdr processes. -csdr_through = False # Setting this True will print out how much data is going into the DSP chains. +csdr_through = False # Setting this True will print out how much data is going into the DSP chains. -nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux. +nmux_memory = 50 # in megabytes. This sets the approximate size of the circular buffer used by nmux. google_maps_api_key = "" diff --git a/csdr.py b/csdr.py index 8b8edef..54fd13c 100755 --- a/csdr.py +++ b/csdr.py @@ -28,26 +28,29 @@ from functools import partial from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper import logging + logger = logging.getLogger(__name__) + class output(object): def add_output(self, type, read_fn): pass + def reset(self): pass -class dsp(object): +class dsp(object): def __init__(self, output): self.samp_rate = 250000 - self.output_rate = 11025 #this is default, and cannot be set at the moment + self.output_rate = 11025 # this is default, and cannot be set at the moment self.fft_size = 1024 self.fft_fps = 5 self.offset_freq = 0 self.low_cut = -4000 self.high_cut = 4000 - self.bpf_transition_bw = 320 #Hz, and this is a constant - self.ddc_transition_bw_rate = 0.15 # of the IF sample rate + self.bpf_transition_bw = 320 # Hz, and this is a constant + self.ddc_transition_bw_rate = 0.15 # of the IF sample rate self.running = False self.secondary_processes_running = False self.audio_compression = "none" @@ -67,9 +70,17 @@ class dsp(object): self.secondary_fft_size = 1024 self.secondary_process_fft = None self.secondary_process_demod = None - self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", - "iqtee2_pipe", "dmr_control_pipe"] - self.secondary_pipe_names=["secondary_shift_pipe"] + self.pipe_names = [ + "bpf_pipe", + "shift_pipe", + "squelch_pipe", + "smeter_pipe", + "meta_pipe", + "iqtee_pipe", + "iqtee2_pipe", + "dmr_control_pipe", + ] + self.secondary_pipe_names = ["secondary_shift_pipe"] self.secondary_offset_freq = 1000 self.unvoiced_quality = 1 self.modification_lock = threading.Lock() @@ -79,15 +90,19 @@ class dsp(object): def set_temporary_directory(self, what): self.temporary_directory = what - def chain(self,which): + def chain(self, which): chain = ["nc -v 127.0.0.1 {nc_port}"] - if self.csdr_dynamic_bufsize: chain += ["csdr setbuf {start_bufsize}"] - if self.csdr_through: chain += ["csdr through"] + if self.csdr_dynamic_bufsize: + chain += ["csdr setbuf {start_bufsize}"] + if self.csdr_through: + chain += ["csdr through"] if which == "fft": chain += [ "csdr fft_cc {fft_size} {fft_block_size}", - "csdr logpower_cf -70" if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages}", - "csdr fft_exchange_sides_ff {fft_size}" + "csdr logpower_cf -70" + if self.fft_averages == 0 + else "csdr logaveragepower_cf -70 {fft_size} {fft_averages}", + "csdr fft_exchange_sides_ff {fft_size}", ] if self.fft_compression == "adpcm": chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"] @@ -96,37 +111,24 @@ class dsp(object): "csdr shift_addition_cc --fifo {shift_pipe}", "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING", "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING", - "csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}" + "csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}", ] if self.secondary_demodulator: - chain += [ - "csdr tee {iqtee_pipe}", - "csdr tee {iqtee2_pipe}" - ] + chain += ["csdr tee {iqtee_pipe}", "csdr tee {iqtee2_pipe}"] # safe some cpu cycles... no need to decimate if decimation factor is 1 - last_decimation_block = ["csdr fractional_decimator_ff {last_decimation}"] if self.last_decimation != 1.0 else [] + last_decimation_block = ( + ["csdr fractional_decimator_ff {last_decimation}"] if self.last_decimation != 1.0 else [] + ) if which == "nfm": - chain += [ - "csdr fmdemod_quadri_cf", - "csdr limit_ff" - ] + chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"] chain += last_decimation_block - chain += [ - "csdr deemphasis_nfm_ff {output_rate}", - "csdr convert_f_s16" - ] + chain += ["csdr deemphasis_nfm_ff {output_rate}", "csdr convert_f_s16"] elif self.isDigitalVoice(which): - chain += [ - "csdr fmdemod_quadri_cf", - "dc_block " - ] + chain += ["csdr fmdemod_quadri_cf", "dc_block "] chain += last_decimation_block # dsd modes - if which in [ "dstar", "nxdn" ]: - chain += [ - "csdr limit_ff", - "csdr convert_f_s16" - ] + if which in ["dstar", "nxdn"]: + chain += ["csdr limit_ff", "csdr convert_f_s16"] if which == "dstar": chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "] elif which == "nxdn": @@ -135,44 +137,28 @@ class dsp(object): max_gain = 5 # digiham modes else: - chain += [ - "rrc_filter", - "gfsk_demodulator" - ] + chain += ["rrc_filter", "gfsk_demodulator"] if which == "dmr": chain += [ "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe}", - "mbe_synthesizer -f -u {unvoiced_quality}" + "mbe_synthesizer -f -u {unvoiced_quality}", ] elif which == "ysf": - chain += [ - "ysf_decoder --fifo {meta_pipe}", - "mbe_synthesizer -y -f -u {unvoiced_quality}" - ] + chain += ["ysf_decoder --fifo {meta_pipe}", "mbe_synthesizer -y -f -u {unvoiced_quality}"] max_gain = 0.0005 chain += [ "digitalvoice_filter -f", "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain}".format(max_gain=max_gain), - "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " + "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ", ] elif which == "am": - chain += [ - "csdr amdemod_cf", - "csdr fastdcblock_ff" - ] + chain += ["csdr amdemod_cf", "csdr fastdcblock_ff"] chain += last_decimation_block - chain += [ - "csdr agc_ff", - "csdr limit_ff", - "csdr convert_f_s16" - ] + chain += ["csdr agc_ff", "csdr limit_ff", "csdr convert_f_s16"] elif which == "ssb": chain += ["csdr realpart_cf"] chain += last_decimation_block - chain += [ - "csdr agc_ff", - "csdr limit_ff" - ] + chain += ["csdr agc_ff", "csdr limit_ff"] # fixed sample rate necessary for the wsjt-x tools. fix with sox... if self.isWsjtMode() and self.get_audio_rate() != self.get_output_rate(): chain += [ @@ -181,24 +167,31 @@ class dsp(object): else: chain += ["csdr convert_f_s16"] - if self.audio_compression=="adpcm": + if self.audio_compression == "adpcm": chain += ["csdr encode_ima_adpcm_i16_u8"] return chain def secondary_chain(self, which): - secondary_chain_base="cat {input_pipe} | " + secondary_chain_base = "cat {input_pipe} | " if which == "fft": - return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "") + return ( + secondary_chain_base + + "csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression == "adpcm" else "") + ) elif which == "bpsk31": - return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \ - "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " + \ - "csdr simple_agc_cc 0.001 0.5 | " + \ - "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ - "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ - "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" + return ( + secondary_chain_base + + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + + "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " + + "csdr simple_agc_cc 0.001 0.5 | " + + "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + + "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + + "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" + ) elif self.isWsjtMode(which): chain = secondary_chain_base + "csdr realpart_cf | " - if self.last_decimation != 1.0 : + if self.last_decimation != 1.0: chain += "csdr fractional_decimator_ff {last_decimation} | " chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" return chain @@ -211,14 +204,16 @@ class dsp(object): self.restart() def secondary_fft_block_size(self): - return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here + return (self.samp_rate / self.decimation) / ( + self.fft_fps * 2 + ) # *2 is there because we do FFT on real signal here def secondary_decimation(self): - return 1 #currently unused + return 1 # currently unused def secondary_bpf_cutoff(self): if self.secondary_demodulator == "bpsk31": - return 31.25 / self.if_samp_rate() + return 31.25 / self.if_samp_rate() return 0 def secondary_bpf_transition_bw(self): @@ -228,7 +223,7 @@ class dsp(object): def secondary_samples_per_bits(self): if self.secondary_demodulator == "bpsk31": - return int(round(self.if_samp_rate()/31.25))&~3 + return int(round(self.if_samp_rate() / 31.25)) & ~3 return 0 def secondary_bw(self): @@ -236,19 +231,20 @@ class dsp(object): return 31.25 def start_secondary_demodulator(self): - if not self.secondary_demodulator: return - logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate()) - secondary_command_fft=self.secondary_chain("fft") - secondary_command_demod=self.secondary_chain(self.secondary_demodulator) + if not self.secondary_demodulator: + return + logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate()) + secondary_command_fft = self.secondary_chain("fft") + secondary_command_demod = self.secondary_chain(self.secondary_demodulator) self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft) - secondary_command_fft=secondary_command_fft.format( + secondary_command_fft = secondary_command_fft.format( input_pipe=self.iqtee_pipe, secondary_fft_input_size=self.secondary_fft_size, secondary_fft_size=self.secondary_fft_size, secondary_fft_block_size=self.secondary_fft_block_size(), - ) - secondary_command_demod=secondary_command_demod.format( + ) + secondary_command_demod = secondary_command_demod.format( input_pipe=self.iqtee2_pipe, secondary_shift_pipe=self.secondary_shift_pipe, secondary_decimation=self.secondary_decimation(), @@ -256,21 +252,29 @@ class dsp(object): secondary_bpf_cutoff=self.secondary_bpf_cutoff(), secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), if_samp_rate=self.if_samp_rate(), - last_decimation=self.last_decimation - ) + last_decimation=self.last_decimation, + ) logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft) logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (demod) = %s", secondary_command_demod) - my_env=os.environ.copy() - #if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; - if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; - self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) + my_env = os.environ.copy() + # if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; + if self.csdr_print_bufsizes: + my_env["CSDR_PRINT_BUFSIZES"] = "1" + self.secondary_process_fft = subprocess.Popen( + secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env + ) logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)") - self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes - logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") #TODO digimodes + self.secondary_process_demod = subprocess.Popen( + secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env + ) # TODO digimodes + logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") # TODO digimodes self.secondary_processes_running = True - self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read()))) + self.output.add_output( + "secondary_fft", + partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())), + ) if self.isWsjtMode(): smd = self.get_secondary_demodulator() if smd == "ft8": @@ -288,19 +292,20 @@ class dsp(object): else: self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) - #open control pipes for csdr and send initialization data - if self.secondary_shift_pipe != None: #TODO digimodes - self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes - self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes + # open control pipes for csdr and send initialization data + if self.secondary_shift_pipe != None: # TODO digimodes + self.secondary_shift_pipe_file = open(self.secondary_shift_pipe, "w") # TODO digimodes + self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes def set_secondary_offset_freq(self, value): - self.secondary_offset_freq=value + self.secondary_offset_freq = value if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"): - self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate())) + self.secondary_shift_pipe_file.write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())) self.secondary_shift_pipe_file.flush() def stop_secondary_demodulator(self): - if self.secondary_processes_running == False: return + if self.secondary_processes_running == False: + return self.try_delete_pipes(self.secondary_pipe_names) if self.secondary_process_fft: try: @@ -319,42 +324,47 @@ class dsp(object): def get_secondary_demodulator(self): return self.secondary_demodulator - def set_secondary_fft_size(self,secondary_fft_size): - #to change this, restart is required - self.secondary_fft_size=secondary_fft_size + def set_secondary_fft_size(self, secondary_fft_size): + # to change this, restart is required + self.secondary_fft_size = secondary_fft_size - def set_audio_compression(self,what): + def set_audio_compression(self, what): self.audio_compression = what - def set_fft_compression(self,what): + def set_fft_compression(self, what): self.fft_compression = what def get_fft_bytes_to_read(self): - if self.fft_compression=="none": return self.fft_size*4 - if self.fft_compression=="adpcm": return (self.fft_size/2)+(10/2) + if self.fft_compression == "none": + return self.fft_size * 4 + if self.fft_compression == "adpcm": + return (self.fft_size / 2) + (10 / 2) def get_secondary_fft_bytes_to_read(self): - if self.fft_compression=="none": return self.secondary_fft_size*4 - if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2) + if self.fft_compression == "none": + return self.secondary_fft_size * 4 + if self.fft_compression == "adpcm": + return (self.secondary_fft_size / 2) + (10 / 2) - def set_samp_rate(self,samp_rate): - self.samp_rate=samp_rate + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate self.calculate_decimation() - if self.running: self.restart() + if self.running: + self.restart() def calculate_decimation(self): (self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate()) def get_decimation(self, input_rate, output_rate): - decimation=1 - while input_rate / (decimation+1) >= output_rate: + decimation = 1 + while input_rate / (decimation + 1) >= output_rate: decimation += 1 fraction = float(input_rate / decimation) / output_rate intermediate_rate = input_rate / decimation return (decimation, fraction, intermediate_rate) def if_samp_rate(self): - return self.samp_rate/self.decimation + return self.samp_rate / self.decimation def get_name(self): return self.name @@ -369,59 +379,64 @@ class dsp(object): return 12000 return self.get_output_rate() - def isDigitalVoice(self, demodulator = None): + def isDigitalVoice(self, demodulator=None): if demodulator is None: demodulator = self.get_demodulator() return demodulator in ["dmr", "dstar", "nxdn", "ysf"] - def isWsjtMode(self, demodulator = None): + def isWsjtMode(self, demodulator=None): if demodulator is None: demodulator = self.get_secondary_demodulator() return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"] - def set_output_rate(self,output_rate): - self.output_rate=output_rate + def set_output_rate(self, output_rate): + self.output_rate = output_rate self.calculate_decimation() - def set_demodulator(self,demodulator): - if (self.demodulator == demodulator): return - self.demodulator=demodulator + def set_demodulator(self, demodulator): + if self.demodulator == demodulator: + return + self.demodulator = demodulator self.calculate_decimation() self.restart() def get_demodulator(self): return self.demodulator - def set_fft_size(self,fft_size): - self.fft_size=fft_size + def set_fft_size(self, fft_size): + self.fft_size = fft_size self.restart() - def set_fft_fps(self,fft_fps): - self.fft_fps=fft_fps + def set_fft_fps(self, fft_fps): + self.fft_fps = fft_fps self.restart() - def set_fft_averages(self,fft_averages): - self.fft_averages=fft_averages + def set_fft_averages(self, fft_averages): + self.fft_averages = fft_averages self.restart() def fft_block_size(self): - if self.fft_averages == 0: return self.samp_rate/self.fft_fps - else: return self.samp_rate/self.fft_fps/self.fft_averages + if self.fft_averages == 0: + return self.samp_rate / self.fft_fps + else: + return self.samp_rate / self.fft_fps / self.fft_averages - def set_offset_freq(self,offset_freq): - self.offset_freq=offset_freq + def set_offset_freq(self, offset_freq): + self.offset_freq = offset_freq if self.running: self.modification_lock.acquire() - self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate)) + self.shift_pipe_file.write("%g\n" % (-float(self.offset_freq) / self.samp_rate)) self.shift_pipe_file.flush() self.modification_lock.release() - def set_bpf(self,low_cut,high_cut): - self.low_cut=low_cut - self.high_cut=high_cut + def set_bpf(self, low_cut, high_cut): + self.low_cut = low_cut + self.high_cut = high_cut if self.running: self.modification_lock.acquire() - self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) ) + self.bpf_pipe_file.write( + "%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate()) + ) self.bpf_pipe_file.flush() self.modification_lock.release() @@ -429,12 +444,12 @@ class dsp(object): return [self.low_cut, self.high_cut] def set_squelch_level(self, squelch_level): - self.squelch_level=squelch_level - #no squelch required on digital voice modes + self.squelch_level = squelch_level + # no squelch required on digital voice modes actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level if self.running: self.modification_lock.acquire() - self.squelch_pipe_file.write("%g\n"%(float(actual_squelch))) + self.squelch_pipe_file.write("%g\n" % (float(actual_squelch))) self.squelch_pipe_file.flush() self.modification_lock.release() @@ -450,7 +465,7 @@ class dsp(object): self.dmr_control_pipe_file.write("{0}\n".format(filter)) self.dmr_control_pipe_file.flush() - def mkfifo(self,path): + def mkfifo(self, path): try: os.unlink(path) except: @@ -458,27 +473,28 @@ class dsp(object): os.mkfifo(path) def ddc_transition_bw(self): - return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate)) + return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate)) def try_create_pipes(self, pipe_names, command_base): for pipe_name in pipe_names: - if "{"+pipe_name+"}" in command_base: - setattr(self, pipe_name, self.pipe_base_path+pipe_name) + if "{" + pipe_name + "}" in command_base: + setattr(self, pipe_name, self.pipe_base_path + pipe_name) self.mkfifo(getattr(self, pipe_name)) else: setattr(self, pipe_name, None) def try_delete_pipes(self, pipe_names): for pipe_name in pipe_names: - pipe_path = getattr(self,pipe_name,None) + pipe_path = getattr(self, pipe_name, None) if pipe_path: - try: os.unlink(pipe_path) + try: + os.unlink(pipe_path) except Exception: logger.exception("try_delete_pipes()") def start(self): self.modification_lock.acquire() - if (self.running): + if self.running: self.modification_lock.release() return self.running = True @@ -486,37 +502,58 @@ class dsp(object): command_base = " | ".join(self.chain(self.demodulator)) logger.debug(command_base) - #create control pipes for csdr + # create control pipes for csdr self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self)) self.try_create_pipes(self.pipe_names, command_base) - #run the command - command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, - last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, - bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), - flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, - squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, - output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000), - unvoiced_quality = self.get_unvoiced_quality(), dmr_control_pipe = self.dmr_control_pipe, - audio_rate = self.get_audio_rate()) + # run the command + command = command_base.format( + bpf_pipe=self.bpf_pipe, + shift_pipe=self.shift_pipe, + decimation=self.decimation, + last_decimation=self.last_decimation, + fft_size=self.fft_size, + fft_block_size=self.fft_block_size(), + fft_averages=self.fft_averages, + bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(), + ddc_transition_bw=self.ddc_transition_bw(), + flowcontrol=int(self.samp_rate * 2), + start_bufsize=self.base_bufsize * self.decimation, + nc_port=self.nc_port, + squelch_pipe=self.squelch_pipe, + smeter_pipe=self.smeter_pipe, + meta_pipe=self.meta_pipe, + iqtee_pipe=self.iqtee_pipe, + iqtee2_pipe=self.iqtee2_pipe, + output_rate=self.get_output_rate(), + smeter_report_every=int(self.if_samp_rate() / 6000), + unvoiced_quality=self.get_unvoiced_quality(), + dmr_control_pipe=self.dmr_control_pipe, + audio_rate=self.get_audio_rate(), + ) logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) - my_env=os.environ.copy() - if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; - if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; + my_env = os.environ.copy() + if self.csdr_dynamic_bufsize: + my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1" + if self.csdr_print_bufsizes: + my_env["CSDR_PRINT_BUFSIZES"] = "1" self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) def watch_thread(): rc = self.process.wait() logger.debug("dsp thread ended with rc=%d", rc) - if (rc == 0 and self.running and not self.modification_lock.locked()): + if rc == 0 and self.running and not self.modification_lock.locked(): logger.debug("restarting since rc = 0, self.running = true, and no modification") self.restart() - threading.Thread(target = watch_thread).start() + threading.Thread(target=watch_thread).start() - self.output.add_output("audio", partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256)) + self.output.add_output( + "audio", + partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256), + ) # open control pipes for csdr if self.bpf_pipe: @@ -537,23 +574,27 @@ class dsp(object): if self.bpf_pipe: self.set_bpf(self.low_cut, self.high_cut) if self.smeter_pipe: - self.smeter_pipe_file=open(self.smeter_pipe,"r") + self.smeter_pipe_file = open(self.smeter_pipe, "r") + def read_smeter(): raw = self.smeter_pipe_file.readline() if len(raw) == 0: return None else: return float(raw.rstrip("\n")) + self.output.add_output("smeter", read_smeter) if self.meta_pipe != None: # TODO make digiham output unicode and then change this here - self.meta_pipe_file=open(self.meta_pipe, "r", encoding="cp437") + self.meta_pipe_file = open(self.meta_pipe, "r", encoding="cp437") + def read_meta(): raw = self.meta_pipe_file.readline() if len(raw) == 0: return None else: return raw.rstrip("\n") + self.output.add_output("meta", read_meta) if self.dmr_control_pipe: @@ -575,10 +616,11 @@ class dsp(object): self.modification_lock.release() def restart(self): - if not self.running: return + if not self.running: + return self.stop() self.start() def __del__(self): self.stop() - del(self.process) + del self.process diff --git a/openwebrx.py b/openwebrx.py index 99b1419..07b48de 100644 --- a/openwebrx.py +++ b/openwebrx.py @@ -1,13 +1,14 @@ from http.server import HTTPServer from owrx.http import RequestHandler from owrx.config import PropertyManager -from owrx.feature import FeatureDetector +from owrx.feature import FeatureDetector from owrx.source import SdrService, ClientRegistry from socketserver import ThreadingMixIn from owrx.sdrhu import SdrHuUpdater import logging -logging.basicConfig(level = logging.DEBUG, format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s") + +logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") class ThreadedHttpServer(ThreadingMixIn, HTTPServer): @@ -15,21 +16,25 @@ class ThreadedHttpServer(ThreadingMixIn, HTTPServer): def main(): - print(""" + print( + """ OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package _________________________________________________________________________________________________ Author contact info: Andras Retzler, HA7ILM - """) + """ + ) pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") featureDetector = FeatureDetector() if not featureDetector.is_available("core"): - print("you are missing required dependencies to run openwebrx. " - "please check that the following core requirements are installed:") + print( + "you are missing required dependencies to run openwebrx. " + "please check that the following core requirements are installed:" + ) print(", ".join(featureDetector.get_requirements("core"))) return @@ -40,7 +45,7 @@ Author contact info: Andras Retzler, HA7ILM updater = SdrHuUpdater() updater.start() - server = ThreadedHttpServer(('0.0.0.0', pm.getPropertyValue("web_port")), RequestHandler) + server = ThreadedHttpServer(("0.0.0.0", pm.getPropertyValue("web_port")), RequestHandler) server.serve_forever() diff --git a/owrx/bands.py b/owrx/bands.py index 5971bf8..bc76b2a 100644 --- a/owrx/bands.py +++ b/owrx/bands.py @@ -1,6 +1,7 @@ import json import logging + logger = logging.getLogger(__name__) @@ -16,7 +17,11 @@ class Band(object): freqs = [freqs] for f in freqs: if not self.inBand(f): - logger.warning("Frequency for {mode} on {band} is not within band limits: {frequency}".format(mode = mode, frequency = f, band = self.name)) + logger.warning( + "Frequency for {mode} on {band} is not within band limits: {frequency}".format( + mode=mode, frequency=f, band=self.name + ) + ) else: self.frequencies.append({"mode": mode, "frequency": f}) @@ -33,6 +38,7 @@ class Band(object): class Bandplan(object): sharedInstance = None + @staticmethod def getSharedInstance(): if Bandplan.sharedInstance is None: diff --git a/owrx/config.py b/owrx/config.py index 6db151e..d8b6ad2 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -1,4 +1,5 @@ import logging + logger = logging.getLogger(__name__) @@ -15,7 +16,7 @@ class Subscription(object): class Property(object): - def __init__(self, value = None): + def __init__(self, value=None): self.value = value self.subscribers = [] @@ -23,7 +24,7 @@ class Property(object): return self.value def setValue(self, value): - if (self.value == value): + if self.value == value: return self self.value = value for c in self.subscribers: @@ -36,7 +37,8 @@ class Property(object): def wire(self, callback): sub = Subscription(self, callback) self.subscribers.append(sub) - if not self.value is None: sub.call(self.value) + if not self.value is None: + sub.call(self.value) return sub def unwire(self, sub): @@ -47,8 +49,10 @@ class Property(object): pass return self + class PropertyManager(object): sharedInstance = None + @staticmethod def getSharedInstance(): if PropertyManager.sharedInstance is None: @@ -56,9 +60,11 @@ class PropertyManager(object): return PropertyManager.sharedInstance def collect(self, *props): - return PropertyManager({name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props}) + return PropertyManager( + {name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props} + ) - def __init__(self, properties = None): + def __init__(self, properties=None): self.properties = {} self.subscribers = [] if properties is not None: @@ -67,12 +73,14 @@ class PropertyManager(object): def add(self, name, prop): self.properties[name] = prop + def fireCallbacks(value): for c in self.subscribers: try: c.call(name, value) except Exception as e: logger.exception(e) + prop.wire(fireCallbacks) return self @@ -88,7 +96,7 @@ class PropertyManager(object): self.getProperty(name).setValue(value) def __dict__(self): - return {k:v.getValue() for k, v in self.properties.items()} + return {k: v.getValue() for k, v in self.properties.items()} def hasProperty(self, name): return name in self.properties diff --git a/owrx/connection.py b/owrx/connection.py index 17551ff..b68cbaf 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -7,6 +7,7 @@ import json from owrx.map import Map import logging + logger = logging.getLogger(__name__) @@ -29,11 +30,26 @@ class Client(object): class OpenWebRxReceiverClient(Client): - config_keys = ["waterfall_colors", "waterfall_min_level", "waterfall_max_level", - "waterfall_auto_level_margin", "lfo_offset", "samp_rate", "fft_size", "fft_fps", - "audio_compression", "fft_compression", "max_clients", "start_mod", - "client_audio_buffer_size", "start_freq", "center_freq", "mathbox_waterfall_colors", - "mathbox_waterfall_history_length", "mathbox_waterfall_frequency_resolution"] + config_keys = [ + "waterfall_colors", + "waterfall_min_level", + "waterfall_max_level", + "waterfall_auto_level_margin", + "lfo_offset", + "samp_rate", + "fft_size", + "fft_fps", + "audio_compression", + "fft_compression", + "max_clients", + "start_mod", + "client_audio_buffer_size", + "start_freq", + "center_freq", + "mathbox_waterfall_colors", + "mathbox_waterfall_history_length", + "mathbox_waterfall_frequency_resolution", + ] def __init__(self, conn): super().__init__(conn) @@ -49,12 +65,23 @@ class OpenWebRxReceiverClient(Client): self.setSdr() # send receiver info - receiver_keys = ["receiver_name", "receiver_location", "receiver_qra", "receiver_asl", "receiver_gps", - "photo_title", "photo_desc"] + receiver_keys = [ + "receiver_name", + "receiver_location", + "receiver_qra", + "receiver_asl", + "receiver_gps", + "photo_title", + "photo_desc", + ] receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys) self.write_receiver_details(receiver_details) - profiles = [{"name": s.getName() + " " + p["name"], "id":sid + "|" + pid} for (sid, s) in SdrService.getSources().items() for (pid, p) in s.getProfiles().items()] + profiles = [ + {"name": s.getName() + " " + p["name"], "id": sid + "|" + pid} + for (sid, s) in SdrService.getSources().items() + for (pid, p) in s.getProfiles().items() + ] self.write_profiles(profiles) features = FeatureDetector().feature_availability() @@ -62,9 +89,9 @@ class OpenWebRxReceiverClient(Client): CpuUsageThread.getSharedInstance().add_client(self) - def setSdr(self, id = None): + def setSdr(self, id=None): next = SdrService.getSource(id) - if (next == self.sdr): + if next == self.sdr: return self.stopDsp() @@ -76,7 +103,11 @@ class OpenWebRxReceiverClient(Client): self.sdr = next # send initial config - configProps = self.sdr.getProps().collect(*OpenWebRxReceiverClient.config_keys).defaults(PropertyManager.getSharedInstance()) + configProps = ( + self.sdr.getProps() + .collect(*OpenWebRxReceiverClient.config_keys) + .defaults(PropertyManager.getSharedInstance()) + ) def sendConfig(key, value): config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys) @@ -89,7 +120,6 @@ class OpenWebRxReceiverClient(Client): frequencyRange = (cf - srh, cf + srh) self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencis(frequencyRange)) - self.configSub = configProps.wire(sendConfig) sendConfig(None, None) @@ -118,8 +148,11 @@ class OpenWebRxReceiverClient(Client): def setParams(self, params): # only the keys in the protected property manager can be overridden from the web - protected = self.sdr.getProps().collect("samp_rate", "center_freq", "rf_gain", "type", "if_gain") \ + protected = ( + self.sdr.getProps() + .collect("samp_rate", "center_freq", "rf_gain", "type", "if_gain") .defaults(PropertyManager.getSharedInstance()) + ) for key, value in params.items(): protected[key] = value @@ -134,13 +167,13 @@ class OpenWebRxReceiverClient(Client): self.protected_send(bytes([0x02]) + data) def write_s_meter_level(self, level): - self.protected_send({"type":"smeter","value":level}) + self.protected_send({"type": "smeter", "value": level}) def write_cpu_usage(self, usage): - self.protected_send({"type":"cpuusage","value":usage}) + self.protected_send({"type": "cpuusage", "value": usage}) def write_clients(self, clients): - self.protected_send({"type":"clients","value":clients}) + self.protected_send({"type": "clients", "value": clients}) def write_secondary_fft(self, data): self.protected_send(bytes([0x03]) + data) @@ -149,22 +182,22 @@ class OpenWebRxReceiverClient(Client): self.protected_send(bytes([0x04]) + data) def write_secondary_dsp_config(self, cfg): - self.protected_send({"type":"secondary_config", "value":cfg}) + self.protected_send({"type": "secondary_config", "value": cfg}) def write_config(self, cfg): - self.protected_send({"type":"config","value":cfg}) + self.protected_send({"type": "config", "value": cfg}) def write_receiver_details(self, details): - self.protected_send({"type":"receiver_details","value":details}) + self.protected_send({"type": "receiver_details", "value": details}) def write_profiles(self, profiles): - self.protected_send({"type":"profiles","value":profiles}) + self.protected_send({"type": "profiles", "value": profiles}) def write_features(self, features): - self.protected_send({"type":"features","value":features}) + self.protected_send({"type": "features", "value": features}) def write_metadata(self, metadata): - self.protected_send({"type":"metadata","value":metadata}) + self.protected_send({"type": "metadata", "value": metadata}) def write_wsjt_message(self, message): self.protected_send({"type": "wsjt_message", "value": message}) @@ -187,10 +220,11 @@ class MapConnection(Client): super().close() def write_config(self, cfg): - self.protected_send({"type":"config","value":cfg}) + self.protected_send({"type": "config", "value": cfg}) def write_update(self, update): - self.protected_send({"type":"update","value":update}) + self.protected_send({"type": "update", "value": update}) + class WebSocketMessageHandler(object): def __init__(self): @@ -199,11 +233,11 @@ class WebSocketMessageHandler(object): self.dsp = None def handleTextMessage(self, conn, message): - if (message[:16] == "SERVER DE CLIENT"): + if message[:16] == "SERVER DE CLIENT": meta = message[17:].split(" ") self.handshake = {v[0]: "=".join(v[1:]) for v in map(lambda x: x.split("="), meta)} - conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version = openwebrx_version)) + conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version=openwebrx_version)) logger.debug("client connection intitialized") if "type" in self.handshake: diff --git a/owrx/controllers.py b/owrx/controllers.py index c011677..c6c0da5 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -11,13 +11,16 @@ from owrx.version import openwebrx_version from owrx.feature import FeatureDetector import logging + logger = logging.getLogger(__name__) + class Controller(object): def __init__(self, handler, request): self.handler = handler self.request = request - def send_response(self, content, code = 200, content_type = "text/html", last_modified: datetime = None, max_age = None): + + def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None): self.handler.send_response(code) if content_type is not None: self.handler.send_header("Content-Type", content_type) @@ -26,7 +29,7 @@ class Controller(object): if max_age is not None: self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age)) self.handler.end_headers() - if (type(content) == str): + if type(content) == str: content = content.encode() self.handler.wfile.write(content) @@ -45,44 +48,49 @@ class StatusController(Controller): "asl": pm["receiver_asl"], "loc": pm["receiver_location"], "sw_version": openwebrx_version, - "avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png") + "avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png"), } - self.send_response("\n".join(["{key}={value}".format(key = key, value = value) for key, value in vars.items()])) + self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()])) + class AssetsController(Controller): - def serve_file(self, file, content_type = None): + def serve_file(self, file, content_type=None): try: - modified = datetime.fromtimestamp(os.path.getmtime('htdocs/' + file)) + modified = datetime.fromtimestamp(os.path.getmtime("htdocs/" + file)) if "If-Modified-Since" in self.handler.headers: - client_modified = datetime.strptime(self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z") + client_modified = datetime.strptime( + self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z" + ) if modified <= client_modified: - self.send_response("", code = 304) + self.send_response("", code=304) return - f = open('htdocs/' + file, 'rb') + f = open("htdocs/" + file, "rb") data = f.read() f.close() if content_type is None: (content_type, encoding) = mimetypes.MimeTypes().guess_type(file) - self.send_response(data, content_type = content_type, last_modified = modified, max_age = 3600) + self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600) except FileNotFoundError: - self.send_response("file not found", code = 404) + self.send_response("file not found", code=404) + def handle_request(self): filename = self.request.matches.group(1) self.serve_file(filename) + class TemplateController(Controller): def render_template(self, file, **vars): - f = open('htdocs/' + file, 'r') + f = open("htdocs/" + file, "r") template = Template(f.read()) f.close() return template.safe_substitute(**vars) def serve_template(self, file, **vars): - self.send_response(self.render_template(file, **vars), content_type = 'text/html') + self.send_response(self.render_template(file, **vars), content_type="text/html") def default_variables(self): return {} @@ -90,8 +98,8 @@ class TemplateController(Controller): class WebpageController(TemplateController): def template_variables(self): - header = self.render_template('include/header.include.html') - return { "header": header } + header = self.render_template("include/header.include.html") + return {"header": header} class IndexController(WebpageController): @@ -101,17 +109,20 @@ class IndexController(WebpageController): class MapController(WebpageController): def handle_request(self): - #TODO check if we have a google maps api key first? + # TODO check if we have a google maps api key first? self.serve_template("map.html", **self.template_variables()) + class FeatureController(WebpageController): def handle_request(self): self.serve_template("features.html", **self.template_variables()) + class ApiController(Controller): def handle_request(self): data = json.dumps(FeatureDetector().feature_report()) - self.send_response(data, content_type = "application/json") + self.send_response(data, content_type="application/json") + class WebSocketController(Controller): def handle_request(self): diff --git a/owrx/feature.py b/owrx/feature.py index cd67f62..66fe1f9 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -7,6 +7,7 @@ from distutils.version import LooseVersion import inspect import logging + logger = logging.getLogger(__name__) @@ -16,14 +17,14 @@ class UnknownFeatureException(Exception): class FeatureDetector(object): features = { - "core": [ "csdr", "nmux", "nc" ], - "rtl_sdr": [ "rtl_sdr" ], - "sdrplay": [ "rx_tools" ], - "hackrf": [ "hackrf_transfer" ], - "airspy": [ "airspy_rx" ], - "digital_voice_digiham": [ "digiham", "sox" ], - "digital_voice_dsd": [ "dsd", "sox", "digiham" ], - "wsjt-x": [ "wsjtx", "sox" ] + "core": ["csdr", "nmux", "nc"], + "rtl_sdr": ["rtl_sdr"], + "sdrplay": ["rx_tools"], + "hackrf": ["hackrf_transfer"], + "airspy": ["airspy_rx"], + "digital_voice_digiham": ["digiham", "sox"], + "digital_voice_dsd": ["dsd", "sox", "digiham"], + "wsjt-x": ["wsjtx", "sox"], } def feature_availability(self): @@ -36,14 +37,14 @@ class FeatureDetector(object): "available": available, # as of now, features are always enabled as soon as they are available. this may change in the future. "enabled": available, - "description": self.get_requirement_description(name) + "description": self.get_requirement_description(name), } def feature_details(name): return { "description": "", "available": self.is_available(name), - "requirements": {name: requirement_details(name) for name in self.get_requirements(name)} + "requirements": {name: requirement_details(name) for name in self.get_requirements(name)}, } return {name: feature_details(name) for name in FeatureDetector.features} @@ -55,7 +56,7 @@ class FeatureDetector(object): try: return FeatureDetector.features[feature] except KeyError: - raise UnknownFeatureException("Feature \"{0}\" is not known.".format(feature)) + raise UnknownFeatureException('Feature "{0}" is not known.'.format(feature)) def has_requirements(self, requirements): passed = True @@ -102,7 +103,7 @@ class FeatureDetector(object): Nc is the client used to connect to the nmux multiplexer. It is provided by either the BSD netcat (recommended for better performance) or GNU netcat packages. Please check your distribution package manager for options. """ - return self.command_is_runnable('nc --help') + return self.command_is_runnable("nc --help") def has_rtl_sdr(self): """ @@ -156,7 +157,8 @@ class FeatureDetector(object): """ required_version = LooseVersion("0.2") - digiham_version_regex = re.compile('^digiham version (.*)$') + digiham_version_regex = re.compile("^digiham version (.*)$") + def check_digiham_version(command): try: process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE) @@ -165,14 +167,21 @@ class FeatureDetector(object): return version >= required_version except FileNotFoundError: return False + return reduce( and_, map( check_digiham_version, - ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator", - "digitalvoice_filter"] + [ + "rrc_filter", + "ysf_decoder", + "dmr_decoder", + "mbe_synthesizer", + "gfsk_demodulator", + "digitalvoice_filter", + ], ), - True + True, ) def has_dsd(self): @@ -203,11 +212,4 @@ class FeatureDetector(object): [WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions on how to build from source. """ - return reduce( - and_, - map( - self.command_is_runnable, - ["jt9", "wsprd"] - ), - True - ) + return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True) diff --git a/owrx/http.py b/owrx/http.py index ce821b9..99c1003 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -1,23 +1,36 @@ -from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController, MapController, FeatureController, ApiController +from owrx.controllers import ( + StatusController, + IndexController, + AssetsController, + WebSocketController, + MapController, + FeatureController, + ApiController, +) from http.server import BaseHTTPRequestHandler import re from urllib.parse import urlparse, parse_qs import logging + logger = logging.getLogger(__name__) + class RequestHandler(BaseHTTPRequestHandler): def __init__(self, request, client_address, server): self.router = Router() super().__init__(request, client_address, server) + def do_GET(self): self.router.route(self) + class Request(object): - def __init__(self, query = None, matches = None): + def __init__(self, query=None, matches=None): self.query = query self.matches = matches + class Router(object): mappings = [ {"route": "/", "controller": IndexController}, @@ -29,8 +42,9 @@ class Router(object): {"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController}, {"route": "/map", "controller": MapController}, {"route": "/features", "controller": FeatureController}, - {"route": "/api/features", "controller": ApiController} + {"route": "/api/features", "controller": ApiController}, ] + def find_controller(self, path): for m in Router.mappings: if "route" in m: @@ -41,13 +55,16 @@ class Router(object): matches = regex.match(path) if matches: return (m["controller"], matches) + def route(self, handler): url = urlparse(handler.path) res = self.find_controller(url.path) if res is not None: (controller, matches) = res query = parse_qs(url.query) - logger.debug("path: {0}, controller: {1}, query: {2}, matches: {3}".format(handler.path, controller, query, matches)) + logger.debug( + "path: {0}, controller: {1}, query: {2}, matches: {3}".format(handler.path, controller, query, matches) + ) request = Request(query, matches) controller(handler, request).handle_request() else: diff --git a/owrx/map.py b/owrx/map.py index a6adbb6..9908d7a 100644 --- a/owrx/map.py +++ b/owrx/map.py @@ -4,6 +4,7 @@ from owrx.config import PropertyManager from owrx.bands import Band import logging + logger = logging.getLogger(__name__) @@ -14,6 +15,7 @@ class Location(object): class Map(object): sharedInstance = None + @staticmethod def getSharedInstance(): if Map.sharedInstance is None: @@ -41,16 +43,18 @@ class Map(object): def addClient(self, client): self.clients.append(client) - client.write_update([ - { - "callsign": callsign, - "location": record["location"].__dict__(), - "lastseen": record["updated"].timestamp() * 1000, - "mode" : record["mode"], - "band" : record["band"].getName() if record["band"] is not None else None - } - for (callsign, record) in self.positions.items() - ]) + client.write_update( + [ + { + "callsign": callsign, + "location": record["location"].__dict__(), + "lastseen": record["updated"].timestamp() * 1000, + "mode": record["mode"], + "band": record["band"].getName() if record["band"] is not None else None, + } + for (callsign, record) in self.positions.items() + ] + ) def removeClient(self, client): try: @@ -61,15 +65,17 @@ class Map(object): def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None): ts = datetime.now() self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band} - self.broadcast([ - { - "callsign": callsign, - "location": loc.__dict__(), - "lastseen": ts.timestamp() * 1000, - "mode" : mode, - "band" : band.getName() if band is not None else None - } - ]) + self.broadcast( + [ + { + "callsign": callsign, + "location": loc.__dict__(), + "lastseen": ts.timestamp() * 1000, + "mode": mode, + "band": band.getName() if band is not None else None, + } + ] + ) def removeLocation(self, callsign): self.positions.pop(callsign, None) @@ -84,17 +90,14 @@ class Map(object): for callsign in to_be_removed: self.removeLocation(callsign) + class LatLngLocation(Location): def __init__(self, lat: float, lon: float): self.lat = lat self.lon = lon def __dict__(self): - return { - "type":"latlon", - "lat":self.lat, - "lon":self.lon - } + return {"type": "latlon", "lat": self.lat, "lon": self.lon} class LocatorLocation(Location): @@ -102,7 +105,4 @@ class LocatorLocation(Location): self.locator = locator def __dict__(self): - return { - "type":"locator", - "locator":self.locator - } + return {"type": "locator", "locator": self.locator} diff --git a/owrx/meta.py b/owrx/meta.py index 8a85bad..1d7a63f 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -8,8 +8,10 @@ from owrx.map import Map, LatLngLocation logger = logging.getLogger(__name__) + class DmrCache(object): sharedInstance = None + @staticmethod def getSharedInstance(): if DmrCache.sharedInstance is None: @@ -18,21 +20,20 @@ class DmrCache(object): def __init__(self): self.cache = {} - self.cacheTimeout = timedelta(seconds = 86400) + self.cacheTimeout = timedelta(seconds=86400) def isValid(self, key): - if not key in self.cache: return False + if not key in self.cache: + return False entry = self.cache[key] return entry["timestamp"] + self.cacheTimeout > datetime.now() def put(self, key, value): - self.cache[key] = { - "timestamp": datetime.now(), - "data": value - } + self.cache[key] = {"timestamp": datetime.now(), "data": value} def get(self, key): - if not self.isValid(key): return None + if not self.isValid(key): + return None return self.cache[key]["data"] @@ -52,8 +53,10 @@ class DmrMetaEnricher(object): del self.threads[id] def enrich(self, meta): - if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: return None - if not "source" in meta: return None + if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: + return None + if not "source" in meta: + return None id = meta["source"] cache = DmrCache.getSharedInstance() if not cache.isValid(id): @@ -77,10 +80,7 @@ class YsfMetaEnricher(object): class MetaParser(object): - enrichers = { - "DMR": DmrMetaEnricher(), - "YSF": YsfMetaEnricher() - } + enrichers = {"DMR": DmrMetaEnricher(), "YSF": YsfMetaEnricher()} def __init__(self, handler): self.handler = handler @@ -93,6 +93,6 @@ class MetaParser(object): protocol = meta["protocol"] if protocol in MetaParser.enrichers: additional_data = MetaParser.enrichers[protocol].enrich(meta) - if additional_data is not None: meta["additional"] = additional_data + if additional_data is not None: + meta["additional"] = additional_data self.handler.write_metadata(meta) - diff --git a/owrx/sdrhu.py b/owrx/sdrhu.py index 5f0d7fb..c84a2f5 100644 --- a/owrx/sdrhu.py +++ b/owrx/sdrhu.py @@ -4,23 +4,26 @@ 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) + super().__init__(daemon=True) def update(self): pm = PropertyManager.getSharedInstance() - cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}\" 2>&1".format(**pm.__dict__()) + cmd = 'wget --timeout=15 -4qO- https://sdr.hu/update --post-data "url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}" 2>&1'.format( + **pm.__dict__() + ) logger.debug(cmd) - returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate() - returned=returned[0].decode('utf-8') + returned = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate() + returned = returned[0].decode("utf-8") if "UPDATE:" in returned: retrytime_mins = 20 - value=returned.split("UPDATE:")[1].split("\n",1)[0] + value = returned.split("UPDATE:")[1].split("\n", 1)[0] if value.startswith("SUCCESS"): logger.info("Update succeeded!") else: @@ -33,4 +36,4 @@ class SdrHuUpdater(threading.Thread): def run(self): while self.doRun: retrytime_mins = self.update() - time.sleep(60*retrytime_mins) + time.sleep(60 * retrytime_mins) diff --git a/owrx/source.py b/owrx/source.py index 2e2bca7..da2e928 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -14,10 +14,12 @@ import logging logger = logging.getLogger(__name__) + class SdrService(object): sdrProps = None sources = {} lastPort = None + @staticmethod def getNextPort(): pm = PropertyManager.getSharedInstance() @@ -29,45 +31,61 @@ class SdrService(object): if SdrService.lastPort > end: raise IndexError("no more available ports to start more sdrs") return SdrService.lastPort + @staticmethod def loadProps(): if SdrService.sdrProps is None: pm = PropertyManager.getSharedInstance() featureDetector = FeatureDetector() + def loadIntoPropertyManager(dict: dict): propertyManager = PropertyManager() for (name, value) in dict.items(): propertyManager[name] = value return propertyManager + def sdrTypeAvailable(value): try: if not featureDetector.is_available(value["type"]): - logger.error("The RTL source type \"{0}\" is not available. please check requirements.".format(value["type"])) + logger.error( + 'The RTL source type "{0}" is not available. please check requirements.'.format( + value["type"] + ) + ) return False return True except UnknownFeatureException: - logger.error("The RTL source type \"{0}\" is invalid. Please check your configuration".format(value["type"])) + logger.error( + 'The RTL source type "{0}" is invalid. Please check your configuration'.format(value["type"]) + ) return False + # transform all dictionary items into PropertyManager object, filtering out unavailable ones SdrService.sdrProps = { name: loadIntoPropertyManager(value) for (name, value) in pm["sdrs"].items() if sdrTypeAvailable(value) } - logger.info("SDR sources loaded. Availables SDRs: {0}".format(", ".join(map(lambda x: x["name"], SdrService.sdrProps.values())))) + logger.info( + "SDR sources loaded. Availables SDRs: {0}".format( + ", ".join(map(lambda x: x["name"], SdrService.sdrProps.values())) + ) + ) + @staticmethod - def getSource(id = None): + def getSource(id=None): SdrService.loadProps() if id is None: # TODO: configure default sdr in config? right now it will pick the first one off the list. id = list(SdrService.sdrProps.keys())[0] sources = SdrService.getSources() return sources[id] + @staticmethod def getSources(): SdrService.loadProps() for id in SdrService.sdrProps.keys(): if not id in SdrService.sources: props = SdrService.sdrProps[id] - className = ''.join(x for x in props["type"].title() if x.isalnum()) + "Source" + className = "".join(x for x in props["type"].title() if x.isalnum()) + "Source" cls = getattr(sys.modules[__name__], className) SdrService.sources[id] = cls(props, SdrService.getNextPort()) return SdrService.sources @@ -85,6 +103,7 @@ class SdrSource(object): logger.debug("restarting sdr source due to property change: {0} changed to {1}".format(name, value)) self.stop() self.start() + self.rtlProps.wire(restart) self.port = port self.monitor = None @@ -102,7 +121,7 @@ class SdrSource(object): def getFormatConversion(self): return None - def activateProfile(self, id = None): + def activateProfile(self, id=None): profiles = self.props["profiles"] if id is None: id = list(profiles.keys())[0] @@ -110,7 +129,8 @@ class SdrSource(object): profile = profiles[id] for (key, value) in profile.items(): # skip the name, that would overwrite the source name. - if key == "name": continue + if key == "name": + continue self.props[key] = value def getProfiles(self): @@ -134,7 +154,9 @@ class SdrSource(object): props = self.rtlProps start_sdr_command = self.getCommand().format( - **props.collect("samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain").__dict__() + **props.collect( + "samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain" + ).__dict__() ) format_conversion = self.getFormatConversion() @@ -142,14 +164,22 @@ class SdrSource(object): start_sdr_command += " | " + format_conversion nmux_bufcnt = nmux_bufsize = 0 - while nmux_bufsize < props["samp_rate"]/4: nmux_bufsize += 4096 - while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6: nmux_bufcnt += 1 + while nmux_bufsize < props["samp_rate"] / 4: + nmux_bufsize += 4096 + while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6: + nmux_bufcnt += 1 if nmux_bufcnt == 0 or nmux_bufsize == 0: - logger.error("Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py") + logger.error( + "Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py" + ) self.modificationLock.release() return logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)) - cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, self.port) + cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % ( + nmux_bufsize, + nmux_bufcnt, + self.port, + ) self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp) logger.info("Started rtl source: " + cmd) @@ -158,7 +188,7 @@ class SdrSource(object): logger.debug("shut down with RC={0}".format(rc)) self.monitor = None - self.monitor = threading.Thread(target = wait_for_process_to_end) + self.monitor = threading.Thread(target=wait_for_process_to_end) self.monitor.start() while True: @@ -201,6 +231,7 @@ class SdrSource(object): def addClient(self, c): self.clients.append(c) self.start() + def removeClient(self, c): try: self.clients.remove(c) @@ -236,6 +267,7 @@ class RtlSdrSource(SdrSource): def getFormatConversion(self): return "csdr convert_u8_f" + class HackrfSource(SdrSource): def getCommand(self): return "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-" @@ -243,39 +275,54 @@ class HackrfSource(SdrSource): def getFormatConversion(self): return "csdr convert_s8_f" + class SdrplaySource(SdrSource): def getCommand(self): command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}" - gainMap = { "rf_gain" : "RFGR", "if_gain" : "IFGR"} - gains = [ "{0}={{{1}}}".format(gainMap[name], name) for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items() if value is not None ] + gainMap = {"rf_gain": "RFGR", "if_gain": "IFGR"} + gains = [ + "{0}={{{1}}}".format(gainMap[name], name) + for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items() + if value is not None + ] if gains: - command += " -g {gains}".format(gains = ",".join(gains)) + command += " -g {gains}".format(gains=",".join(gains)) if self.rtlProps["antenna"] is not None: - command += " -a \"{antenna}\"" + command += ' -a "{antenna}"' command += " -" return command def sleepOnRestart(self): time.sleep(1) + class AirspySource(SdrSource): def getCommand(self): - frequency = self.props['center_freq'] / 1e6 + frequency = self.props["center_freq"] / 1e6 command = "airspy_rx" command += " -f{0}".format(frequency) command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}" return command + def getFormatConversion(self): return "csdr convert_s16_f" + class SpectrumThread(csdr.output): def __init__(self, sdrSource): self.sdrSource = sdrSource super().__init__() self.props = props = self.sdrSource.props.collect( - "samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression", - "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through", "temporary_directory" + "samp_rate", + "fft_size", + "fft_fps", + "fft_voverlap_factor", + "fft_compression", + "csdr_dynamic_bufsize", + "csdr_print_bufsizes", + "csdr_through", + "temporary_directory", ).defaults(PropertyManager.getSharedInstance()) self.dsp = dsp = csdr.dsp(self) @@ -288,7 +335,11 @@ class SpectrumThread(csdr.output): fft_fps = props["fft_fps"] fft_voverlap_factor = props["fft_voverlap_factor"] - dsp.set_fft_averages(int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor))) if fft_voverlap_factor>0 else 0) + dsp.set_fft_averages( + int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor))) + if fft_voverlap_factor > 0 + else 0 + ) self.subscriptions = [ props.getProperty("samp_rate").wire(dsp.set_samp_rate), @@ -296,7 +347,7 @@ class SpectrumThread(csdr.output): props.getProperty("fft_fps").wire(dsp.set_fft_fps), props.getProperty("fft_compression").wire(dsp.set_fft_compression), props.getProperty("temporary_directory").wire(dsp.set_temporary_directory), - props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages) + props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages), ] set_fft_averages(None, None) @@ -317,7 +368,7 @@ class SpectrumThread(csdr.output): return if self.props["csdr_dynamic_bufsize"]: - read_fn(8) #dummy read to skip bufsize & preamble + read_fn(8) # dummy read to skip bufsize & preamble logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1") def pipe(): @@ -329,7 +380,7 @@ class SpectrumThread(csdr.output): else: self.sdrSource.writeSpectrumData(data) - threading.Thread(target = pipe).start() + threading.Thread(target=pipe).start() def stop(self): self.dsp.stop() @@ -340,9 +391,11 @@ class SpectrumThread(csdr.output): def onSdrAvailable(self): self.dsp.start() + def onSdrUnavailable(self): self.dsp.stop() + class DspManager(csdr.output): def __init__(self, handler, sdrSource): self.handler = handler @@ -350,11 +403,24 @@ class DspManager(csdr.output): self.metaParser = MetaParser(self.handler) self.wsjtParser = WsjtParser(self.handler) - self.localProps = self.sdrSource.getProps().collect( - "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", - "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality", - "dmr_filter", "temporary_directory", "center_freq" - ).defaults(PropertyManager.getSharedInstance()) + self.localProps = ( + self.sdrSource.getProps() + .collect( + "audio_compression", + "fft_compression", + "digimodes_fft_size", + "csdr_dynamic_bufsize", + "csdr_print_bufsizes", + "csdr_through", + "digimodes_enable", + "samp_rate", + "digital_voice_unvoiced_quality", + "dmr_filter", + "temporary_directory", + "center_freq", + ) + .defaults(PropertyManager.getSharedInstance()) + ) self.dsp = csdr.dsp(self) self.dsp.nc_port = self.sdrSource.getPort() @@ -386,28 +452,33 @@ class DspManager(csdr.output): self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality), self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter), self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory), - self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq) + self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq), ] self.dsp.set_offset_freq(0) - self.dsp.set_bpf(-4000,4000) + self.dsp.set_bpf(-4000, 4000) self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"] self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"] self.dsp.csdr_through = self.localProps["csdr_through"] - if (self.localProps["digimodes_enable"]): + if self.localProps["digimodes_enable"]: + def set_secondary_mod(mod): - if mod == False: mod = None + if mod == False: + mod = None self.dsp.set_secondary_demodulator(mod) if mod is not None: - self.handler.write_secondary_dsp_config({ - "secondary_fft_size":self.localProps["digimodes_fft_size"], - "if_samp_rate":self.dsp.if_samp_rate(), - "secondary_bw":self.dsp.secondary_bw() - }) + self.handler.write_secondary_dsp_config( + { + "secondary_fft_size": self.localProps["digimodes_fft_size"], + "if_samp_rate": self.dsp.if_samp_rate(), + "secondary_bw": self.dsp.secondary_bw(), + } + ) + self.subscriptions += [ self.localProps.getProperty("secondary_mod").wire(set_secondary_mod), - self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq) + self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq), ] self.sdrSource.addClient(self) @@ -426,7 +497,7 @@ class DspManager(csdr.output): "secondary_fft": self.handler.write_secondary_fft, "secondary_demod": self.handler.write_secondary_demod, "meta": self.metaParser.parse, - "wsjt_demod": self.wsjtParser.parse + "wsjt_demod": self.wsjtParser.parse, } write = writers[t] @@ -440,6 +511,7 @@ class DspManager(csdr.output): run = False else: write(data) + return copy threading.Thread(target=pump(read_fn, write)).start() @@ -462,8 +534,10 @@ class DspManager(csdr.output): logger.debug("received onSdrUnavailable, shutting down DspSource") self.dsp.stop() + class CpuUsageThread(threading.Thread): sharedInstance = None + @staticmethod def getSharedInstance(): if CpuUsageThread.sharedInstance is None: @@ -491,21 +565,23 @@ class CpuUsageThread(threading.Thread): def get_cpu_usage(self): try: - f = open("/proc/stat","r") + f = open("/proc/stat", "r") except: - return 0 #Workaround, possibly we're on a Mac + return 0 # Workaround, possibly we're on a Mac line = "" - while not "cpu " in line: line=f.readline() + while not "cpu " in line: + line = f.readline() f.close() spl = line.split(" ") worktime = int(spl[2]) + int(spl[3]) + int(spl[4]) idletime = int(spl[5]) - dworktime = (worktime - self.last_worktime) - didletime = (idletime - self.last_idletime) - rate = float(dworktime) / (didletime+dworktime) + dworktime = worktime - self.last_worktime + didletime = idletime - self.last_idletime + rate = float(dworktime) / (didletime + dworktime) self.last_worktime = worktime self.last_idletime = idletime - if (self.last_worktime==0): return 0 + if self.last_worktime == 0: + return 0 return rate def add_client(self, c): @@ -523,11 +599,14 @@ class CpuUsageThread(threading.Thread): CpuUsageThread.sharedInstance = None self.doRun = False + class TooManyClientsException(Exception): pass + class ClientRegistry(object): sharedInstance = None + @staticmethod def getSharedInstance(): if ClientRegistry.sharedInstance is None: @@ -558,4 +637,4 @@ class ClientRegistry(object): self.clients.remove(client) except ValueError: pass - self.broadcast() \ No newline at end of file + self.broadcast() diff --git a/owrx/version.py b/owrx/version.py index 7437eda..73f2d99 100644 --- a/owrx/version.py +++ b/owrx/version.py @@ -1 +1 @@ -openwebrx_version = "v0.18" \ No newline at end of file +openwebrx_version = "v0.18" diff --git a/owrx/websocket.py b/owrx/websocket.py index 360f28b..c773cf9 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -3,69 +3,76 @@ 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")): + my_header_keys = list(map(lambda x: x[0], my_headers)) + h_key_exists = lambda x: my_header_keys.count(x) + h_value = lambda x: my_headers[my_header_keys.index(x)][1] + if ( + (not h_key_exists("Upgrade")) + or not (h_value("Upgrade") == "websocket") + or (not h_key_exists("Sec-WebSocket-Key")) + ): raise WebSocketException ws_key = h_value("Sec-WebSocket-Key") shakey = hashlib.sha1() - shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key = ws_key).encode()) + shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key=ws_key).encode()) ws_key_toreturn = base64.b64encode(shakey.digest()) - self.handler.wfile.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format(ws_key_toreturn.decode()).encode()) + self.handler.wfile.write( + "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format( + ws_key_toreturn.decode() + ).encode() + ) def get_header(self, size, opcode): ws_first_byte = 0b10000000 | (opcode & 0x0F) - if (size > 2**16 - 1): + if size > 2 ** 16 - 1: # frame size can be increased up to 2^64 by setting the size to 127 # anything beyond that would need to be segmented into frames. i don't really think we'll need more. - return bytes([ - ws_first_byte, - 127, - (size >> 56) & 0xff, - (size >> 48) & 0xff, - (size >> 40) & 0xff, - (size >> 32) & 0xff, - (size >> 24) & 0xff, - (size >> 16) & 0xff, - (size >> 8) & 0xff, - size & 0xff - ]) - elif (size > 125): + return bytes( + [ + ws_first_byte, + 127, + (size >> 56) & 0xFF, + (size >> 48) & 0xFF, + (size >> 40) & 0xFF, + (size >> 32) & 0xFF, + (size >> 24) & 0xFF, + (size >> 16) & 0xFF, + (size >> 8) & 0xFF, + size & 0xFF, + ] + ) + elif size > 125: # up to 2^16 can be sent using the extended payload size field by putting the size to 126 - return bytes([ - ws_first_byte, - 126, - (size >> 8) & 0xff, - size & 0xff - ]) + return bytes([ws_first_byte, 126, (size >> 8) & 0xFF, size & 0xFF]) else: # 125 bytes binary message in a single unmasked frame return bytes([ws_first_byte, size]) def send(self, data): # convenience - if (type(data) == dict): + if type(data) == dict: # allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway. - data = json.dumps(data, allow_nan = False) + data = json.dumps(data, allow_nan=False) # string-type messages are sent as text frames - if (type(data) == str): + if type(data) == str: header = self.get_header(len(data), 1) - data_to_send = header + data.encode('utf-8') + data_to_send = header + data.encode("utf-8") # anything else as binary else: header = self.get_header(len(data), 2) data_to_send = header + data written = self.handler.wfile.write(data_to_send) - if (written != len(data_to_send)): + if written != len(data_to_send): logger.error("incomplete write! closing socket!") self.close() else: @@ -73,25 +80,25 @@ class WebSocketConnection(object): def read_loop(self): open = True - while (open): + while open: header = self.handler.rfile.read(2) opcode = header[0] & 0x0F length = header[1] & 0x7F mask = (header[1] & 0x80) >> 7 - if (length == 126): + if length == 126: header = self.handler.rfile.read(2) length = (header[0] << 8) + header[1] - if (mask): + if mask: masking_key = self.handler.rfile.read(4) data = self.handler.rfile.read(length) - if (mask): + if mask: data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) - if (opcode == 1): - message = data.decode('utf-8') + if opcode == 1: + message = data.decode("utf-8") self.messageHandler.handleTextMessage(self, message) - elif (opcode == 2): + elif opcode == 2: self.messageHandler.handleBinaryMessage(self, data) - elif (opcode == 8): + elif opcode == 8: open = False self.messageHandler.handleClose(self) else: diff --git a/owrx/wsjt.py b/owrx/wsjt.py index fb97ac8..0a401a4 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -12,6 +12,7 @@ from owrx.config import PropertyManager from owrx.bands import Bandplan import logging + logger = logging.getLogger(__name__) @@ -29,9 +30,7 @@ class WsjtChopper(threading.Thread): def getWaveFile(self): filename = "{tmp_dir}/openwebrx-wsjtchopper-{id}-{timestamp}.wav".format( - tmp_dir = self.tmp_dir, - id = id(self), - timestamp = datetime.utcnow().strftime(self.fileTimestampFormat) + tmp_dir=self.tmp_dir, id=id(self), timestamp=datetime.utcnow().strftime(self.fileTimestampFormat) ) wavefile = wave.open(filename, "wb") wavefile.setnchannels(1) @@ -44,13 +43,13 @@ class WsjtChopper(threading.Thread): zeroed = t.replace(minute=0, second=0, microsecond=0) delta = t - zeroed seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval - t = zeroed + timedelta(seconds = seconds) + t = zeroed + timedelta(seconds=seconds) logger.debug("scheduling: {0}".format(t)) return t.timestamp() def startScheduler(self): self._scheduleNextSwitch() - threading.Thread(target = self.scheduler.run).start() + threading.Thread(target=self.scheduler.run).start() def emptyScheduler(self): for event in self.scheduler.queue: @@ -132,7 +131,7 @@ class Ft8Chopper(WsjtChopper): super().__init__(source) def decoder_commandline(self, file): - #TODO expose decoding quality parameters through config + # TODO expose decoding quality parameters through config return ["jt9", "--ft8", "-d", "3", file] @@ -143,7 +142,7 @@ class WsprChopper(WsjtChopper): super().__init__(source) def decoder_commandline(self, file): - #TODO expose decoding quality parameters through config + # TODO expose decoding quality parameters through config return ["wsprd", "-d", file] @@ -154,7 +153,7 @@ class Jt65Chopper(WsjtChopper): super().__init__(source) def decoder_commandline(self, file): - #TODO expose decoding quality parameters through config + # TODO expose decoding quality parameters through config return ["jt9", "--jt65", "-d", "3", file] @@ -165,7 +164,7 @@ class Jt9Chopper(WsjtChopper): super().__init__(source) def decoder_commandline(self, file): - #TODO expose decoding quality parameters through config + # TODO expose decoding quality parameters through config return ["jt9", "--jt9", "-d", "3", file] @@ -176,7 +175,7 @@ class Ft4Chopper(WsjtChopper): super().__init__(source) def decoder_commandline(self, file): - #TODO expose decoding quality parameters through config + # TODO expose decoding quality parameters through config return ["jt9", "--ft4", "-d", "3", file] @@ -189,12 +188,7 @@ class WsjtParser(object): self.dial_freq = None self.band = None - modes = { - "~": "FT8", - "#": "JT65", - "@": "JT9", - "+": "FT4" - } + modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"} def parse(self, data): try: @@ -230,8 +224,8 @@ class WsjtParser(object): dateformat = "%H%M" else: dateformat = "%H%M%S" - timestamp = self.parse_timestamp(msg[0:len(dateformat)], dateformat) - msg = msg[len(dateformat) + 1:] + timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat) + msg = msg[len(dateformat) + 1 :] modeChar = msg[14:15] mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" wsjt_msg = msg[17:53].strip() @@ -242,7 +236,7 @@ class WsjtParser(object): "dt": float(msg[4:8]), "freq": int(msg[9:13]), "mode": mode, - "msg": wsjt_msg + "msg": wsjt_msg, } def parseLocator(self, msg, mode): @@ -268,7 +262,7 @@ class WsjtParser(object): "freq": float(msg[14:24]), "drift": int(msg[25:28]), "mode": "WSPR", - "msg": wsjt_msg + "msg": wsjt_msg, } def parseWsprMessage(self, msg): diff --git a/sdrhu.py b/sdrhu.py index 3060789..87459b3 100755 --- a/sdrhu.py +++ b/sdrhu.py @@ -23,10 +23,9 @@ from owrx.sdrhu import SdrHuUpdater from owrx.config import PropertyManager -if __name__=="__main__": +if __name__ == "__main__": pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") if not "sdrhu_key" in pm: exit(1) SdrHuUpdater().update() -