Reformatted with black -l 120 -t py35 .

This commit is contained in:
D0han 2019-07-21 19:40:28 +02:00
parent 79062ff3d6
commit e15dc1ce11
17 changed files with 681 additions and 462 deletions

View File

@ -35,21 +35,21 @@ config_webrx: configuration options for OpenWebRX
# https://github.com/simonyiszk/openwebrx/wiki # https://github.com/simonyiszk/openwebrx/wiki
# ==== Server settings ==== # ==== Server settings ====
web_port=8073 web_port = 8073
max_clients=20 max_clients = 20
# ==== Web GUI configuration ==== # ==== Web GUI configuration ====
receiver_name="[Callsign]" receiver_name = "[Callsign]"
receiver_location="Budapest, Hungary" receiver_location = "Budapest, Hungary"
receiver_qra="JN97ML" receiver_qra = "JN97ML"
receiver_asl=200 receiver_asl = 200
receiver_ant="Longwire" receiver_ant = "Longwire"
receiver_device="RTL-SDR" receiver_device = "RTL-SDR"
receiver_admin="example@example.com" receiver_admin = "example@example.com"
receiver_gps=(47.000000,19.000000) receiver_gps = (47.000000, 19.000000)
photo_height=350 photo_height = 350
photo_title="Panorama of Budapest from Schönherz Zoltán Dormitory" photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
photo_desc=""" photo_desc = """
You can add your own background photo and receiver information.<br /> You can add your own background photo and receiver information.<br />
Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/> Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/>
Device: %[RX_DEVICE]<br /> Device: %[RX_DEVICE]<br />
@ -64,18 +64,20 @@ Website: <a href="http://localhost" target="_blank">http://localhost</a>
sdrhu_key = "" sdrhu_key = ""
# 3. Set this setting to True to enable listing: # 3. Set this setting to True to enable listing:
sdrhu_public_listing = False sdrhu_public_listing = False
server_hostname="localhost" server_hostname = "localhost"
# ==== DSP/RX settings ==== # ==== DSP/RX settings ====
fft_fps=9 fft_fps = 9
fft_size=4096 #Should be power of 2 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_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" audio_compression = "adpcm" # valid values: "adpcm", "none"
fft_compression="adpcm" #valid values: "adpcm", "none" fft_compression = "adpcm" # valid values: "adpcm", "none"
digimodes_enable=True #Decoding digimodes come with higher CPU usage. digimodes_enable = True # Decoding digimodes come with higher CPU usage.
digimodes_fft_size=1024 digimodes_fft_size = 1024
# determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes # 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 # 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, "rf_gain": 30,
"samp_rate": 2400000, "samp_rate": 2400000,
"start_freq": 439275000, "start_freq": 439275000,
"start_mod": "nfm" "start_mod": "nfm",
}, },
"2m": { "2m": {
"name": "2m komplett", "name": "2m komplett",
@ -124,9 +126,9 @@ sdrs = {
"rf_gain": 30, "rf_gain": 30,
"samp_rate": 2400000, "samp_rate": 2400000,
"start_freq": 145725000, "start_freq": 145725000,
"start_mod": "nfm" "start_mod": "nfm",
} },
} },
}, },
"sdrplay": { "sdrplay": {
"name": "SDRPlay RSP2", "name": "SDRPlay RSP2",
@ -134,39 +136,39 @@ sdrs = {
"ppm": 0, "ppm": 0,
"profiles": { "profiles": {
"20m": { "20m": {
"name":"20m", "name": "20m",
"center_freq": 14150000, "center_freq": 14150000,
"rf_gain": 4, "rf_gain": 4,
"samp_rate": 500000, "samp_rate": 500000,
"start_freq": 14070000, "start_freq": 14070000,
"start_mod": "usb", "start_mod": "usb",
"antenna": "Antenna A" "antenna": "Antenna A",
}, },
"30m": { "30m": {
"name":"30m", "name": "30m",
"center_freq": 10125000, "center_freq": 10125000,
"rf_gain": 4, "rf_gain": 4,
"samp_rate": 250000, "samp_rate": 250000,
"start_freq": 10142000, "start_freq": 10142000,
"start_mod": "usb" "start_mod": "usb",
}, },
"40m": { "40m": {
"name":"40m", "name": "40m",
"center_freq": 7100000, "center_freq": 7100000,
"rf_gain": 4, "rf_gain": 4,
"samp_rate": 500000, "samp_rate": 500000,
"start_freq": 7070000, "start_freq": 7070000,
"start_mod": "usb", "start_mod": "usb",
"antenna": "Antenna A" "antenna": "Antenna A",
}, },
"80m": { "80m": {
"name":"80m", "name": "80m",
"center_freq": 3650000, "center_freq": 3650000,
"rf_gain": 4, "rf_gain": 4,
"samp_rate": 500000, "samp_rate": 500000,
"start_freq": 3570000, "start_freq": 3570000,
"start_mod": "usb", "start_mod": "usb",
"antenna": "Antenna A" "antenna": "Antenna A",
}, },
"49m": { "49m": {
"name": "49m Broadcast", "name": "49m Broadcast",
@ -175,42 +177,43 @@ sdrs = {
"samp_rate": 500000, "samp_rate": 500000,
"start_freq": 6070000, "start_freq": 6070000,
"start_mod": "am", "start_mod": "am",
"antenna": "Antenna A" "antenna": "Antenna A",
} },
} },
}, },
# this one is just here to test feature detection # this one is just here to test feature detection
"test": { "test": {"type": "test"},
"type": "test"
}
} }
# ==== Misc settings ==== # ==== Misc settings ====
client_audio_buffer_size = 5 client_audio_buffer_size = 5
#increasing client_audio_buffer_size will: # increasing client_audio_buffer_size will:
# - also increase the latency # - also increase the latency
# - decrease the chance of audio underruns # - 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 ==== # ==== 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: ### default theme by teejez:
waterfall_colors = [0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff] waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF]
waterfall_min_level = -88 #in dB waterfall_min_level = -88 # in dB
waterfall_max_level = -20 waterfall_max_level = -20
waterfall_auto_level_margin = (5, 40) waterfall_auto_level_margin = (5, 40)
### old theme by HA7ILM: ### old theme by HA7ILM:
#waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" # waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
#waterfall_min_level = -115 #in dB # waterfall_min_level = -115 #in dB
#waterfall_max_level = 0 # waterfall_max_level = 0
#waterfall_auto_level_margin = (20, 30) # waterfall_auto_level_margin = (20, 30)
##For the old colors, you might also want to set [fft_voverlap_factor] to 0. ##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_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]]
# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]] # [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 __| # current_max_power_level __|
# 3D view settings # 3D view settings
mathbox_waterfall_frequency_resolution = 128 #bins mathbox_waterfall_frequency_resolution = 128 # bins
mathbox_waterfall_history_length = 10 #seconds mathbox_waterfall_history_length = 10 # seconds
mathbox_waterfall_colors = [0x000000ff,0x2e6893ff,0x69a5d0ff,0x214b69ff,0x9dc4e0ff,0xfff775ff,0xff8a8aff,0xb20000ff] mathbox_waterfall_colors = [
0x000000FF,
0x2E6893FF,
0x69A5D0FF,
0x214B69FF,
0x9DC4E0FF,
0xFFF775FF,
0xFF8A8AFF,
0xB20000FF,
]
# === Experimental settings === # === Experimental settings ===
#Warning! The settings below are very experimental. # Warning! The settings below are very experimental.
csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr. 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_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 = "" google_maps_api_key = ""

366
csdr.py
View File

@ -28,26 +28,29 @@ from functools import partial
from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class output(object): class output(object):
def add_output(self, type, read_fn): def add_output(self, type, read_fn):
pass pass
def reset(self): def reset(self):
pass pass
class dsp(object):
class dsp(object):
def __init__(self, output): def __init__(self, output):
self.samp_rate = 250000 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_size = 1024
self.fft_fps = 5 self.fft_fps = 5
self.offset_freq = 0 self.offset_freq = 0
self.low_cut = -4000 self.low_cut = -4000
self.high_cut = 4000 self.high_cut = 4000
self.bpf_transition_bw = 320 #Hz, and this is a constant self.bpf_transition_bw = 320 # Hz, and this is a constant
self.ddc_transition_bw_rate = 0.15 # of the IF sample rate self.ddc_transition_bw_rate = 0.15 # of the IF sample rate
self.running = False self.running = False
self.secondary_processes_running = False self.secondary_processes_running = False
self.audio_compression = "none" self.audio_compression = "none"
@ -67,9 +70,17 @@ class dsp(object):
self.secondary_fft_size = 1024 self.secondary_fft_size = 1024
self.secondary_process_fft = None self.secondary_process_fft = None
self.secondary_process_demod = None self.secondary_process_demod = None
self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", self.pipe_names = [
"iqtee2_pipe", "dmr_control_pipe"] "bpf_pipe",
self.secondary_pipe_names=["secondary_shift_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.secondary_offset_freq = 1000
self.unvoiced_quality = 1 self.unvoiced_quality = 1
self.modification_lock = threading.Lock() self.modification_lock = threading.Lock()
@ -79,15 +90,19 @@ class dsp(object):
def set_temporary_directory(self, what): def set_temporary_directory(self, what):
self.temporary_directory = what self.temporary_directory = what
def chain(self,which): def chain(self, which):
chain = ["nc -v 127.0.0.1 {nc_port}"] chain = ["nc -v 127.0.0.1 {nc_port}"]
if self.csdr_dynamic_bufsize: chain += ["csdr setbuf {start_bufsize}"] if self.csdr_dynamic_bufsize:
if self.csdr_through: chain += ["csdr through"] chain += ["csdr setbuf {start_bufsize}"]
if self.csdr_through:
chain += ["csdr through"]
if which == "fft": if which == "fft":
chain += [ chain += [
"csdr fft_cc {fft_size} {fft_block_size}", "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 logpower_cf -70"
"csdr fft_exchange_sides_ff {fft_size}" 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": if self.fft_compression == "adpcm":
chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"] chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"]
@ -96,37 +111,24 @@ class dsp(object):
"csdr shift_addition_cc --fifo {shift_pipe}", "csdr shift_addition_cc --fifo {shift_pipe}",
"csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING", "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING",
"csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_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: if self.secondary_demodulator:
chain += [ chain += ["csdr tee {iqtee_pipe}", "csdr tee {iqtee2_pipe}"]
"csdr tee {iqtee_pipe}",
"csdr tee {iqtee2_pipe}"
]
# safe some cpu cycles... no need to decimate if decimation factor is 1 # 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": if which == "nfm":
chain += [ chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"]
"csdr fmdemod_quadri_cf",
"csdr limit_ff"
]
chain += last_decimation_block chain += last_decimation_block
chain += [ chain += ["csdr deemphasis_nfm_ff {output_rate}", "csdr convert_f_s16"]
"csdr deemphasis_nfm_ff {output_rate}",
"csdr convert_f_s16"
]
elif self.isDigitalVoice(which): elif self.isDigitalVoice(which):
chain += [ chain += ["csdr fmdemod_quadri_cf", "dc_block "]
"csdr fmdemod_quadri_cf",
"dc_block "
]
chain += last_decimation_block chain += last_decimation_block
# dsd modes # dsd modes
if which in [ "dstar", "nxdn" ]: if which in ["dstar", "nxdn"]:
chain += [ chain += ["csdr limit_ff", "csdr convert_f_s16"]
"csdr limit_ff",
"csdr convert_f_s16"
]
if which == "dstar": if which == "dstar":
chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "] chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "]
elif which == "nxdn": elif which == "nxdn":
@ -135,44 +137,28 @@ class dsp(object):
max_gain = 5 max_gain = 5
# digiham modes # digiham modes
else: else:
chain += [ chain += ["rrc_filter", "gfsk_demodulator"]
"rrc_filter",
"gfsk_demodulator"
]
if which == "dmr": if which == "dmr":
chain += [ chain += [
"dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe}", "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": elif which == "ysf":
chain += [ chain += ["ysf_decoder --fifo {meta_pipe}", "mbe_synthesizer -y -f -u {unvoiced_quality}"]
"ysf_decoder --fifo {meta_pipe}",
"mbe_synthesizer -y -f -u {unvoiced_quality}"
]
max_gain = 0.0005 max_gain = 0.0005
chain += [ chain += [
"digitalvoice_filter -f", "digitalvoice_filter -f",
"CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain}".format(max_gain=max_gain), "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": elif which == "am":
chain += [ chain += ["csdr amdemod_cf", "csdr fastdcblock_ff"]
"csdr amdemod_cf",
"csdr fastdcblock_ff"
]
chain += last_decimation_block chain += last_decimation_block
chain += [ chain += ["csdr agc_ff", "csdr limit_ff", "csdr convert_f_s16"]
"csdr agc_ff",
"csdr limit_ff",
"csdr convert_f_s16"
]
elif which == "ssb": elif which == "ssb":
chain += ["csdr realpart_cf"] chain += ["csdr realpart_cf"]
chain += last_decimation_block chain += last_decimation_block
chain += [ chain += ["csdr agc_ff", "csdr limit_ff"]
"csdr agc_ff",
"csdr limit_ff"
]
# fixed sample rate necessary for the wsjt-x tools. fix with sox... # fixed sample rate necessary for the wsjt-x tools. fix with sox...
if self.isWsjtMode() and self.get_audio_rate() != self.get_output_rate(): if self.isWsjtMode() and self.get_audio_rate() != self.get_output_rate():
chain += [ chain += [
@ -181,24 +167,31 @@ class dsp(object):
else: else:
chain += ["csdr convert_f_s16"] chain += ["csdr convert_f_s16"]
if self.audio_compression=="adpcm": if self.audio_compression == "adpcm":
chain += ["csdr encode_ima_adpcm_i16_u8"] chain += ["csdr encode_ima_adpcm_i16_u8"]
return chain return chain
def secondary_chain(self, which): def secondary_chain(self, which):
secondary_chain_base="cat {input_pipe} | " secondary_chain_base = "cat {input_pipe} | "
if which == "fft": 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": elif which == "bpsk31":
return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \ return (
"csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " + \ secondary_chain_base
"csdr simple_agc_cc 0.001 0.5 | " + \ + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | "
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ + "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | "
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ + "csdr simple_agc_cc 0.001 0.5 | "
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" + "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): elif self.isWsjtMode(which):
chain = secondary_chain_base + "csdr realpart_cf | " 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 fractional_decimator_ff {last_decimation} | "
chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16"
return chain return chain
@ -211,14 +204,16 @@ class dsp(object):
self.restart() self.restart()
def secondary_fft_block_size(self): 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): def secondary_decimation(self):
return 1 #currently unused return 1 # currently unused
def secondary_bpf_cutoff(self): def secondary_bpf_cutoff(self):
if self.secondary_demodulator == "bpsk31": if self.secondary_demodulator == "bpsk31":
return 31.25 / self.if_samp_rate() return 31.25 / self.if_samp_rate()
return 0 return 0
def secondary_bpf_transition_bw(self): def secondary_bpf_transition_bw(self):
@ -228,7 +223,7 @@ class dsp(object):
def secondary_samples_per_bits(self): def secondary_samples_per_bits(self):
if self.secondary_demodulator == "bpsk31": 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 return 0
def secondary_bw(self): def secondary_bw(self):
@ -236,19 +231,20 @@ class dsp(object):
return 31.25 return 31.25
def start_secondary_demodulator(self): def start_secondary_demodulator(self):
if not self.secondary_demodulator: return if not self.secondary_demodulator:
logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate()) return
secondary_command_fft=self.secondary_chain("fft") logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate())
secondary_command_demod=self.secondary_chain(self.secondary_demodulator) 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) 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, input_pipe=self.iqtee_pipe,
secondary_fft_input_size=self.secondary_fft_size, secondary_fft_input_size=self.secondary_fft_size,
secondary_fft_size=self.secondary_fft_size, secondary_fft_size=self.secondary_fft_size,
secondary_fft_block_size=self.secondary_fft_block_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, input_pipe=self.iqtee2_pipe,
secondary_shift_pipe=self.secondary_shift_pipe, secondary_shift_pipe=self.secondary_shift_pipe,
secondary_decimation=self.secondary_decimation(), secondary_decimation=self.secondary_decimation(),
@ -256,21 +252,29 @@ class dsp(object):
secondary_bpf_cutoff=self.secondary_bpf_cutoff(), secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(),
if_samp_rate=self.if_samp_rate(), 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 (fft) = %s", secondary_command_fft)
logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (demod) = %s", secondary_command_demod) logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (demod) = %s", secondary_command_demod)
my_env=os.environ.copy() my_env = os.environ.copy()
#if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; # if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; if self.csdr_print_bufsizes:
self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) 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)") 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 self.secondary_process_demod = subprocess.Popen(
logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") #TODO digimodes 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.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(): if self.isWsjtMode():
smd = self.get_secondary_demodulator() smd = self.get_secondary_demodulator()
if smd == "ft8": if smd == "ft8":
@ -288,19 +292,20 @@ class dsp(object):
else: else:
self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
#open control pipes for csdr and send initialization data # open control pipes for csdr and send initialization data
if self.secondary_shift_pipe != None: #TODO digimodes if self.secondary_shift_pipe != None: # TODO digimodes
self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #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 self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes
def set_secondary_offset_freq(self, value): 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"): 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() self.secondary_shift_pipe_file.flush()
def stop_secondary_demodulator(self): 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) self.try_delete_pipes(self.secondary_pipe_names)
if self.secondary_process_fft: if self.secondary_process_fft:
try: try:
@ -319,42 +324,47 @@ class dsp(object):
def get_secondary_demodulator(self): def get_secondary_demodulator(self):
return self.secondary_demodulator return self.secondary_demodulator
def set_secondary_fft_size(self,secondary_fft_size): def set_secondary_fft_size(self, secondary_fft_size):
#to change this, restart is required # to change this, restart is required
self.secondary_fft_size=secondary_fft_size self.secondary_fft_size = secondary_fft_size
def set_audio_compression(self,what): def set_audio_compression(self, what):
self.audio_compression = what self.audio_compression = what
def set_fft_compression(self,what): def set_fft_compression(self, what):
self.fft_compression = what self.fft_compression = what
def get_fft_bytes_to_read(self): def get_fft_bytes_to_read(self):
if self.fft_compression=="none": return self.fft_size*4 if self.fft_compression == "none":
if self.fft_compression=="adpcm": return (self.fft_size/2)+(10/2) 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): def get_secondary_fft_bytes_to_read(self):
if self.fft_compression=="none": return self.secondary_fft_size*4 if self.fft_compression == "none":
if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2) 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): def set_samp_rate(self, samp_rate):
self.samp_rate=samp_rate self.samp_rate = samp_rate
self.calculate_decimation() self.calculate_decimation()
if self.running: self.restart() if self.running:
self.restart()
def calculate_decimation(self): def calculate_decimation(self):
(self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate()) (self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate())
def get_decimation(self, input_rate, output_rate): def get_decimation(self, input_rate, output_rate):
decimation=1 decimation = 1
while input_rate / (decimation+1) >= output_rate: while input_rate / (decimation + 1) >= output_rate:
decimation += 1 decimation += 1
fraction = float(input_rate / decimation) / output_rate fraction = float(input_rate / decimation) / output_rate
intermediate_rate = input_rate / decimation intermediate_rate = input_rate / decimation
return (decimation, fraction, intermediate_rate) return (decimation, fraction, intermediate_rate)
def if_samp_rate(self): def if_samp_rate(self):
return self.samp_rate/self.decimation return self.samp_rate / self.decimation
def get_name(self): def get_name(self):
return self.name return self.name
@ -369,59 +379,64 @@ class dsp(object):
return 12000 return 12000
return self.get_output_rate() return self.get_output_rate()
def isDigitalVoice(self, demodulator = None): def isDigitalVoice(self, demodulator=None):
if demodulator is None: if demodulator is None:
demodulator = self.get_demodulator() demodulator = self.get_demodulator()
return demodulator in ["dmr", "dstar", "nxdn", "ysf"] return demodulator in ["dmr", "dstar", "nxdn", "ysf"]
def isWsjtMode(self, demodulator = None): def isWsjtMode(self, demodulator=None):
if demodulator is None: if demodulator is None:
demodulator = self.get_secondary_demodulator() demodulator = self.get_secondary_demodulator()
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"] return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"]
def set_output_rate(self,output_rate): def set_output_rate(self, output_rate):
self.output_rate=output_rate self.output_rate = output_rate
self.calculate_decimation() self.calculate_decimation()
def set_demodulator(self,demodulator): def set_demodulator(self, demodulator):
if (self.demodulator == demodulator): return if self.demodulator == demodulator:
self.demodulator=demodulator return
self.demodulator = demodulator
self.calculate_decimation() self.calculate_decimation()
self.restart() self.restart()
def get_demodulator(self): def get_demodulator(self):
return self.demodulator return self.demodulator
def set_fft_size(self,fft_size): def set_fft_size(self, fft_size):
self.fft_size=fft_size self.fft_size = fft_size
self.restart() self.restart()
def set_fft_fps(self,fft_fps): def set_fft_fps(self, fft_fps):
self.fft_fps=fft_fps self.fft_fps = fft_fps
self.restart() self.restart()
def set_fft_averages(self,fft_averages): def set_fft_averages(self, fft_averages):
self.fft_averages=fft_averages self.fft_averages = fft_averages
self.restart() self.restart()
def fft_block_size(self): def fft_block_size(self):
if self.fft_averages == 0: return self.samp_rate/self.fft_fps if self.fft_averages == 0:
else: return self.samp_rate/self.fft_fps/self.fft_averages 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): def set_offset_freq(self, offset_freq):
self.offset_freq=offset_freq self.offset_freq = offset_freq
if self.running: if self.running:
self.modification_lock.acquire() 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.shift_pipe_file.flush()
self.modification_lock.release() self.modification_lock.release()
def set_bpf(self,low_cut,high_cut): def set_bpf(self, low_cut, high_cut):
self.low_cut=low_cut self.low_cut = low_cut
self.high_cut=high_cut self.high_cut = high_cut
if self.running: if self.running:
self.modification_lock.acquire() 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.bpf_pipe_file.flush()
self.modification_lock.release() self.modification_lock.release()
@ -429,12 +444,12 @@ class dsp(object):
return [self.low_cut, self.high_cut] return [self.low_cut, self.high_cut]
def set_squelch_level(self, squelch_level): def set_squelch_level(self, squelch_level):
self.squelch_level=squelch_level self.squelch_level = squelch_level
#no squelch required on digital voice modes # no squelch required on digital voice modes
actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level
if self.running: if self.running:
self.modification_lock.acquire() 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.squelch_pipe_file.flush()
self.modification_lock.release() 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.write("{0}\n".format(filter))
self.dmr_control_pipe_file.flush() self.dmr_control_pipe_file.flush()
def mkfifo(self,path): def mkfifo(self, path):
try: try:
os.unlink(path) os.unlink(path)
except: except:
@ -458,27 +473,28 @@ class dsp(object):
os.mkfifo(path) os.mkfifo(path)
def ddc_transition_bw(self): 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): def try_create_pipes(self, pipe_names, command_base):
for pipe_name in pipe_names: for pipe_name in pipe_names:
if "{"+pipe_name+"}" in command_base: if "{" + pipe_name + "}" in command_base:
setattr(self, pipe_name, self.pipe_base_path+pipe_name) setattr(self, pipe_name, self.pipe_base_path + pipe_name)
self.mkfifo(getattr(self, pipe_name)) self.mkfifo(getattr(self, pipe_name))
else: else:
setattr(self, pipe_name, None) setattr(self, pipe_name, None)
def try_delete_pipes(self, pipe_names): def try_delete_pipes(self, pipe_names):
for pipe_name in 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: if pipe_path:
try: os.unlink(pipe_path) try:
os.unlink(pipe_path)
except Exception: except Exception:
logger.exception("try_delete_pipes()") logger.exception("try_delete_pipes()")
def start(self): def start(self):
self.modification_lock.acquire() self.modification_lock.acquire()
if (self.running): if self.running:
self.modification_lock.release() self.modification_lock.release()
return return
self.running = True self.running = True
@ -486,37 +502,58 @@ class dsp(object):
command_base = " | ".join(self.chain(self.demodulator)) command_base = " | ".join(self.chain(self.demodulator))
logger.debug(command_base) 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.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) self.try_create_pipes(self.pipe_names, command_base)
#run the command # run the command
command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, command = command_base.format(
last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, bpf_pipe=self.bpf_pipe,
bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), shift_pipe=self.shift_pipe,
flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, decimation=self.decimation,
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, last_decimation=self.last_decimation,
output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000), fft_size=self.fft_size,
unvoiced_quality = self.get_unvoiced_quality(), dmr_control_pipe = self.dmr_control_pipe, fft_block_size=self.fft_block_size(),
audio_rate = self.get_audio_rate()) 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) logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command)
my_env=os.environ.copy() my_env = os.environ.copy()
if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; if self.csdr_dynamic_bufsize:
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1"
if self.csdr_print_bufsizes:
my_env["CSDR_PRINT_BUFSIZES"] = "1"
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
def watch_thread(): def watch_thread():
rc = self.process.wait() rc = self.process.wait()
logger.debug("dsp thread ended with rc=%d", rc) 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") logger.debug("restarting since rc = 0, self.running = true, and no modification")
self.restart() 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 # open control pipes for csdr
if self.bpf_pipe: if self.bpf_pipe:
@ -537,23 +574,27 @@ class dsp(object):
if self.bpf_pipe: if self.bpf_pipe:
self.set_bpf(self.low_cut, self.high_cut) self.set_bpf(self.low_cut, self.high_cut)
if self.smeter_pipe: 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(): def read_smeter():
raw = self.smeter_pipe_file.readline() raw = self.smeter_pipe_file.readline()
if len(raw) == 0: if len(raw) == 0:
return None return None
else: else:
return float(raw.rstrip("\n")) return float(raw.rstrip("\n"))
self.output.add_output("smeter", read_smeter) self.output.add_output("smeter", read_smeter)
if self.meta_pipe != None: if self.meta_pipe != None:
# TODO make digiham output unicode and then change this here # 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(): def read_meta():
raw = self.meta_pipe_file.readline() raw = self.meta_pipe_file.readline()
if len(raw) == 0: if len(raw) == 0:
return None return None
else: else:
return raw.rstrip("\n") return raw.rstrip("\n")
self.output.add_output("meta", read_meta) self.output.add_output("meta", read_meta)
if self.dmr_control_pipe: if self.dmr_control_pipe:
@ -575,10 +616,11 @@ class dsp(object):
self.modification_lock.release() self.modification_lock.release()
def restart(self): def restart(self):
if not self.running: return if not self.running:
return
self.stop() self.stop()
self.start() self.start()
def __del__(self): def __del__(self):
self.stop() self.stop()
del(self.process) del self.process

View File

@ -1,13 +1,14 @@
from http.server import HTTPServer from http.server import HTTPServer
from owrx.http import RequestHandler from owrx.http import RequestHandler
from owrx.config import PropertyManager from owrx.config import PropertyManager
from owrx.feature import FeatureDetector from owrx.feature import FeatureDetector
from owrx.source import SdrService, ClientRegistry from owrx.source import SdrService, ClientRegistry
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
from owrx.sdrhu import SdrHuUpdater from owrx.sdrhu import SdrHuUpdater
import logging 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): class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
@ -15,21 +16,25 @@ class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
def main(): def main():
print(""" print(
"""
OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package 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> Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>
""") """
)
pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
featureDetector = FeatureDetector() featureDetector = FeatureDetector()
if not featureDetector.is_available("core"): if not featureDetector.is_available("core"):
print("you are missing required dependencies to run openwebrx. " print(
"please check that the following core requirements are installed:") "you are missing required dependencies to run openwebrx. "
"please check that the following core requirements are installed:"
)
print(", ".join(featureDetector.get_requirements("core"))) print(", ".join(featureDetector.get_requirements("core")))
return return
@ -40,7 +45,7 @@ Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>
updater = SdrHuUpdater() updater = SdrHuUpdater()
updater.start() 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() server.serve_forever()

View File

@ -1,6 +1,7 @@
import json import json
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -16,7 +17,11 @@ class Band(object):
freqs = [freqs] freqs = [freqs]
for f in freqs: for f in freqs:
if not self.inBand(f): 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: else:
self.frequencies.append({"mode": mode, "frequency": f}) self.frequencies.append({"mode": mode, "frequency": f})
@ -33,6 +38,7 @@ class Band(object):
class Bandplan(object): class Bandplan(object):
sharedInstance = None sharedInstance = None
@staticmethod @staticmethod
def getSharedInstance(): def getSharedInstance():
if Bandplan.sharedInstance is None: if Bandplan.sharedInstance is None:

View File

@ -1,4 +1,5 @@
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -15,7 +16,7 @@ class Subscription(object):
class Property(object): class Property(object):
def __init__(self, value = None): def __init__(self, value=None):
self.value = value self.value = value
self.subscribers = [] self.subscribers = []
@ -23,7 +24,7 @@ class Property(object):
return self.value return self.value
def setValue(self, value): def setValue(self, value):
if (self.value == value): if self.value == value:
return self return self
self.value = value self.value = value
for c in self.subscribers: for c in self.subscribers:
@ -36,7 +37,8 @@ class Property(object):
def wire(self, callback): def wire(self, callback):
sub = Subscription(self, callback) sub = Subscription(self, callback)
self.subscribers.append(sub) 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 return sub
def unwire(self, sub): def unwire(self, sub):
@ -47,8 +49,10 @@ class Property(object):
pass pass
return self return self
class PropertyManager(object): class PropertyManager(object):
sharedInstance = None sharedInstance = None
@staticmethod @staticmethod
def getSharedInstance(): def getSharedInstance():
if PropertyManager.sharedInstance is None: if PropertyManager.sharedInstance is None:
@ -56,9 +60,11 @@ class PropertyManager(object):
return PropertyManager.sharedInstance return PropertyManager.sharedInstance
def collect(self, *props): 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.properties = {}
self.subscribers = [] self.subscribers = []
if properties is not None: if properties is not None:
@ -67,12 +73,14 @@ class PropertyManager(object):
def add(self, name, prop): def add(self, name, prop):
self.properties[name] = prop self.properties[name] = prop
def fireCallbacks(value): def fireCallbacks(value):
for c in self.subscribers: for c in self.subscribers:
try: try:
c.call(name, value) c.call(name, value)
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
prop.wire(fireCallbacks) prop.wire(fireCallbacks)
return self return self
@ -88,7 +96,7 @@ class PropertyManager(object):
self.getProperty(name).setValue(value) self.getProperty(name).setValue(value)
def __dict__(self): 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): def hasProperty(self, name):
return name in self.properties return name in self.properties

View File

@ -7,6 +7,7 @@ import json
from owrx.map import Map from owrx.map import Map
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,11 +30,26 @@ class Client(object):
class OpenWebRxReceiverClient(Client): class OpenWebRxReceiverClient(Client):
config_keys = ["waterfall_colors", "waterfall_min_level", "waterfall_max_level", config_keys = [
"waterfall_auto_level_margin", "lfo_offset", "samp_rate", "fft_size", "fft_fps", "waterfall_colors",
"audio_compression", "fft_compression", "max_clients", "start_mod", "waterfall_min_level",
"client_audio_buffer_size", "start_freq", "center_freq", "mathbox_waterfall_colors", "waterfall_max_level",
"mathbox_waterfall_history_length", "mathbox_waterfall_frequency_resolution"] "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): def __init__(self, conn):
super().__init__(conn) super().__init__(conn)
@ -49,12 +65,23 @@ class OpenWebRxReceiverClient(Client):
self.setSdr() self.setSdr()
# send receiver info # send receiver info
receiver_keys = ["receiver_name", "receiver_location", "receiver_qra", "receiver_asl", "receiver_gps", receiver_keys = [
"photo_title", "photo_desc"] "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) receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys)
self.write_receiver_details(receiver_details) 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) self.write_profiles(profiles)
features = FeatureDetector().feature_availability() features = FeatureDetector().feature_availability()
@ -62,9 +89,9 @@ class OpenWebRxReceiverClient(Client):
CpuUsageThread.getSharedInstance().add_client(self) CpuUsageThread.getSharedInstance().add_client(self)
def setSdr(self, id = None): def setSdr(self, id=None):
next = SdrService.getSource(id) next = SdrService.getSource(id)
if (next == self.sdr): if next == self.sdr:
return return
self.stopDsp() self.stopDsp()
@ -76,7 +103,11 @@ class OpenWebRxReceiverClient(Client):
self.sdr = next self.sdr = next
# send initial config # 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): def sendConfig(key, value):
config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys) config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys)
@ -89,7 +120,6 @@ class OpenWebRxReceiverClient(Client):
frequencyRange = (cf - srh, cf + srh) frequencyRange = (cf - srh, cf + srh)
self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencis(frequencyRange)) self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencis(frequencyRange))
self.configSub = configProps.wire(sendConfig) self.configSub = configProps.wire(sendConfig)
sendConfig(None, None) sendConfig(None, None)
@ -118,8 +148,11 @@ class OpenWebRxReceiverClient(Client):
def setParams(self, params): def setParams(self, params):
# only the keys in the protected property manager can be overridden from the web # 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()) .defaults(PropertyManager.getSharedInstance())
)
for key, value in params.items(): for key, value in params.items():
protected[key] = value protected[key] = value
@ -134,13 +167,13 @@ class OpenWebRxReceiverClient(Client):
self.protected_send(bytes([0x02]) + data) self.protected_send(bytes([0x02]) + data)
def write_s_meter_level(self, level): 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): 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): 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): def write_secondary_fft(self, data):
self.protected_send(bytes([0x03]) + data) self.protected_send(bytes([0x03]) + data)
@ -149,22 +182,22 @@ class OpenWebRxReceiverClient(Client):
self.protected_send(bytes([0x04]) + data) self.protected_send(bytes([0x04]) + data)
def write_secondary_dsp_config(self, cfg): 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): 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): 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): def write_profiles(self, profiles):
self.protected_send({"type":"profiles","value":profiles}) self.protected_send({"type": "profiles", "value": profiles})
def write_features(self, features): def write_features(self, features):
self.protected_send({"type":"features","value":features}) self.protected_send({"type": "features", "value": features})
def write_metadata(self, metadata): 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): def write_wsjt_message(self, message):
self.protected_send({"type": "wsjt_message", "value": message}) self.protected_send({"type": "wsjt_message", "value": message})
@ -187,10 +220,11 @@ class MapConnection(Client):
super().close() super().close()
def write_config(self, cfg): def write_config(self, cfg):
self.protected_send({"type":"config","value":cfg}) self.protected_send({"type": "config", "value": cfg})
def write_update(self, update): def write_update(self, update):
self.protected_send({"type":"update","value":update}) self.protected_send({"type": "update", "value": update})
class WebSocketMessageHandler(object): class WebSocketMessageHandler(object):
def __init__(self): def __init__(self):
@ -199,11 +233,11 @@ class WebSocketMessageHandler(object):
self.dsp = None self.dsp = None
def handleTextMessage(self, conn, message): def handleTextMessage(self, conn, message):
if (message[:16] == "SERVER DE CLIENT"): if message[:16] == "SERVER DE CLIENT":
meta = message[17:].split(" ") meta = message[17:].split(" ")
self.handshake = {v[0]: "=".join(v[1:]) for v in map(lambda x: x.split("="), meta)} 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") logger.debug("client connection intitialized")
if "type" in self.handshake: if "type" in self.handshake:

View File

@ -11,13 +11,16 @@ from owrx.version import openwebrx_version
from owrx.feature import FeatureDetector from owrx.feature import FeatureDetector
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Controller(object): class Controller(object):
def __init__(self, handler, request): def __init__(self, handler, request):
self.handler = handler self.handler = handler
self.request = request 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) self.handler.send_response(code)
if content_type is not None: if content_type is not None:
self.handler.send_header("Content-Type", content_type) self.handler.send_header("Content-Type", content_type)
@ -26,7 +29,7 @@ class Controller(object):
if max_age is not None: if max_age is not None:
self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age)) self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age))
self.handler.end_headers() self.handler.end_headers()
if (type(content) == str): if type(content) == str:
content = content.encode() content = content.encode()
self.handler.wfile.write(content) self.handler.wfile.write(content)
@ -45,44 +48,49 @@ class StatusController(Controller):
"asl": pm["receiver_asl"], "asl": pm["receiver_asl"],
"loc": pm["receiver_location"], "loc": pm["receiver_location"],
"sw_version": openwebrx_version, "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): class AssetsController(Controller):
def serve_file(self, file, content_type = None): def serve_file(self, file, content_type=None):
try: 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: 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: if modified <= client_modified:
self.send_response("", code = 304) self.send_response("", code=304)
return return
f = open('htdocs/' + file, 'rb') f = open("htdocs/" + file, "rb")
data = f.read() data = f.read()
f.close() f.close()
if content_type is None: if content_type is None:
(content_type, encoding) = mimetypes.MimeTypes().guess_type(file) (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: except FileNotFoundError:
self.send_response("file not found", code = 404) self.send_response("file not found", code=404)
def handle_request(self): def handle_request(self):
filename = self.request.matches.group(1) filename = self.request.matches.group(1)
self.serve_file(filename) self.serve_file(filename)
class TemplateController(Controller): class TemplateController(Controller):
def render_template(self, file, **vars): def render_template(self, file, **vars):
f = open('htdocs/' + file, 'r') f = open("htdocs/" + file, "r")
template = Template(f.read()) template = Template(f.read())
f.close() f.close()
return template.safe_substitute(**vars) return template.safe_substitute(**vars)
def serve_template(self, file, **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): def default_variables(self):
return {} return {}
@ -90,8 +98,8 @@ class TemplateController(Controller):
class WebpageController(TemplateController): class WebpageController(TemplateController):
def template_variables(self): def template_variables(self):
header = self.render_template('include/header.include.html') header = self.render_template("include/header.include.html")
return { "header": header } return {"header": header}
class IndexController(WebpageController): class IndexController(WebpageController):
@ -101,17 +109,20 @@ class IndexController(WebpageController):
class MapController(WebpageController): class MapController(WebpageController):
def handle_request(self): 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()) self.serve_template("map.html", **self.template_variables())
class FeatureController(WebpageController): class FeatureController(WebpageController):
def handle_request(self): def handle_request(self):
self.serve_template("features.html", **self.template_variables()) self.serve_template("features.html", **self.template_variables())
class ApiController(Controller): class ApiController(Controller):
def handle_request(self): def handle_request(self):
data = json.dumps(FeatureDetector().feature_report()) 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): class WebSocketController(Controller):
def handle_request(self): def handle_request(self):

View File

@ -7,6 +7,7 @@ from distutils.version import LooseVersion
import inspect import inspect
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -16,14 +17,14 @@ class UnknownFeatureException(Exception):
class FeatureDetector(object): class FeatureDetector(object):
features = { features = {
"core": [ "csdr", "nmux", "nc" ], "core": ["csdr", "nmux", "nc"],
"rtl_sdr": [ "rtl_sdr" ], "rtl_sdr": ["rtl_sdr"],
"sdrplay": [ "rx_tools" ], "sdrplay": ["rx_tools"],
"hackrf": [ "hackrf_transfer" ], "hackrf": ["hackrf_transfer"],
"airspy": [ "airspy_rx" ], "airspy": ["airspy_rx"],
"digital_voice_digiham": [ "digiham", "sox" ], "digital_voice_digiham": ["digiham", "sox"],
"digital_voice_dsd": [ "dsd", "sox", "digiham" ], "digital_voice_dsd": ["dsd", "sox", "digiham"],
"wsjt-x": [ "wsjtx", "sox" ] "wsjt-x": ["wsjtx", "sox"],
} }
def feature_availability(self): def feature_availability(self):
@ -36,14 +37,14 @@ class FeatureDetector(object):
"available": available, "available": available,
# as of now, features are always enabled as soon as they are available. this may change in the future. # as of now, features are always enabled as soon as they are available. this may change in the future.
"enabled": available, "enabled": available,
"description": self.get_requirement_description(name) "description": self.get_requirement_description(name),
} }
def feature_details(name): def feature_details(name):
return { return {
"description": "", "description": "",
"available": self.is_available(name), "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} return {name: feature_details(name) for name in FeatureDetector.features}
@ -55,7 +56,7 @@ class FeatureDetector(object):
try: try:
return FeatureDetector.features[feature] return FeatureDetector.features[feature]
except KeyError: 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): def has_requirements(self, requirements):
passed = True 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 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. 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): def has_rtl_sdr(self):
""" """
@ -156,7 +157,8 @@ class FeatureDetector(object):
""" """
required_version = LooseVersion("0.2") required_version = LooseVersion("0.2")
digiham_version_regex = re.compile('^digiham version (.*)$') digiham_version_regex = re.compile("^digiham version (.*)$")
def check_digiham_version(command): def check_digiham_version(command):
try: try:
process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE) process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE)
@ -165,14 +167,21 @@ class FeatureDetector(object):
return version >= required_version return version >= required_version
except FileNotFoundError: except FileNotFoundError:
return False return False
return reduce( return reduce(
and_, and_,
map( map(
check_digiham_version, 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): 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 [WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions
on how to build from source. on how to build from source.
""" """
return reduce( return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True)
and_,
map(
self.command_is_runnable,
["jt9", "wsprd"]
),
True
)

View File

@ -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 from http.server import BaseHTTPRequestHandler
import re import re
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RequestHandler(BaseHTTPRequestHandler): class RequestHandler(BaseHTTPRequestHandler):
def __init__(self, request, client_address, server): def __init__(self, request, client_address, server):
self.router = Router() self.router = Router()
super().__init__(request, client_address, server) super().__init__(request, client_address, server)
def do_GET(self): def do_GET(self):
self.router.route(self) self.router.route(self)
class Request(object): class Request(object):
def __init__(self, query = None, matches = None): def __init__(self, query=None, matches=None):
self.query = query self.query = query
self.matches = matches self.matches = matches
class Router(object): class Router(object):
mappings = [ mappings = [
{"route": "/", "controller": IndexController}, {"route": "/", "controller": IndexController},
@ -29,8 +42,9 @@ class Router(object):
{"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController}, {"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController},
{"route": "/map", "controller": MapController}, {"route": "/map", "controller": MapController},
{"route": "/features", "controller": FeatureController}, {"route": "/features", "controller": FeatureController},
{"route": "/api/features", "controller": ApiController} {"route": "/api/features", "controller": ApiController},
] ]
def find_controller(self, path): def find_controller(self, path):
for m in Router.mappings: for m in Router.mappings:
if "route" in m: if "route" in m:
@ -41,13 +55,16 @@ class Router(object):
matches = regex.match(path) matches = regex.match(path)
if matches: if matches:
return (m["controller"], matches) return (m["controller"], matches)
def route(self, handler): def route(self, handler):
url = urlparse(handler.path) url = urlparse(handler.path)
res = self.find_controller(url.path) res = self.find_controller(url.path)
if res is not None: if res is not None:
(controller, matches) = res (controller, matches) = res
query = parse_qs(url.query) 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) request = Request(query, matches)
controller(handler, request).handle_request() controller(handler, request).handle_request()
else: else:

View File

@ -4,6 +4,7 @@ from owrx.config import PropertyManager
from owrx.bands import Band from owrx.bands import Band
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -14,6 +15,7 @@ class Location(object):
class Map(object): class Map(object):
sharedInstance = None sharedInstance = None
@staticmethod @staticmethod
def getSharedInstance(): def getSharedInstance():
if Map.sharedInstance is None: if Map.sharedInstance is None:
@ -41,16 +43,18 @@ class Map(object):
def addClient(self, client): def addClient(self, client):
self.clients.append(client) self.clients.append(client)
client.write_update([ client.write_update(
{ [
"callsign": callsign, {
"location": record["location"].__dict__(), "callsign": callsign,
"lastseen": record["updated"].timestamp() * 1000, "location": record["location"].__dict__(),
"mode" : record["mode"], "lastseen": record["updated"].timestamp() * 1000,
"band" : record["band"].getName() if record["band"] is not None else None "mode": record["mode"],
} "band": record["band"].getName() if record["band"] is not None else None,
for (callsign, record) in self.positions.items() }
]) for (callsign, record) in self.positions.items()
]
)
def removeClient(self, client): def removeClient(self, client):
try: try:
@ -61,15 +65,17 @@ class Map(object):
def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None): def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None):
ts = datetime.now() ts = datetime.now()
self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band} self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band}
self.broadcast([ self.broadcast(
{ [
"callsign": callsign, {
"location": loc.__dict__(), "callsign": callsign,
"lastseen": ts.timestamp() * 1000, "location": loc.__dict__(),
"mode" : mode, "lastseen": ts.timestamp() * 1000,
"band" : band.getName() if band is not None else None "mode": mode,
} "band": band.getName() if band is not None else None,
]) }
]
)
def removeLocation(self, callsign): def removeLocation(self, callsign):
self.positions.pop(callsign, None) self.positions.pop(callsign, None)
@ -84,17 +90,14 @@ class Map(object):
for callsign in to_be_removed: for callsign in to_be_removed:
self.removeLocation(callsign) self.removeLocation(callsign)
class LatLngLocation(Location): class LatLngLocation(Location):
def __init__(self, lat: float, lon: float): def __init__(self, lat: float, lon: float):
self.lat = lat self.lat = lat
self.lon = lon self.lon = lon
def __dict__(self): def __dict__(self):
return { return {"type": "latlon", "lat": self.lat, "lon": self.lon}
"type":"latlon",
"lat":self.lat,
"lon":self.lon
}
class LocatorLocation(Location): class LocatorLocation(Location):
@ -102,7 +105,4 @@ class LocatorLocation(Location):
self.locator = locator self.locator = locator
def __dict__(self): def __dict__(self):
return { return {"type": "locator", "locator": self.locator}
"type":"locator",
"locator":self.locator
}

View File

@ -8,8 +8,10 @@ from owrx.map import Map, LatLngLocation
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DmrCache(object): class DmrCache(object):
sharedInstance = None sharedInstance = None
@staticmethod @staticmethod
def getSharedInstance(): def getSharedInstance():
if DmrCache.sharedInstance is None: if DmrCache.sharedInstance is None:
@ -18,21 +20,20 @@ class DmrCache(object):
def __init__(self): def __init__(self):
self.cache = {} self.cache = {}
self.cacheTimeout = timedelta(seconds = 86400) self.cacheTimeout = timedelta(seconds=86400)
def isValid(self, key): def isValid(self, key):
if not key in self.cache: return False if not key in self.cache:
return False
entry = self.cache[key] entry = self.cache[key]
return entry["timestamp"] + self.cacheTimeout > datetime.now() return entry["timestamp"] + self.cacheTimeout > datetime.now()
def put(self, key, value): def put(self, key, value):
self.cache[key] = { self.cache[key] = {"timestamp": datetime.now(), "data": value}
"timestamp": datetime.now(),
"data": value
}
def get(self, key): def get(self, key):
if not self.isValid(key): return None if not self.isValid(key):
return None
return self.cache[key]["data"] return self.cache[key]["data"]
@ -52,8 +53,10 @@ class DmrMetaEnricher(object):
del self.threads[id] del self.threads[id]
def enrich(self, meta): def enrich(self, meta):
if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: return None if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]:
if not "source" in meta: return None return None
if not "source" in meta:
return None
id = meta["source"] id = meta["source"]
cache = DmrCache.getSharedInstance() cache = DmrCache.getSharedInstance()
if not cache.isValid(id): if not cache.isValid(id):
@ -77,10 +80,7 @@ class YsfMetaEnricher(object):
class MetaParser(object): class MetaParser(object):
enrichers = { enrichers = {"DMR": DmrMetaEnricher(), "YSF": YsfMetaEnricher()}
"DMR": DmrMetaEnricher(),
"YSF": YsfMetaEnricher()
}
def __init__(self, handler): def __init__(self, handler):
self.handler = handler self.handler = handler
@ -93,6 +93,6 @@ class MetaParser(object):
protocol = meta["protocol"] protocol = meta["protocol"]
if protocol in MetaParser.enrichers: if protocol in MetaParser.enrichers:
additional_data = MetaParser.enrichers[protocol].enrich(meta) 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) self.handler.write_metadata(meta)

View File

@ -4,23 +4,26 @@ import time
from owrx.config import PropertyManager from owrx.config import PropertyManager
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SdrHuUpdater(threading.Thread): class SdrHuUpdater(threading.Thread):
def __init__(self): def __init__(self):
self.doRun = True self.doRun = True
super().__init__(daemon = True) super().__init__(daemon=True)
def update(self): def update(self):
pm = PropertyManager.getSharedInstance() 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) logger.debug(cmd)
returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate() returned = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()
returned=returned[0].decode('utf-8') returned = returned[0].decode("utf-8")
if "UPDATE:" in returned: if "UPDATE:" in returned:
retrytime_mins = 20 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"): if value.startswith("SUCCESS"):
logger.info("Update succeeded!") logger.info("Update succeeded!")
else: else:
@ -33,4 +36,4 @@ class SdrHuUpdater(threading.Thread):
def run(self): def run(self):
while self.doRun: while self.doRun:
retrytime_mins = self.update() retrytime_mins = self.update()
time.sleep(60*retrytime_mins) time.sleep(60 * retrytime_mins)

View File

@ -14,10 +14,12 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SdrService(object): class SdrService(object):
sdrProps = None sdrProps = None
sources = {} sources = {}
lastPort = None lastPort = None
@staticmethod @staticmethod
def getNextPort(): def getNextPort():
pm = PropertyManager.getSharedInstance() pm = PropertyManager.getSharedInstance()
@ -29,45 +31,61 @@ class SdrService(object):
if SdrService.lastPort > end: if SdrService.lastPort > end:
raise IndexError("no more available ports to start more sdrs") raise IndexError("no more available ports to start more sdrs")
return SdrService.lastPort return SdrService.lastPort
@staticmethod @staticmethod
def loadProps(): def loadProps():
if SdrService.sdrProps is None: if SdrService.sdrProps is None:
pm = PropertyManager.getSharedInstance() pm = PropertyManager.getSharedInstance()
featureDetector = FeatureDetector() featureDetector = FeatureDetector()
def loadIntoPropertyManager(dict: dict): def loadIntoPropertyManager(dict: dict):
propertyManager = PropertyManager() propertyManager = PropertyManager()
for (name, value) in dict.items(): for (name, value) in dict.items():
propertyManager[name] = value propertyManager[name] = value
return propertyManager return propertyManager
def sdrTypeAvailable(value): def sdrTypeAvailable(value):
try: try:
if not featureDetector.is_available(value["type"]): 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 False
return True return True
except UnknownFeatureException: 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 return False
# transform all dictionary items into PropertyManager object, filtering out unavailable ones # transform all dictionary items into PropertyManager object, filtering out unavailable ones
SdrService.sdrProps = { SdrService.sdrProps = {
name: loadIntoPropertyManager(value) for (name, value) in pm["sdrs"].items() if sdrTypeAvailable(value) 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 @staticmethod
def getSource(id = None): def getSource(id=None):
SdrService.loadProps() SdrService.loadProps()
if id is None: if id is None:
# TODO: configure default sdr in config? right now it will pick the first one off the list. # TODO: configure default sdr in config? right now it will pick the first one off the list.
id = list(SdrService.sdrProps.keys())[0] id = list(SdrService.sdrProps.keys())[0]
sources = SdrService.getSources() sources = SdrService.getSources()
return sources[id] return sources[id]
@staticmethod @staticmethod
def getSources(): def getSources():
SdrService.loadProps() SdrService.loadProps()
for id in SdrService.sdrProps.keys(): for id in SdrService.sdrProps.keys():
if not id in SdrService.sources: if not id in SdrService.sources:
props = SdrService.sdrProps[id] 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) cls = getattr(sys.modules[__name__], className)
SdrService.sources[id] = cls(props, SdrService.getNextPort()) SdrService.sources[id] = cls(props, SdrService.getNextPort())
return SdrService.sources 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)) logger.debug("restarting sdr source due to property change: {0} changed to {1}".format(name, value))
self.stop() self.stop()
self.start() self.start()
self.rtlProps.wire(restart) self.rtlProps.wire(restart)
self.port = port self.port = port
self.monitor = None self.monitor = None
@ -102,7 +121,7 @@ class SdrSource(object):
def getFormatConversion(self): def getFormatConversion(self):
return None return None
def activateProfile(self, id = None): def activateProfile(self, id=None):
profiles = self.props["profiles"] profiles = self.props["profiles"]
if id is None: if id is None:
id = list(profiles.keys())[0] id = list(profiles.keys())[0]
@ -110,7 +129,8 @@ class SdrSource(object):
profile = profiles[id] profile = profiles[id]
for (key, value) in profile.items(): for (key, value) in profile.items():
# skip the name, that would overwrite the source name. # skip the name, that would overwrite the source name.
if key == "name": continue if key == "name":
continue
self.props[key] = value self.props[key] = value
def getProfiles(self): def getProfiles(self):
@ -134,7 +154,9 @@ class SdrSource(object):
props = self.rtlProps props = self.rtlProps
start_sdr_command = self.getCommand().format( 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() format_conversion = self.getFormatConversion()
@ -142,14 +164,22 @@ class SdrSource(object):
start_sdr_command += " | " + format_conversion start_sdr_command += " | " + format_conversion
nmux_bufcnt = nmux_bufsize = 0 nmux_bufcnt = nmux_bufsize = 0
while nmux_bufsize < props["samp_rate"]/4: nmux_bufsize += 4096 while nmux_bufsize < props["samp_rate"] / 4:
while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6: nmux_bufcnt += 1 nmux_bufsize += 4096
while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6:
nmux_bufcnt += 1
if nmux_bufcnt == 0 or nmux_bufsize == 0: 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() self.modificationLock.release()
return return
logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)) 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) self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp)
logger.info("Started rtl source: " + cmd) logger.info("Started rtl source: " + cmd)
@ -158,7 +188,7 @@ class SdrSource(object):
logger.debug("shut down with RC={0}".format(rc)) logger.debug("shut down with RC={0}".format(rc))
self.monitor = None 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() self.monitor.start()
while True: while True:
@ -201,6 +231,7 @@ class SdrSource(object):
def addClient(self, c): def addClient(self, c):
self.clients.append(c) self.clients.append(c)
self.start() self.start()
def removeClient(self, c): def removeClient(self, c):
try: try:
self.clients.remove(c) self.clients.remove(c)
@ -236,6 +267,7 @@ class RtlSdrSource(SdrSource):
def getFormatConversion(self): def getFormatConversion(self):
return "csdr convert_u8_f" return "csdr convert_u8_f"
class HackrfSource(SdrSource): class HackrfSource(SdrSource):
def getCommand(self): def getCommand(self):
return "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-" 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): def getFormatConversion(self):
return "csdr convert_s8_f" return "csdr convert_s8_f"
class SdrplaySource(SdrSource): class SdrplaySource(SdrSource):
def getCommand(self): def getCommand(self):
command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}" command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}"
gainMap = { "rf_gain" : "RFGR", "if_gain" : "IFGR"} 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 ] 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: if gains:
command += " -g {gains}".format(gains = ",".join(gains)) command += " -g {gains}".format(gains=",".join(gains))
if self.rtlProps["antenna"] is not None: if self.rtlProps["antenna"] is not None:
command += " -a \"{antenna}\"" command += ' -a "{antenna}"'
command += " -" command += " -"
return command return command
def sleepOnRestart(self): def sleepOnRestart(self):
time.sleep(1) time.sleep(1)
class AirspySource(SdrSource): class AirspySource(SdrSource):
def getCommand(self): def getCommand(self):
frequency = self.props['center_freq'] / 1e6 frequency = self.props["center_freq"] / 1e6
command = "airspy_rx" command = "airspy_rx"
command += " -f{0}".format(frequency) command += " -f{0}".format(frequency)
command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}" command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}"
return command return command
def getFormatConversion(self): def getFormatConversion(self):
return "csdr convert_s16_f" return "csdr convert_s16_f"
class SpectrumThread(csdr.output): class SpectrumThread(csdr.output):
def __init__(self, sdrSource): def __init__(self, sdrSource):
self.sdrSource = sdrSource self.sdrSource = sdrSource
super().__init__() super().__init__()
self.props = props = self.sdrSource.props.collect( self.props = props = self.sdrSource.props.collect(
"samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression", "samp_rate",
"csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through", "temporary_directory" "fft_size",
"fft_fps",
"fft_voverlap_factor",
"fft_compression",
"csdr_dynamic_bufsize",
"csdr_print_bufsizes",
"csdr_through",
"temporary_directory",
).defaults(PropertyManager.getSharedInstance()) ).defaults(PropertyManager.getSharedInstance())
self.dsp = dsp = csdr.dsp(self) self.dsp = dsp = csdr.dsp(self)
@ -288,7 +335,11 @@ class SpectrumThread(csdr.output):
fft_fps = props["fft_fps"] fft_fps = props["fft_fps"]
fft_voverlap_factor = props["fft_voverlap_factor"] 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 = [ self.subscriptions = [
props.getProperty("samp_rate").wire(dsp.set_samp_rate), 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_fps").wire(dsp.set_fft_fps),
props.getProperty("fft_compression").wire(dsp.set_fft_compression), props.getProperty("fft_compression").wire(dsp.set_fft_compression),
props.getProperty("temporary_directory").wire(dsp.set_temporary_directory), 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) set_fft_averages(None, None)
@ -317,7 +368,7 @@ class SpectrumThread(csdr.output):
return return
if self.props["csdr_dynamic_bufsize"]: 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") logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
def pipe(): def pipe():
@ -329,7 +380,7 @@ class SpectrumThread(csdr.output):
else: else:
self.sdrSource.writeSpectrumData(data) self.sdrSource.writeSpectrumData(data)
threading.Thread(target = pipe).start() threading.Thread(target=pipe).start()
def stop(self): def stop(self):
self.dsp.stop() self.dsp.stop()
@ -340,9 +391,11 @@ class SpectrumThread(csdr.output):
def onSdrAvailable(self): def onSdrAvailable(self):
self.dsp.start() self.dsp.start()
def onSdrUnavailable(self): def onSdrUnavailable(self):
self.dsp.stop() self.dsp.stop()
class DspManager(csdr.output): class DspManager(csdr.output):
def __init__(self, handler, sdrSource): def __init__(self, handler, sdrSource):
self.handler = handler self.handler = handler
@ -350,11 +403,24 @@ class DspManager(csdr.output):
self.metaParser = MetaParser(self.handler) self.metaParser = MetaParser(self.handler)
self.wsjtParser = WsjtParser(self.handler) self.wsjtParser = WsjtParser(self.handler)
self.localProps = self.sdrSource.getProps().collect( self.localProps = (
"audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", self.sdrSource.getProps()
"csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality", .collect(
"dmr_filter", "temporary_directory", "center_freq" "audio_compression",
).defaults(PropertyManager.getSharedInstance()) "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 = csdr.dsp(self)
self.dsp.nc_port = self.sdrSource.getPort() 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("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality),
self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter), self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter),
self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory), 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_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_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"]
self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"] self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"]
self.dsp.csdr_through = self.localProps["csdr_through"] self.dsp.csdr_through = self.localProps["csdr_through"]
if (self.localProps["digimodes_enable"]): if self.localProps["digimodes_enable"]:
def set_secondary_mod(mod): def set_secondary_mod(mod):
if mod == False: mod = None if mod == False:
mod = None
self.dsp.set_secondary_demodulator(mod) self.dsp.set_secondary_demodulator(mod)
if mod is not None: if mod is not None:
self.handler.write_secondary_dsp_config({ self.handler.write_secondary_dsp_config(
"secondary_fft_size":self.localProps["digimodes_fft_size"], {
"if_samp_rate":self.dsp.if_samp_rate(), "secondary_fft_size": self.localProps["digimodes_fft_size"],
"secondary_bw":self.dsp.secondary_bw() "if_samp_rate": self.dsp.if_samp_rate(),
}) "secondary_bw": self.dsp.secondary_bw(),
}
)
self.subscriptions += [ self.subscriptions += [
self.localProps.getProperty("secondary_mod").wire(set_secondary_mod), 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) self.sdrSource.addClient(self)
@ -426,7 +497,7 @@ class DspManager(csdr.output):
"secondary_fft": self.handler.write_secondary_fft, "secondary_fft": self.handler.write_secondary_fft,
"secondary_demod": self.handler.write_secondary_demod, "secondary_demod": self.handler.write_secondary_demod,
"meta": self.metaParser.parse, "meta": self.metaParser.parse,
"wsjt_demod": self.wsjtParser.parse "wsjt_demod": self.wsjtParser.parse,
} }
write = writers[t] write = writers[t]
@ -440,6 +511,7 @@ class DspManager(csdr.output):
run = False run = False
else: else:
write(data) write(data)
return copy return copy
threading.Thread(target=pump(read_fn, write)).start() threading.Thread(target=pump(read_fn, write)).start()
@ -462,8 +534,10 @@ class DspManager(csdr.output):
logger.debug("received onSdrUnavailable, shutting down DspSource") logger.debug("received onSdrUnavailable, shutting down DspSource")
self.dsp.stop() self.dsp.stop()
class CpuUsageThread(threading.Thread): class CpuUsageThread(threading.Thread):
sharedInstance = None sharedInstance = None
@staticmethod @staticmethod
def getSharedInstance(): def getSharedInstance():
if CpuUsageThread.sharedInstance is None: if CpuUsageThread.sharedInstance is None:
@ -491,21 +565,23 @@ class CpuUsageThread(threading.Thread):
def get_cpu_usage(self): def get_cpu_usage(self):
try: try:
f = open("/proc/stat","r") f = open("/proc/stat", "r")
except: except:
return 0 #Workaround, possibly we're on a Mac return 0 # Workaround, possibly we're on a Mac
line = "" line = ""
while not "cpu " in line: line=f.readline() while not "cpu " in line:
line = f.readline()
f.close() f.close()
spl = line.split(" ") spl = line.split(" ")
worktime = int(spl[2]) + int(spl[3]) + int(spl[4]) worktime = int(spl[2]) + int(spl[3]) + int(spl[4])
idletime = int(spl[5]) idletime = int(spl[5])
dworktime = (worktime - self.last_worktime) dworktime = worktime - self.last_worktime
didletime = (idletime - self.last_idletime) didletime = idletime - self.last_idletime
rate = float(dworktime) / (didletime+dworktime) rate = float(dworktime) / (didletime + dworktime)
self.last_worktime = worktime self.last_worktime = worktime
self.last_idletime = idletime self.last_idletime = idletime
if (self.last_worktime==0): return 0 if self.last_worktime == 0:
return 0
return rate return rate
def add_client(self, c): def add_client(self, c):
@ -523,11 +599,14 @@ class CpuUsageThread(threading.Thread):
CpuUsageThread.sharedInstance = None CpuUsageThread.sharedInstance = None
self.doRun = False self.doRun = False
class TooManyClientsException(Exception): class TooManyClientsException(Exception):
pass pass
class ClientRegistry(object): class ClientRegistry(object):
sharedInstance = None sharedInstance = None
@staticmethod @staticmethod
def getSharedInstance(): def getSharedInstance():
if ClientRegistry.sharedInstance is None: if ClientRegistry.sharedInstance is None:
@ -558,4 +637,4 @@ class ClientRegistry(object):
self.clients.remove(client) self.clients.remove(client)
except ValueError: except ValueError:
pass pass
self.broadcast() self.broadcast()

View File

@ -1 +1 @@
openwebrx_version = "v0.18" openwebrx_version = "v0.18"

View File

@ -3,69 +3,76 @@ import hashlib
import json import json
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WebSocketConnection(object): class WebSocketConnection(object):
def __init__(self, handler, messageHandler): def __init__(self, handler, messageHandler):
self.handler = handler self.handler = handler
self.messageHandler = messageHandler self.messageHandler = messageHandler
my_headers = self.handler.headers.items() my_headers = self.handler.headers.items()
my_header_keys = list(map(lambda x:x[0],my_headers)) my_header_keys = list(map(lambda x: x[0], my_headers))
h_key_exists = lambda x:my_header_keys.count(x) h_key_exists = lambda x: my_header_keys.count(x)
h_value = lambda x:my_headers[my_header_keys.index(x)][1] 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")): if (
(not h_key_exists("Upgrade"))
or not (h_value("Upgrade") == "websocket")
or (not h_key_exists("Sec-WebSocket-Key"))
):
raise WebSocketException raise WebSocketException
ws_key = h_value("Sec-WebSocket-Key") ws_key = h_value("Sec-WebSocket-Key")
shakey = hashlib.sha1() 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()) 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): def get_header(self, size, opcode):
ws_first_byte = 0b10000000 | (opcode & 0x0F) 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 # 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. # anything beyond that would need to be segmented into frames. i don't really think we'll need more.
return bytes([ return bytes(
ws_first_byte, [
127, ws_first_byte,
(size >> 56) & 0xff, 127,
(size >> 48) & 0xff, (size >> 56) & 0xFF,
(size >> 40) & 0xff, (size >> 48) & 0xFF,
(size >> 32) & 0xff, (size >> 40) & 0xFF,
(size >> 24) & 0xff, (size >> 32) & 0xFF,
(size >> 16) & 0xff, (size >> 24) & 0xFF,
(size >> 8) & 0xff, (size >> 16) & 0xFF,
size & 0xff (size >> 8) & 0xFF,
]) size & 0xFF,
elif (size > 125): ]
)
elif size > 125:
# up to 2^16 can be sent using the extended payload size field by putting the size to 126 # up to 2^16 can be sent using the extended payload size field by putting the size to 126
return bytes([ return bytes([ws_first_byte, 126, (size >> 8) & 0xFF, size & 0xFF])
ws_first_byte,
126,
(size >> 8) & 0xff,
size & 0xff
])
else: else:
# 125 bytes binary message in a single unmasked frame # 125 bytes binary message in a single unmasked frame
return bytes([ws_first_byte, size]) return bytes([ws_first_byte, size])
def send(self, data): def send(self, data):
# convenience # 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. # 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 # string-type messages are sent as text frames
if (type(data) == str): if type(data) == str:
header = self.get_header(len(data), 1) 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 # anything else as binary
else: else:
header = self.get_header(len(data), 2) header = self.get_header(len(data), 2)
data_to_send = header + data data_to_send = header + data
written = self.handler.wfile.write(data_to_send) 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!") logger.error("incomplete write! closing socket!")
self.close() self.close()
else: else:
@ -73,25 +80,25 @@ class WebSocketConnection(object):
def read_loop(self): def read_loop(self):
open = True open = True
while (open): while open:
header = self.handler.rfile.read(2) header = self.handler.rfile.read(2)
opcode = header[0] & 0x0F opcode = header[0] & 0x0F
length = header[1] & 0x7F length = header[1] & 0x7F
mask = (header[1] & 0x80) >> 7 mask = (header[1] & 0x80) >> 7
if (length == 126): if length == 126:
header = self.handler.rfile.read(2) header = self.handler.rfile.read(2)
length = (header[0] << 8) + header[1] length = (header[0] << 8) + header[1]
if (mask): if mask:
masking_key = self.handler.rfile.read(4) masking_key = self.handler.rfile.read(4)
data = self.handler.rfile.read(length) data = self.handler.rfile.read(length)
if (mask): if mask:
data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)])
if (opcode == 1): if opcode == 1:
message = data.decode('utf-8') message = data.decode("utf-8")
self.messageHandler.handleTextMessage(self, message) self.messageHandler.handleTextMessage(self, message)
elif (opcode == 2): elif opcode == 2:
self.messageHandler.handleBinaryMessage(self, data) self.messageHandler.handleBinaryMessage(self, data)
elif (opcode == 8): elif opcode == 8:
open = False open = False
self.messageHandler.handleClose(self) self.messageHandler.handleClose(self)
else: else:

View File

@ -12,6 +12,7 @@ from owrx.config import PropertyManager
from owrx.bands import Bandplan from owrx.bands import Bandplan
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,9 +30,7 @@ class WsjtChopper(threading.Thread):
def getWaveFile(self): def getWaveFile(self):
filename = "{tmp_dir}/openwebrx-wsjtchopper-{id}-{timestamp}.wav".format( filename = "{tmp_dir}/openwebrx-wsjtchopper-{id}-{timestamp}.wav".format(
tmp_dir = self.tmp_dir, tmp_dir=self.tmp_dir, id=id(self), timestamp=datetime.utcnow().strftime(self.fileTimestampFormat)
id = id(self),
timestamp = datetime.utcnow().strftime(self.fileTimestampFormat)
) )
wavefile = wave.open(filename, "wb") wavefile = wave.open(filename, "wb")
wavefile.setnchannels(1) wavefile.setnchannels(1)
@ -44,13 +43,13 @@ class WsjtChopper(threading.Thread):
zeroed = t.replace(minute=0, second=0, microsecond=0) zeroed = t.replace(minute=0, second=0, microsecond=0)
delta = t - zeroed delta = t - zeroed
seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval 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)) logger.debug("scheduling: {0}".format(t))
return t.timestamp() return t.timestamp()
def startScheduler(self): def startScheduler(self):
self._scheduleNextSwitch() self._scheduleNextSwitch()
threading.Thread(target = self.scheduler.run).start() threading.Thread(target=self.scheduler.run).start()
def emptyScheduler(self): def emptyScheduler(self):
for event in self.scheduler.queue: for event in self.scheduler.queue:
@ -132,7 +131,7 @@ class Ft8Chopper(WsjtChopper):
super().__init__(source) super().__init__(source)
def decoder_commandline(self, file): 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] return ["jt9", "--ft8", "-d", "3", file]
@ -143,7 +142,7 @@ class WsprChopper(WsjtChopper):
super().__init__(source) super().__init__(source)
def decoder_commandline(self, file): def decoder_commandline(self, file):
#TODO expose decoding quality parameters through config # TODO expose decoding quality parameters through config
return ["wsprd", "-d", file] return ["wsprd", "-d", file]
@ -154,7 +153,7 @@ class Jt65Chopper(WsjtChopper):
super().__init__(source) super().__init__(source)
def decoder_commandline(self, file): 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] return ["jt9", "--jt65", "-d", "3", file]
@ -165,7 +164,7 @@ class Jt9Chopper(WsjtChopper):
super().__init__(source) super().__init__(source)
def decoder_commandline(self, file): 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] return ["jt9", "--jt9", "-d", "3", file]
@ -176,7 +175,7 @@ class Ft4Chopper(WsjtChopper):
super().__init__(source) super().__init__(source)
def decoder_commandline(self, file): 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] return ["jt9", "--ft4", "-d", "3", file]
@ -189,12 +188,7 @@ class WsjtParser(object):
self.dial_freq = None self.dial_freq = None
self.band = None self.band = None
modes = { modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"}
"~": "FT8",
"#": "JT65",
"@": "JT9",
"+": "FT4"
}
def parse(self, data): def parse(self, data):
try: try:
@ -230,8 +224,8 @@ class WsjtParser(object):
dateformat = "%H%M" dateformat = "%H%M"
else: else:
dateformat = "%H%M%S" dateformat = "%H%M%S"
timestamp = self.parse_timestamp(msg[0:len(dateformat)], dateformat) timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat)
msg = msg[len(dateformat) + 1:] msg = msg[len(dateformat) + 1 :]
modeChar = msg[14:15] modeChar = msg[14:15]
mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown"
wsjt_msg = msg[17:53].strip() wsjt_msg = msg[17:53].strip()
@ -242,7 +236,7 @@ class WsjtParser(object):
"dt": float(msg[4:8]), "dt": float(msg[4:8]),
"freq": int(msg[9:13]), "freq": int(msg[9:13]),
"mode": mode, "mode": mode,
"msg": wsjt_msg "msg": wsjt_msg,
} }
def parseLocator(self, msg, mode): def parseLocator(self, msg, mode):
@ -268,7 +262,7 @@ class WsjtParser(object):
"freq": float(msg[14:24]), "freq": float(msg[14:24]),
"drift": int(msg[25:28]), "drift": int(msg[25:28]),
"mode": "WSPR", "mode": "WSPR",
"msg": wsjt_msg "msg": wsjt_msg,
} }
def parseWsprMessage(self, msg): def parseWsprMessage(self, msg):

View File

@ -23,10 +23,9 @@
from owrx.sdrhu import SdrHuUpdater from owrx.sdrhu import SdrHuUpdater
from owrx.config import PropertyManager from owrx.config import PropertyManager
if __name__=="__main__": if __name__ == "__main__":
pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
if not "sdrhu_key" in pm: if not "sdrhu_key" in pm:
exit(1) exit(1)
SdrHuUpdater().update() SdrHuUpdater().update()