Reformatted with black -l 120 -t py35 .
This commit is contained in:
parent
79062ff3d6
commit
e15dc1ce11
124
config_webrx.py
124
config_webrx.py
@ -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
366
csdr.py
@ -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
|
||||||
|
19
openwebrx.py
19
openwebrx.py
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
|
||||||
)
|
|
||||||
|
25
owrx/http.py
25
owrx/http.py
@ -1,23 +1,36 @@
|
|||||||
from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController, MapController, FeatureController, ApiController
|
from owrx.controllers import (
|
||||||
|
StatusController,
|
||||||
|
IndexController,
|
||||||
|
AssetsController,
|
||||||
|
WebSocketController,
|
||||||
|
MapController,
|
||||||
|
FeatureController,
|
||||||
|
ApiController,
|
||||||
|
)
|
||||||
from http.server import BaseHTTPRequestHandler
|
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:
|
||||||
|
56
owrx/map.py
56
owrx/map.py
@ -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
|
|
||||||
}
|
|
||||||
|
30
owrx/meta.py
30
owrx/meta.py
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
173
owrx/source.py
173
owrx/source.py
@ -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:
|
||||||
|
@ -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:
|
||||||
|
34
owrx/wsjt.py
34
owrx/wsjt.py
@ -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):
|
||||||
|
3
sdrhu.py
3
sdrhu.py
@ -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()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user