Reformatted with black -l 120 -t py35 .
This commit is contained in:
		
							
								
								
									
										124
									
								
								config_webrx.py
									
									
									
									
									
								
							
							
						
						
									
										124
									
								
								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.<br /> | ||||
| Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/> | ||||
| Device: %[RX_DEVICE]<br /> | ||||
| @@ -64,18 +64,20 @@ Website: <a href="http://localhost" target="_blank">http://localhost</a> | ||||
| 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 = "" | ||||
|  | ||||
|   | ||||
							
								
								
									
										366
									
								
								csdr.py
									
									
									
									
									
								
							
							
						
						
									
										366
									
								
								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 | ||||
|   | ||||
							
								
								
									
										19
									
								
								openwebrx.py
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								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 <randras@sdr.hu> | ||||
|  | ||||
|     """) | ||||
|     """ | ||||
|     ) | ||||
|  | ||||
|     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 <randras@sdr.hu> | ||||
|         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() | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
							
								
								
									
										25
									
								
								owrx/http.py
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								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: | ||||
|   | ||||
							
								
								
									
										56
									
								
								owrx/map.py
									
									
									
									
									
								
							
							
						
						
									
										56
									
								
								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} | ||||
|   | ||||
							
								
								
									
										30
									
								
								owrx/meta.py
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								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) | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
							
								
								
									
										175
									
								
								owrx/source.py
									
									
									
									
									
								
							
							
						
						
									
										175
									
								
								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() | ||||
|         self.broadcast() | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| openwebrx_version = "v0.18" | ||||
| openwebrx_version = "v0.18" | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
							
								
								
									
										34
									
								
								owrx/wsjt.py
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								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): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 D0han
					D0han