diff --git a/config_webrx.py b/config_webrx.py
index 34e480c..0613278 100644
--- a/config_webrx.py
+++ b/config_webrx.py
@@ -36,7 +36,6 @@ config_webrx: configuration options for OpenWebRX
# ==== Server settings ====
web_port=8073
-server_hostname="localhost" # If this contains an incorrect value, the web UI may freeze on load (it can't open websocket)
max_clients=20
# ==== Web GUI configuration ====
@@ -65,25 +64,24 @@ Website: http://localhost
sdrhu_key = ""
# 3. Set this setting to True to enable listing:
sdrhu_public_listing = False
+server_hostname="localhost"
# ==== DSP/RX settings ====
fft_fps=9
fft_size=4096 #Should be power of 2
fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
-# samp_rate = 250000
-samp_rate = 2400000
-center_freq = 144250000
-rf_gain = 5 #in dB. For an RTL-SDR, rf_gain=0 will set the tuner to auto gain mode, else it will be in manual gain mode.
-ppm = 0
-
audio_compression="adpcm" #valid values: "adpcm", "none"
fft_compression="adpcm" #valid values: "adpcm", "none"
digimodes_enable=True #Decoding digimodes come with higher CPU usage.
digimodes_fft_size=1024
-start_rtl_thread=True
+# determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes
+# if you're running on a Raspi (up to 3B+) you'll want to leave this on 1
+digital_voice_unvoiced_quality = 1
+# enables lookup of DMR ids using the radioid api
+digital_voice_dmr_id_lookup = True
"""
Note: if you experience audio underruns while CPU usage is 100%, you can:
@@ -101,81 +99,107 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
# Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support #
#################################################################################################
-# You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... Some examples of configuration are available here (default is RTL-SDR):
+# Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf", "airspy"
-# >> RTL-SDR via rtl_sdr
-start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
-format_conversion="csdr convert_u8_f"
-
-#lna_gain=8
-#rf_amp=1
-#start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm, rf_amp=rf_amp, lna_gain=lna_gain)
-#format_conversion="csdr convert_s8_f"
-"""
-To use a HackRF, compile the HackRF host tools from its "stdout" branch:
- git clone https://github.com/mossmann/hackrf/
- cd hackrf
- git fetch
- git checkout origin/stdout
- cd host
- mkdir build
- cd build
- cmake .. -DINSTALL_UDEV_RULES=ON
- make
- sudo make install
-"""
-
-# >> Sound card SDR (needs ALSA)
-# I did not have the chance to properly test it.
-#samp_rate = 96000
-#start_rtl_command="arecord -f S16_LE -r {samp_rate} -c2 -".format(samp_rate=samp_rate)
-#format_conversion="csdr convert_s16_f | csdr gain_ff 30"
-
-# >> /dev/urandom test signal source
-# samp_rate = 2400000
-# start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate)
-# format_conversion="csdr convert_u8_f"
-
-# >> Pre-recorded raw I/Q file as signal source
-# You will have to correctly specify: samp_rate, center_freq, format_conversion in order to correctly play an I/Q file.
-#start_rtl_command="(while true; do cat my_iq_file.raw; done) | csdr flowcontrol {sr} 20 ".format(sr=samp_rate*2*1.05)
-#format_conversion="csdr convert_u8_f"
-
-#>> The rx_sdr command works with a variety of SDR harware: RTL-SDR, HackRF, SDRplay, UHD, Airspy, Red Pitaya, audio devices, etc.
-# It will auto-detect your SDR hardware if the following tools are installed:
-# * the vendor provided driver and library,
-# * the vendor-specific SoapySDR wrapper library,
-# * and SoapySDR itself.
-# Check out this article on the OpenWebRX Wiki: https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX/
-#start_rtl_command="rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
-#format_conversion=""
-
-# >> gr-osmosdr signal source using GNU Radio (follow this guide: https://github.com/simonyiszk/openwebrx/wiki/Using-GrOsmoSDR-as-signal-source)
-#start_rtl_command="cat /tmp/osmocom_fifo"
-#format_conversion=""
+sdrs = {
+ "rtlsdr": {
+ "name": "RTL-SDR USB Stick",
+ "type": "rtl_sdr",
+ "ppm": 0,
+ # you can change this if you use an upconverter. formula is:
+ # shown_center_freq = center_freq + lfo_offset
+ # "lfo_offset": 0,
+ "profiles": {
+ "70cm": {
+ "name": "70cm Relais",
+ "center_freq": 438800000,
+ "rf_gain": 30,
+ "samp_rate": 2400000,
+ "start_freq": 439275000,
+ "start_mod": "nfm"
+ },
+ "2m": {
+ "name": "2m komplett",
+ "center_freq": 145000000,
+ "rf_gain": 30,
+ "samp_rate": 2400000,
+ "start_freq": 145725000,
+ "start_mod": "nfm"
+ }
+ }
+ },
+ "sdrplay": {
+ "name": "SDRPlay RSP2",
+ "type": "sdrplay",
+ "ppm": 0,
+ "profiles": {
+ "20m": {
+ "name":"20m",
+ "center_freq": 14150000,
+ "rf_gain": 4,
+ "samp_rate": 500000,
+ "start_freq": 14070000,
+ "start_mod": "usb",
+ "antenna": "Antenna A"
+ },
+ "30m": {
+ "name":"30m",
+ "center_freq": 10125000,
+ "rf_gain": 4,
+ "samp_rate": 250000,
+ "start_freq": 10142000,
+ "start_mod": "usb"
+ },
+ "40m": {
+ "name":"40m",
+ "center_freq": 7100000,
+ "rf_gain": 4,
+ "samp_rate": 500000,
+ "start_freq": 7070000,
+ "start_mod": "usb",
+ "antenna": "Antenna A"
+ },
+ "80m": {
+ "name":"80m",
+ "center_freq": 3650000,
+ "rf_gain": 4,
+ "samp_rate": 500000,
+ "start_freq": 3570000,
+ "start_mod": "usb",
+ "antenna": "Antenna A"
+ },
+ "49m": {
+ "name": "49m Broadcast",
+ "center_freq": 6000000,
+ "rf_gain": 4,
+ "samp_rate": 500000,
+ "start_freq": 6070000,
+ "start_mod": "am",
+ "antenna": "Antenna A"
+ }
+ }
+ },
+ # this one is just here to test feature detection
+ "test": {
+ "type": "test"
+ }
+}
# ==== Misc settings ====
-shown_center_freq = center_freq #you can change this if you use an upconverter
-
client_audio_buffer_size = 5
#increasing client_audio_buffer_size will:
# - also increase the latency
# - decrease the chance of audio underruns
-start_freq = center_freq
-start_mod = "nfm" #nfm, am, lsb, usb, cw
-
-iq_server_port = 4951 #TCP port for ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
-
-#access_log = "~/openwebrx_access.log"
+iq_port_range = [4950, 4960] #TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
# ==== Color themes ====
#A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels
### default theme by teejez:
-waterfall_colors = "[0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]"
+waterfall_colors = [0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]
waterfall_min_level = -88 #in dB
waterfall_max_level = -20
waterfall_auto_level_margin = (5, 40)
@@ -197,7 +221,7 @@ waterfall_auto_level_margin = (5, 40)
# 3D view settings
mathbox_waterfall_frequency_resolution = 128 #bins
mathbox_waterfall_history_length = 10 #seconds
-mathbox_waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
+mathbox_waterfall_colors = [0x000000ff,0x2e6893ff,0x69a5d0ff,0x214b69ff,0x9dc4e0ff,0xfff775ff,0xff8a8aff,0xb20000ff]
# === Experimental settings ===
#Warning! The settings below are very experimental.
@@ -206,11 +230,3 @@ csdr_print_bufsizes = False # This prints the buffer sizes used for csdr proces
csdr_through = False # Setting this True will print out how much data is going into the DSP chains.
nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux.
-
-#Look up external IP address automatically from icanhazip.com, and use it as [server_hostname]
-"""
-print "[openwebrx-config] Detecting external IP address..."
-import urllib2
-server_hostname=urllib2.urlopen("http://icanhazip.com").read()[:-1]
-print "[openwebrx-config] External IP address detected:", server_hostname
-"""
diff --git a/csdr.py b/csdr.py
index c35cc3e..0ef0250 100755
--- a/csdr.py
+++ b/csdr.py
@@ -23,13 +23,22 @@ OpenWebRX csdr plugin: do the signal processing with csdr
import subprocess
import time
import os
-import code
import signal
-import fcntl
+import threading
+from functools import partial
-class dsp:
+import logging
+logger = logging.getLogger(__name__)
- def __init__(self):
+class output(object):
+ def add_output(self, type, read_fn):
+ pass
+ def reset(self):
+ pass
+
+class dsp(object):
+
+ def __init__(self, output):
self.samp_rate = 250000
self.output_rate = 11025 #this is default, and cannot be set at the moment
self.fft_size = 1024
@@ -45,7 +54,6 @@ class dsp:
self.fft_compression = "none"
self.demodulator = "nfm"
self.name = "csdr"
- self.format_conversion = "csdr convert_u8_f"
self.base_bufsize = 512
self.nc_port = 4951
self.csdr_dynamic_bufsize = False
@@ -59,62 +67,72 @@ class dsp:
self.secondary_fft_size = 1024
self.secondary_process_fft = None
self.secondary_process_demod = None
- self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", "iqtee2_pipe"]
+ self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe",
+ "iqtee2_pipe", "dmr_control_pipe"]
self.secondary_pipe_names=["secondary_shift_pipe"]
self.secondary_offset_freq = 1000
+ self.unvoiced_quality = 1
+ self.modification_lock = threading.Lock()
+ self.output = output
def chain(self,which):
- if which in [ "dmr", "dstar", "nxdn", "ysf" ]:
- self.set_output_rate(48000)
- else:
- self.set_output_rate(11025)
- any_chain_base="nc -v 127.0.0.1 {nc_port} | "
- if self.csdr_dynamic_bufsize: any_chain_base+="csdr setbuf {start_bufsize} | "
- if self.csdr_through: any_chain_base+="csdr through | "
- any_chain_base+=self.format_conversion+(" | " if self.format_conversion!="" else "") ##"csdr flowcontrol {flowcontrol} auto 1.5 10 | "
+ chain ="nc -v 127.0.0.1 {nc_port} | "
+ if self.csdr_dynamic_bufsize: chain += "csdr setbuf {start_bufsize} | "
+ if self.csdr_through: chain +="csdr through | "
if which == "fft":
- fft_chain_base = any_chain_base+"csdr fft_cc {fft_size} {fft_block_size} | " + \
+ chain += "csdr fft_cc {fft_size} {fft_block_size} | " + \
("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \
"csdr fft_exchange_sides_ff {fft_size}"
if self.fft_compression=="adpcm":
- return fft_chain_base+" | csdr compress_fft_adpcm_f_u8 {fft_size}"
- else:
- return fft_chain_base
- chain_begin=any_chain_base+"csdr shift_addition_cc --fifo {shift_pipe} | csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | "
+ chain += " | csdr compress_fft_adpcm_f_u8 {fft_size}"
+ return chain
+ chain += "csdr shift_addition_cc --fifo {shift_pipe} | "
+ chain += "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | "
+ chain += "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every} | "
if self.secondary_demodulator:
- chain_begin+="csdr tee {iqtee_pipe} | "
- chain_begin+="csdr tee {iqtee2_pipe} | "
- chain_end = ""
+ chain += "csdr tee {iqtee_pipe} | "
+ chain += "csdr tee {iqtee2_pipe} | "
+ # safe some cpu cycles... no need to decimate if decimation factor is 1
+ last_decimation_block = "csdr fractional_decimator_ff {last_decimation} | " if self.last_decimation != 1.0 else ""
+ if which == "nfm":
+ chain += "csdr fmdemod_quadri_cf | csdr limit_ff | "
+ chain += last_decimation_block
+ chain += "csdr deemphasis_nfm_ff {output_rate} | csdr convert_f_s16"
+ elif self.isDigitalVoice(which):
+ chain += "csdr fmdemod_quadri_cf | dc_block | "
+ chain += last_decimation_block
+ # dsd modes
+ if which in [ "dstar", "nxdn" ]:
+ chain += "csdr limit_ff | csdr convert_f_s16 | "
+ if which == "dstar":
+ chain += "dsd -fd"
+ elif which == "nxdn":
+ chain += "dsd -fi"
+ chain += " -i - -o - -u {unvoiced_quality} -g -1 | CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f | "
+ max_gain = 5
+ # digiham modes
+ else:
+ chain += "rrc_filter | gfsk_demodulator | "
+ if which == "dmr":
+ chain += "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | "
+ elif which == "ysf":
+ chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | "
+ max_gain = 0.0005
+ chain += "digitalvoice_filter -f | "
+ chain += "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain} | ".format(max_gain=max_gain)
+ chain += "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - "
+ elif which == "am":
+ chain += "csdr amdemod_cf | csdr fastdcblock_ff | "
+ chain += last_decimation_block
+ chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16"
+ elif which == "ssb":
+ chain += "csdr realpart_cf | "
+ chain += last_decimation_block
+ chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16"
+
if self.audio_compression=="adpcm":
- chain_end = " | csdr encode_ima_adpcm_i16_u8"
- if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr convert_f_s16"+chain_end
- if which in [ "dstar", "nxdn" ]:
- c = chain_begin
- c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16"
- if which == "dstar":
- c += " | dsd -fd"
- elif which == "nxdn":
- c += " | dsd -fi"
- c += " -i - -o - -u 2 -g 10"
- c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --input-buffer 160 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 220"
- c += chain_end
- return c
- elif which == "dmr":
- c = chain_begin
- c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16"
- c += " | rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer"
- c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256"
- c += chain_end
- return c
- elif which == "ysf":
- c = chain_begin
- c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16"
- c += " | rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y"
- c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256"
- c += chain_end
- return c
- elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end
- elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end
+ chain += " | csdr encode_ima_adpcm_i16_u8"
+ return chain
def secondary_chain(self, which):
secondary_chain_base="cat {input_pipe} | "
@@ -122,14 +140,17 @@ class dsp:
return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "")
elif which == "bpsk31":
return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \
- "csdr bandpass_fir_fft_cc $(csdr '=-(31.25)/{if_samp_rate}') $(csdr '=(31.25)/{if_samp_rate}') $(csdr '=31.25/{if_samp_rate}') | " + \
+ "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " + \
"csdr simple_agc_cc 0.001 0.5 | " + \
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8"
def set_secondary_demodulator(self, what):
+ if self.get_secondary_demodulator() == what:
+ return
self.secondary_demodulator = what
+ self.restart()
def secondary_fft_block_size(self):
return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here
@@ -139,12 +160,12 @@ class dsp:
def secondary_bpf_cutoff(self):
if self.secondary_demodulator == "bpsk31":
- return (31.25/2) / self.if_samp_rate()
+ return 31.25 / self.if_samp_rate()
return 0
def secondary_bpf_transition_bw(self):
if self.secondary_demodulator == "bpsk31":
- return (31.25/2) / self.if_samp_rate()
+ return 31.25 / self.if_samp_rate()
return 0
def secondary_samples_per_bits(self):
@@ -157,51 +178,46 @@ class dsp:
return 31.25
def start_secondary_demodulator(self):
- if(not self.secondary_demodulator): return
- print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate()
+ if not self.secondary_demodulator: return
+ logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate())
secondary_command_fft=self.secondary_chain("fft")
secondary_command_demod=self.secondary_chain(self.secondary_demodulator)
self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft)
- secondary_command_fft=secondary_command_fft.format( \
- input_pipe=self.iqtee_pipe, \
- secondary_fft_input_size=self.secondary_fft_size, \
- secondary_fft_size=self.secondary_fft_size, \
- secondary_fft_block_size=self.secondary_fft_block_size(), \
+ secondary_command_fft=secondary_command_fft.format(
+ input_pipe=self.iqtee_pipe,
+ secondary_fft_input_size=self.secondary_fft_size,
+ secondary_fft_size=self.secondary_fft_size,
+ secondary_fft_block_size=self.secondary_fft_block_size(),
)
- secondary_command_demod=secondary_command_demod.format( \
- input_pipe=self.iqtee2_pipe, \
- secondary_shift_pipe=self.secondary_shift_pipe, \
- secondary_decimation=self.secondary_decimation(), \
- secondary_samples_per_bits=self.secondary_samples_per_bits(), \
- secondary_bpf_cutoff=self.secondary_bpf_cutoff(), \
- secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), \
+ secondary_command_demod=secondary_command_demod.format(
+ input_pipe=self.iqtee2_pipe,
+ secondary_shift_pipe=self.secondary_shift_pipe,
+ secondary_decimation=self.secondary_decimation(),
+ secondary_samples_per_bits=self.secondary_samples_per_bits(),
+ secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
+ secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(),
if_samp_rate=self.if_samp_rate()
)
- print "[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft
- print "[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod
- #code.interact(local=locals())
+ logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft)
+ logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (demod) = %s", secondary_command_demod)
my_env=os.environ.copy()
#if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
- print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)"
+ logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)")
self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes
- print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)" #TODO digimodes
+ logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") #TODO digimodes
self.secondary_processes_running = True
- #open control pipes for csdr and send initialization data
- # print "==========> 1"
- if self.secondary_shift_pipe != None: #TODO digimodes
- # print "==========> 2", self.secondary_shift_pipe
- self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes
- # print "==========> 3"
- self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes
- # print "==========> 4"
+ self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())))
+ self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
- self.set_pipe_nonblocking(self.secondary_process_demod.stdout)
- self.set_pipe_nonblocking(self.secondary_process_fft.stdout)
+ #open control pipes for csdr and send initialization data
+ if self.secondary_shift_pipe != None: #TODO digimodes
+ self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes
+ self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes
def set_secondary_offset_freq(self, value):
self.secondary_offset_freq=value
@@ -212,16 +228,20 @@ class dsp:
def stop_secondary_demodulator(self):
if self.secondary_processes_running == False: return
self.try_delete_pipes(self.secondary_pipe_names)
- if self.secondary_process_fft: os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
- if self.secondary_process_demod: os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
+ if self.secondary_process_fft:
+ try:
+ os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
+ except ProcessLookupError:
+ # been killed by something else, ignore
+ pass
+ if self.secondary_process_demod:
+ try:
+ os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
+ except ProcessLookupError:
+ # been killed by something else, ignore
+ pass
self.secondary_processes_running = False
- def read_secondary_demod(self, size):
- return self.secondary_process_demod.stdout.read(size)
-
- def read_secondary_fft(self, size):
- return self.secondary_process_fft.stdout.read(size)
-
def get_secondary_demodulator(self):
return self.secondary_demodulator
@@ -244,12 +264,20 @@ class dsp:
if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2)
def set_samp_rate(self,samp_rate):
- #to change this, restart is required
self.samp_rate=samp_rate
- self.decimation=1
- while self.samp_rate/(self.decimation+1)>=self.output_rate:
- self.decimation+=1
- self.last_decimation=float(self.if_samp_rate())/self.output_rate
+ self.calculate_decimation()
+ if self.running: self.restart()
+
+ def calculate_decimation(self):
+ (self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate())
+
+ def get_decimation(self, input_rate, output_rate):
+ decimation=1
+ while input_rate / (decimation+1) >= output_rate:
+ decimation += 1
+ fraction = float(input_rate / decimation) / output_rate
+ intermediate_rate = input_rate / decimation
+ return (decimation, fraction, intermediate_rate)
def if_samp_rate(self):
return self.samp_rate/self.decimation
@@ -260,66 +288,86 @@ class dsp:
def get_output_rate(self):
return self.output_rate
+ def get_audio_rate(self):
+ if self.isDigitalVoice():
+ return 48000
+ return self.get_output_rate()
+
+ def isDigitalVoice(self, demodulator = None):
+ if demodulator is None:
+ demodulator = self.get_demodulator()
+ return demodulator in ["dmr", "dstar", "nxdn", "ysf"]
+
def set_output_rate(self,output_rate):
self.output_rate=output_rate
- self.set_samp_rate(self.samp_rate) #as it depends on output_rate
+ self.calculate_decimation()
def set_demodulator(self,demodulator):
- #to change this, restart is required
+ if (self.demodulator == demodulator): return
self.demodulator=demodulator
+ self.calculate_decimation()
+ self.restart()
def get_demodulator(self):
return self.demodulator
def set_fft_size(self,fft_size):
- #to change this, restart is required
self.fft_size=fft_size
+ self.restart()
def set_fft_fps(self,fft_fps):
- #to change this, restart is required
self.fft_fps=fft_fps
+ self.restart()
def set_fft_averages(self,fft_averages):
- #to change this, restart is required
self.fft_averages=fft_averages
+ self.restart()
def fft_block_size(self):
if self.fft_averages == 0: return self.samp_rate/self.fft_fps
else: return self.samp_rate/self.fft_fps/self.fft_averages
- def set_format_conversion(self,format_conversion):
- self.format_conversion=format_conversion
-
def set_offset_freq(self,offset_freq):
self.offset_freq=offset_freq
if self.running:
+ self.modification_lock.acquire()
self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate))
self.shift_pipe_file.flush()
+ self.modification_lock.release()
def set_bpf(self,low_cut,high_cut):
self.low_cut=low_cut
self.high_cut=high_cut
if self.running:
+ self.modification_lock.acquire()
self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) )
self.bpf_pipe_file.flush()
+ self.modification_lock.release()
def get_bpf(self):
return [self.low_cut, self.high_cut]
def set_squelch_level(self, squelch_level):
self.squelch_level=squelch_level
+ #no squelch required on digital voice modes
+ actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level
if self.running:
- self.squelch_pipe_file.write( "%g\n"%(float(self.squelch_level)) )
+ self.modification_lock.acquire()
+ self.squelch_pipe_file.write("%g\n"%(float(actual_squelch)))
self.squelch_pipe_file.flush()
+ self.modification_lock.release()
- def get_smeter_level(self):
- if self.running:
- line=self.smeter_pipe_file.readline()
- return float(line[:-1])
+ def set_unvoiced_quality(self, q):
+ self.unvoiced_quality = q
+ self.restart()
- def get_metadata(self):
- if self.running and self.meta_pipe:
- return self.meta_pipe_file.readline()
+ def get_unvoiced_quality(self):
+ return self.unvoiced_quality
+
+ def set_dmr_filter(self, filter):
+ if self.dmr_control_pipe_file:
+ self.dmr_control_pipe_file.write("{0}\n".format(filter))
+ self.dmr_control_pipe_file.flush()
def mkfifo(self,path):
try:
@@ -332,9 +380,7 @@ class dsp:
return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate))
def try_create_pipes(self, pipe_names, command_base):
- # print "try_create_pipes"
for pipe_name in pipe_names:
- # print "\t"+pipe_name
if "{"+pipe_name+"}" in command_base:
setattr(self, pipe_name, self.pipe_base_path+pipe_name)
self.mkfifo(getattr(self, pipe_name))
@@ -346,122 +392,107 @@ class dsp:
pipe_path = getattr(self,pipe_name,None)
if pipe_path:
try: os.unlink(pipe_path)
- except Exception as e: print "[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e
-
- def set_pipe_nonblocking(self, pipe):
- flags = fcntl.fcntl(pipe, fcntl.F_GETFL)
- fcntl.fcntl(pipe, fcntl.F_SETFL, flags | os.O_NONBLOCK)
+ except Exception:
+ logger.exception("try_delete_pipes()")
def start(self):
+ self.modification_lock.acquire()
+ if (self.running):
+ self.modification_lock.release()
+ return
+ self.running = True
+
command_base=self.chain(self.demodulator)
#create control pipes for csdr
self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self))
- # self.bpf_pipe = self.shift_pipe = self.squelch_pipe = self.smeter_pipe = None
self.try_create_pipes(self.pipe_names, command_base)
- # if "{bpf_pipe}" in command_base:
- # self.bpf_pipe=pipe_base_path+"bpf"
- # self.mkfifo(self.bpf_pipe)
- # if "{shift_pipe}" in command_base:
- # self.shift_pipe=pipe_base_path+"shift"
- # self.mkfifo(self.shift_pipe)
- # if "{squelch_pipe}" in command_base:
- # self.squelch_pipe=pipe_base_path+"squelch"
- # self.mkfifo(self.squelch_pipe)
- # if "{smeter_pipe}" in command_base:
- # self.smeter_pipe=pipe_base_path+"smeter"
- # self.mkfifo(self.smeter_pipe)
- # if "{iqtee_pipe}" in command_base:
- # self.iqtee_pipe=pipe_base_path+"iqtee"
- # self.mkfifo(self.iqtee_pipe)
- # if "{iqtee2_pipe}" in command_base:
- # self.iqtee2_pipe=pipe_base_path+"iqtee2"
- # self.mkfifo(self.iqtee2_pipe)
-
#run the command
- command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, \
- last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, \
- bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), \
- flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, \
- squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe )
+ command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation,
+ last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages,
+ bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(),
+ flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port,
+ squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe,
+ output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000),
+ unvoiced_quality = self.get_unvoiced_quality(), dmr_control_pipe = self.dmr_control_pipe)
- print "[openwebrx-dsp-plugin:csdr] Command =",command
- #code.interact(local=locals())
+ logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command)
my_env=os.environ.copy()
if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
- #set stdout to non-blocking to avoid blocking the main loop when no audio was decoded in digital modes
- self.set_pipe_nonblocking(self.process.stdout)
+ def watch_thread():
+ rc = self.process.wait()
+ logger.debug("dsp thread ended with rc=%d", rc)
+ if (rc == 0 and self.running and not self.modification_lock.locked()):
+ logger.debug("restarting since rc = 0, self.running = true, and no modification")
+ self.restart()
- self.running = True
+ threading.Thread(target = watch_thread).start()
- #open control pipes for csdr and send initialization data
- if self.bpf_pipe != None:
- self.bpf_pipe_file=open(self.bpf_pipe,"w")
- self.set_bpf(self.low_cut,self.high_cut)
- if self.shift_pipe != None:
- self.shift_pipe_file=open(self.shift_pipe,"w")
- self.set_offset_freq(self.offset_freq)
- if self.squelch_pipe != None:
- self.squelch_pipe_file=open(self.squelch_pipe,"w")
- self.set_squelch_level(self.squelch_level)
- if self.smeter_pipe != None:
- self.smeter_pipe_file=open(self.smeter_pipe,"r")
- self.set_pipe_nonblocking(self.smeter_pipe_file)
- if self.meta_pipe != None:
- self.meta_pipe_file=open(self.meta_pipe,"r")
- self.set_pipe_nonblocking(self.meta_pipe_file)
+ self.output.add_output("audio", partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256))
+ # open control pipes for csdr
+ if self.bpf_pipe:
+ self.bpf_pipe_file = open(self.bpf_pipe, "w")
+ if self.shift_pipe:
+ self.shift_pipe_file = open(self.shift_pipe, "w")
+ if self.squelch_pipe:
+ self.squelch_pipe_file = open(self.squelch_pipe, "w")
self.start_secondary_demodulator()
- def read(self,size):
- return self.process.stdout.read(size)
+ self.modification_lock.release()
- def read_async(self, size):
- try:
- return self.process.stdout.read(size)
- except IOError:
- return None
+ # send initial config through the pipes
+ if self.squelch_pipe:
+ self.set_squelch_level(self.squelch_level)
+ if self.shift_pipe:
+ self.set_offset_freq(self.offset_freq)
+ if self.bpf_pipe:
+ self.set_bpf(self.low_cut, self.high_cut)
+ if self.smeter_pipe:
+ self.smeter_pipe_file=open(self.smeter_pipe,"r")
+ def read_smeter():
+ raw = self.smeter_pipe_file.readline()
+ if len(raw) == 0:
+ return None
+ else:
+ return float(raw.rstrip("\n"))
+ self.output.add_output("smeter", read_smeter)
+ if self.meta_pipe != None:
+ # TODO make digiham output unicode and then change this here
+ self.meta_pipe_file=open(self.meta_pipe, "r", encoding="cp437")
+ def read_meta():
+ raw = self.meta_pipe_file.readline()
+ if len(raw) == 0:
+ return None
+ else:
+ return raw.rstrip("\n")
+ self.output.add_output("meta", read_meta)
+
+ if self.dmr_control_pipe:
+ self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w")
def stop(self):
- os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
+ self.modification_lock.acquire()
+ self.running = False
+ if hasattr(self, "process"):
+ try:
+ os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
+ except ProcessLookupError:
+ # been killed by something else, ignore
+ pass
self.stop_secondary_demodulator()
- #if(self.process.poll()!=None):return # returns None while subprocess is running
- #while(self.process.poll()==None):
- # #self.process.kill()
- # print "killproc",os.getpgid(self.process.pid),self.process.pid
- # os.killpg(self.process.pid, signal.SIGTERM)
- #
- # time.sleep(0.1)
self.try_delete_pipes(self.pipe_names)
- # if self.bpf_pipe:
- # try: os.unlink(self.bpf_pipe)
- # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe
- # if self.shift_pipe:
- # try: os.unlink(self.shift_pipe)
- # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.shift_pipe
- # if self.squelch_pipe:
- # try: os.unlink(self.squelch_pipe)
- # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.squelch_pipe
- # if self.smeter_pipe:
- # try: os.unlink(self.smeter_pipe)
- # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.smeter_pipe
- # if self.iqtee_pipe:
- # try: os.unlink(self.iqtee_pipe)
- # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee_pipe
- # if self.iqtee2_pipe:
- # try: os.unlink(self.iqtee2_pipe)
- # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee2_pipe
-
- self.running = False
+ self.modification_lock.release()
def restart(self):
+ if not self.running: return
self.stop()
self.start()
diff --git a/htdocs/gfx/google_maps_pin.svg b/htdocs/gfx/google_maps_pin.svg
new file mode 100644
index 0000000..2c54fe1
--- /dev/null
+++ b/htdocs/gfx/google_maps_pin.svg
@@ -0,0 +1,77 @@
+
+
+
+
diff --git a/htdocs/gfx/openwebrx-avatar.png b/htdocs/gfx/openwebrx-avatar.png
index 7e9736f..2ae083f 100644
Binary files a/htdocs/gfx/openwebrx-avatar.png and b/htdocs/gfx/openwebrx-avatar.png differ
diff --git a/htdocs/gfx/openwebrx-directcall.png b/htdocs/gfx/openwebrx-directcall.png
new file mode 100644
index 0000000..2d74713
Binary files /dev/null and b/htdocs/gfx/openwebrx-directcall.png differ
diff --git a/htdocs/gfx/openwebrx-groupcall.png b/htdocs/gfx/openwebrx-groupcall.png
new file mode 100644
index 0000000..5d61a4c
Binary files /dev/null and b/htdocs/gfx/openwebrx-groupcall.png differ
diff --git a/htdocs/gfx/openwebrx-mute.png b/htdocs/gfx/openwebrx-mute.png
new file mode 100644
index 0000000..23da7bb
Binary files /dev/null and b/htdocs/gfx/openwebrx-mute.png differ
diff --git a/htdocs/index.wrx b/htdocs/index.html
similarity index 62%
rename from htdocs/index.wrx
rename to htdocs/index.html
index 2fcf3be..90ac89c 100644
--- a/htdocs/index.wrx
+++ b/htdocs/index.html
@@ -22,59 +22,44 @@
OpenWebRX | Open Source SDR Web App for Everyone!
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
%[RX_PHOTO_TITLE]
-
%[RX_PHOTO_DESC]
-
-
-
-
-
-
-
-
%[RX_TITLE]
-
%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m,
[maps]
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Status
+
Log
+
Receiver
+
+
-
-
-
Status
-
Log
-
Receiver
-
-
+
+
@@ -90,6 +75,10 @@
---.--- MHz
---.--- MHz
+
+
+
FM
@@ -102,12 +91,16 @@
CW
DMR
DStar
NXDN
YSF
@@ -118,23 +111,23 @@
-
+
Under construction
We're working on the code right now, so the application might fail.
@@ -175,14 +168,41 @@
-
-
+
Start OpenWebRX
diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css
index 250c54a..5624d79 100644
--- a/htdocs/openwebrx.css
+++ b/htdocs/openwebrx.css
@@ -155,22 +155,12 @@ input[type=range]:focus::-ms-fill-upper
.webrx-top-bar-parts
{
- position: absolute;
- top: 0px;
- left: 0px;
- width:100%;
height:67px;
}
-#webrx-top-bar-background
-{
- background-color: #808080;
- opacity: 0.15;
- filter:alpha(opacity=15);
-}
-
#webrx-top-bar
{
+ background: rgba(128, 128, 128, 0.15);
margin:0;
padding:0;
user-select: none;
@@ -179,20 +169,23 @@ input[type=range]:focus::-ms-fill-upper
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
+ overflow: hidden;
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
}
#webrx-top-logo
{
- position: absolute;
- top: 12px;
- left: 15px;
+ padding: 12px;
+ float: left;
}
#webrx-ha5kfu-top-logo
{
- position: absolute;
- top: 15px;
- right: 15px;
+ float: right;
+ padding: 15px;
}
#webrx-top-photo
@@ -204,79 +197,37 @@ input[type=range]:focus::-ms-fill-upper
#webrx-rx-avatar-background
{
cursor:pointer;
- position: absolute;
- left: 285px;
- top: 6px;
+ background-image: url(gfx/openwebrx-avatar-background.png);
+ background-origin: content-box;
+ background-repeat: no-repeat;
+ float: left;
+ width: 54px;
+ height: 54px;
+ padding: 7px;
}
#webrx-rx-avatar
{
cursor:pointer;
- position: absolute;
- left: 289px;
- top: 10px;
width: 46px;
height: 46px;
+ padding: 4px;
}
#webrx-top-photo-clip
{
+ min-height: 67px;
max-height: 350px;
overflow: hidden;
position: relative;
}
-/*#webrx-bottom-bar
-{
- position: absolute;
- bottom: 0px;
- width: 100%;
- height: 117px;
- background-image:url(gfx/webrx-bottom-bar.png);
-}*/
-
#webrx-page-container
{
min-height:100%;
position:relative;
}
-/*#webrx-photo-gradient-left
-{
- position: absolute;
- bottom: 0px;
- left: 0px;
- background-image:url(gfx/webrx-photo-gradient-corner.png);
- width: 59px;
- height: 92px;
-
-}
-
-#webrx-photo-gradient-middle
-{
- position: absolute;
- bottom: 0px;
- left: 59px;
- right: 59px;
- height: 92px;
- background-image:url(gfx/webrx-photo-gradient-middle.png);
-}
-
-#webrx-photo-gradient-right
-{
- position: absolute;
- bottom: 0px;
- right: 0px;
- background-image:url(gfx/webrx-photo-gradient-corner.png);
- width: 59px;
- height: 92px;
- -webkit-transform:scaleX(-1);
- -moz-transform:scaleX(-1);
- -ms-transform:scaleX(-1);
- -o-transform:scaleX(-1);
- transform:scaleX(-1);
-}*/
-
#webrx-rx-photo-title
{
position: absolute;
@@ -303,10 +254,17 @@ input[type=range]:focus::-ms-fill-upper
#webrx-rx-photo-desc a
{
- /*color: #007df1;*/
color: #5ca8ff;
text-shadow: none;
- /*text-shadow: 0px 0px 7px #fff;*/
+}
+
+#webrx-rx-texts {
+ float: left;
+ padding: 10px;
+}
+
+#webrx-rx-texts div {
+ padding: 3px;
}
#webrx-rx-title
@@ -314,9 +272,6 @@ input[type=range]:focus::-ms-fill-upper
white-space:nowrap;
overflow: hidden;
cursor:pointer;
- position: absolute;
- left: 350px;
- top: 13px;
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
color: #909090;
font-size: 11pt;
@@ -330,15 +285,11 @@ input[type=range]:focus::-ms-fill-upper
cursor:pointer;
font-size: 10pt;
color: #909090;
- position: absolute;
- left: 350px;
- top: 34px;
}
#webrx-rx-desc a
{
color: #909090;
- /*text-decoration: none;*/
}
#openwebrx-rx-details-arrow
@@ -718,9 +669,7 @@ img.openwebrx-mirror-img
#openwebrx-main-buttons
{
- position: absolute;
- right: 133px;
- top: 3px;
+ float: right;
margin:0;
color: white;
text-shadow: 0px 0px 4px #000000;
@@ -841,10 +790,7 @@ img.openwebrx-mirror-img
transition: width 500ms, left 500ms;
}
-#openwebrx-secondary-demod-listbox
-{
- width: 201px;
- height: 27px;
+.openwebrx-panel select {
border-radius: 5px;
background-color: #373737;
color: White;
@@ -856,16 +802,27 @@ img.openwebrx-mirror-img
border-color: transparent;
border-width: 0px;
-moz-appearance: none;
- padding-left:3px;
}
-#openwebrx-secondary-demod-listbox option
-{
+.openwebrx-panel select option {
border-width: 0px;
background-color: #373737;
color: White;
}
+#openwebrx-secondary-demod-listbox
+{
+ width: 201px;
+ height: 27px;
+ padding-left:3px;
+}
+
+#openwebrx-sdr-profiles-listbox {
+ width: 100%;
+ font-size: 10pt;
+ height: 27px;
+}
+
#openwebrx-cursor-blink
{
animation: cursor-blink 1s infinite;
@@ -971,3 +928,88 @@ img.openwebrx-mirror-img
border-color: Red;
}
+.openwebrx-meta-slot {
+ width: 145px;
+ height: 196px;
+ float: left;
+ margin-right: 10px;
+
+ background-color: #676767;
+ padding: 2px 0;
+ color: #333;
+
+ text-align: center;
+ position: relative;
+}
+
+.openwebrx-meta-slot, .openwebrx-meta-slot.muted:before {
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+.openwebrx-meta-slot.muted:before {
+ display: block;
+ content: "";
+ background-image: url("gfx/openwebrx-mute.png");
+ width:100%;
+ height:133px;
+ background-position: center;
+ background-repeat: no-repeat;
+ cursor: pointer;
+
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ background-color: rgba(0,0,0,.3);
+}
+
+.openwebrx-meta-slot.active {
+ background-color: #95bbdf;
+}
+
+.openwebrx-meta-slot.sync .openwebrx-dmr-slot:before {
+ content:"";
+ display: inline-block;
+ margin: 0 5px;
+ width: 12px;
+ height: 12px;
+ background-color: #ABFF00;
+ border-radius: 50%;
+ box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px;
+}
+
+.openwebrx-meta-slot:last-child {
+ margin-right: 0;
+}
+
+.openwebrx-meta-slot .openwebrx-meta-user-image {
+ width:100%;
+ height:133px;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.openwebrx-meta-slot.active .openwebrx-meta-user-image {
+ background-image: url("gfx/openwebrx-directcall.png");
+}
+
+.openwebrx-meta-slot.active .openwebrx-meta-user-image.group {
+ background-image: url("gfx/openwebrx-groupcall.png");
+}
+
+.openwebrx-dmr-timeslot-panel * {
+ cursor: pointer;
+}
+
+.openwebrx-maps-pin {
+ background-image: url("gfx/google_maps_pin.svg");
+ background-position: center;
+ background-repeat: no-repeat;
+ width: 15px;
+ height: 15px;
+ background-size: contain;
+ display: inline-block;
+}
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js
index 8adda72..27cc8fc 100644
--- a/htdocs/openwebrx.js
+++ b/htdocs/openwebrx.js
@@ -52,6 +52,7 @@ var waterfall_setup_done=0;
var waterfall_queue = [];
var waterfall_timer;
var secondary_fft_size;
+var audio_allowed;
/*function fade(something,from,to,time_ms,fps)
{
@@ -79,7 +80,9 @@ is_chrome = /Chrome/.test(navigator.userAgent);
function init_rx_photo()
{
- e("webrx-top-photo-clip").style.maxHeight=rx_photo_height.toString()+"px";
+ var clip = e("webrx-top-photo-clip");
+ rx_photo_height = clip.clientHeight
+ clip.style.maxHeight=rx_photo_height+"px";
window.setTimeout(function() { animate(e("webrx-rx-photo-title"),"opacity","",1,0,1,500,30); },1000);
window.setTimeout(function() { animate(e("webrx-rx-photo-desc"),"opacity","",1,0,1,500,30); },1500);
window.setTimeout(function() { close_rx_photo() },2500);
@@ -133,14 +136,14 @@ function toggleMute()
if (mute) {
mute = false;
e("openwebrx-mute-on").id="openwebrx-mute-off";
- e("openwebrx-mute-img").src="gfx/openwebrx-speaker.png";
+ e("openwebrx-mute-img").src="static/gfx/openwebrx-speaker.png";
e("openwebrx-panel-volume").disabled=false;
e("openwebrx-panel-volume").style.opacity=1.0;
e("openwebrx-panel-volume").value = volumeBeforeMute;
} else {
mute = true;
e("openwebrx-mute-off").id="openwebrx-mute-on";
- e("openwebrx-mute-img").src="gfx/openwebrx-speaker-muted.png";
+ e("openwebrx-mute-img").src="static/gfx/openwebrx-speaker-muted.png";
e("openwebrx-panel-volume").disabled=true;
e("openwebrx-panel-volume").style.opacity=0.5;
volumeBeforeMute = e("openwebrx-panel-volume").value;
@@ -160,7 +163,7 @@ function updateSquelch()
{
var sliderValue=parseInt(e("openwebrx-panel-squelch").value);
var outputValue=(sliderValue==parseInt(e("openwebrx-panel-squelch").min))?0:getLinearSmeterValue(sliderValue);
- ws.send("SET squelch_level="+outputValue.toString());
+ ws.send(JSON.stringify({"type":"dspcontrol","params":{"squelch_level":outputValue}}));
}
function updateWaterfallColors(which)
@@ -433,8 +436,8 @@ function demodulator_default_analog(offset_frequency,subtype)
}
else if(subtype=="dmr" ||Â subtype=="ysf")
{
- this.low_cut=-6500;
- this.high_cut=6500;
+ this.low_cut=-4000;
+ this.high_cut=4000;
}
else if(subtype=="dstar" ||Â subtype=="nxdn")
{
@@ -470,9 +473,13 @@ function demodulator_default_analog(offset_frequency,subtype)
this.doset=function(first_time)
{ //this function sends demodulator parameters to the server
- ws.send("SET"+((first_time)?" mod="+this.server_mod:"")+
- " low_cut="+this.low_cut.toString()+" high_cut="+this.high_cut.toString()+
- " offset_freq="+this.offset_frequency.toString());
+ params = {
+ "low_cut": this.low_cut,
+ "high_cut": this.high_cut,
+ "offset_freq": this.offset_frequency
+ };
+ if (first_time) params.mod = this.server_mod;
+ ws.send(JSON.stringify({"type":"dspcontrol","params":params}));
}
this.doset(true); //we set parameters on object creation
@@ -612,6 +619,8 @@ function demodulator_analog_replace(subtype, for_digital)
}
demodulator_add(new demodulator_default_analog(temp_offset,subtype));
demodulator_buttons_update();
+ hide_digitalvoice_panels();
+ toggle_panel("openwebrx-panel-metadata-" + subtype, true);
}
function demodulator_set_offset_frequency(which,to_what)
@@ -1150,176 +1159,232 @@ function audio_calculate_resampling(targetRate)
debug_ws_data_received=0;
max_clients_num=0;
+clients_num = 0;
var COMPRESS_FFT_PAD_N=10; //should be the same as in csdr.c
function on_ws_recv(evt)
{
- if(!(evt.data instanceof ArrayBuffer)) { divlog("on_ws_recv(): Not ArrayBuffer received...",1); return; }
- //
- debug_ws_data_received+=evt.data.byteLength/1000;
- first4Chars=getFirstChars(evt.data,4);
- first3Chars=first4Chars.slice(0,3);
- if(first3Chars=="CLI")
- {
- var stringData=arrayBufferToString(evt.data);
- if(stringData.substring(0,16)=="CLIENT DE SERVER") divlog("Server acknowledged WebSocket connection.");
+ if (typeof evt.data == 'string') {
+ // text messages
+ debug_ws_data_received += evt.data.length / 1000;
- }
- if(first3Chars=="AUD")
- {
- var audio_data;
- if(audio_compression=="adpcm") audio_data=new Uint8Array(evt.data,4)
- else audio_data=new Int16Array(evt.data,4);
- audio_prepare(audio_data);
- audio_buffer_current_size_debug+=audio_data.length;
- audio_buffer_all_size_debug+=audio_data.length;
- if(!(ios||is_chrome) && (audio_initialized==0 && audio_prepared_buffers.length>audio_buffering_fill_to)) audio_init()
- }
- else if(first3Chars=="FFT")
- {
- //alert("Yupee! Doing FFT");
- //if(first4Chars=="FFTS") console.log("FFTS");
- if(fft_compression=="none") waterfall_add_queue(new Float32Array(evt.data,4));
- else if(fft_compression="adpcm")
- {
- fft_codec.reset();
+ if (evt.data.substr(0, 16) == "CLIENT DE SERVER") {
+ divlog("Server acknowledged WebSocket connection.");
+ } else {
+ try {
+ json = JSON.parse(evt.data)
+ switch (json.type) {
+ case "config":
+ config = json.value;
+ window.waterfall_colors = config.waterfall_colors;
+ window.waterfall_min_level_default = config.waterfall_min_level;
+ window.waterfall_max_level_default = config.waterfall_max_level;
+ window.waterfall_auto_level_margin = config.waterfall_auto_level_margin;
+ waterfallColorsDefault();
+
+ window.starting_mod = config.start_mod
+ window.starting_offset_frequency = config.start_offset_frequency;
+ window.audio_buffering_fill_to = config.client_audio_buffer_size;
+ bandwidth = config.samp_rate;
+ center_freq = config.center_freq + config.lfo_offset;
+ fft_size = config.fft_size;
+ fft_fps = config.fft_fps;
+ audio_compression = config.audio_compression;
+ divlog( "Audio stream is "+ ((audio_compression=="adpcm")?"compressed":"uncompressed")+"." )
+ fft_compression = config.fft_compression;
+ divlog( "FFT stream is "+ ((fft_compression=="adpcm")?"compressed":"uncompressed")+"." )
+ max_clients_num = config.max_clients;
+ progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num*0.85);
+ mathbox_waterfall_colors = config.mathbox_waterfall_colors;
+ mathbox_waterfall_frequency_resolution = config.mathbox_waterfall_frequency_resolution;
+ mathbox_waterfall_history_length = config.mathbox_waterfall_history_length;
- var waterfall_i16=fft_codec.decode(new Uint8Array(evt.data,4));
- var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N);
- for(var i=0;i85);
- break;
- case "clients":
- var clients_num=parseInt(param[1]);
- progressbar_set(e("openwebrx-bar-clients"),clients_num/max_clients_num,"Clients ["+param[1]+"]",clients_num>max_clients_num*0.85);
- break;
- case "max_clients":
- max_clients_num=parseInt(param[1]);
- break;
- case "s":
- smeter_level=parseFloat(param[1]);
- setSmeterAbsoluteValue(smeter_level);
- break;
- }
- }
- /*}
- catch(err)
- {
- divlog("Received invalid message over WebSocket.");
- }*/
- } else if (first3Chars=='MET')
- {
- var stringData=arrayBufferToString(evt.data);
- var metaPanels = Array.prototype.filter.call(document.getElementsByClassName('openwebrx-panel'), function(el) {
- return el.dataset.panelName == 'metadata';
- });
+ break;
+ case "receiver_details":
+ var r = json.value;
+ e('webrx-rx-title').innerHTML = r.receiver_name;
+ e('webrx-rx-desc').innerHTML = r.receiver_location + ' | Loc: ' + r.receiver_qra + ', ASL: ' + r.receiver_asl + ' m, [maps]';
+ e('webrx-rx-photo-title').innerHTML = r.photo_title;
+ e('webrx-rx-photo-desc').innerHTML = r.photo_desc;
+ break;
+ case "smeter":
+ smeter_level = json.value;
+ setSmeterAbsoluteValue(smeter_level);
+ break;
+ case "cpuusage":
+ var server_cpu_usage = json.value;
+ progressbar_set(e("openwebrx-bar-server-cpu"),server_cpu_usage,"Server CPU [" + Math.round(server_cpu_usage * 100) + "%]",server_cpu_usage>85);
+ break;
+ case "clients":
+ client_num = json.value;
+ progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num*0.85);
+ break;
+ case "profiles":
+ var listbox = e("openwebrx-sdr-profiles-listbox");
+ listbox.innerHTML = json.value.map(function(profile){
+ return '";
+ }).join("");
+ break;
+ case "features":
+ for (var feature in json.value) {
+ $('[data-feature="' + feature + '"')[json.value[feature] ? "show" : "hide"]();
+ }
+ break;
+ case "metadata":
+ update_metadata(json.value);
+ break;
+ default:
+ console.warn('received message of unknown type: ' + json.type);
+ }
+ } catch (e) {
+ // don't lose exception
+ console.error(e)
+ }
+ }
+ } else if (evt.data instanceof ArrayBuffer) {
+ // binary messages
+ debug_ws_data_received += evt.data.byteLength / 1000;
- var meta = {};
- stringData.substr(4).split(";").forEach(function(s)Â {
- var item = s.split(":");
- meta[item[0]] = item[1];
- });
+ type = new Uint8Array(evt.data, 0, 1)[0]
+ data = evt.data.slice(1)
- var update = function(el) {
- el.innerHTML = "";
- }
- if (meta.protocol) switch (meta.protocol) {
- case 'DMR':
- if (meta.slot) {
- var html = 'Timeslot: ' + meta.slot;
- if (meta.type) html += ' Typ: ' + meta.type;
- if (meta.source && meta.target) html += ' Source: ' + meta.source + ' Target: ' + meta.target;
- update = function(el) {
- var slotEl = el.getElementsByClassName('slot-' + meta.slot);
- if (!slotEl.length) {
- slotEl = document.createElement('div');
- slotEl.className = 'slot-' + meta.slot;
- el.appendChild(slotEl);
- } else {
- slotEl = slotEl[0];
- }
- slotEl.innerHTML = html;
- };
+ switch (type) {
+ case 1:
+ // FFT data
+ if (fft_compression=="none") {
+ waterfall_add_queue(new Float32Array(data));
+ } else if (fft_compression == "adpcm") {
+ fft_codec.reset();
+
+ var waterfall_i16=fft_codec.decode(new Uint8Array(data));
+ var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N);
+ for(var i=0;iaudio_buffering_fill_to)) audio_init()
+ break;
+ case 3:
+ // secondary FFT
+ if (fft_compression == "none") {
+ secondary_demod_waterfall_add_queue(new Float32Array(data));
+ } else if (fft_compression == "adpcm") {
+ fft_codec.reset();
- metaPanels.forEach(update);
+ var waterfall_i16=fft_codec.decode(new Uint8Array(data));
+ var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N);
+ for(var i=0;i" + source;
+ }
+ up = meta.up ? "Up: " + meta.up : "";
+ down = meta.down ? "Down: " + meta.down : "";
+ $(el).find(".openwebrx-meta-slot").addClass("active");
+ } else {
+ $(el).find(".openwebrx-meta-slot").removeClass("active");
+ }
+ $(el).find(".openwebrx-ysf-mode").text(mode);
+ $(el).find(".openwebrx-ysf-source").html(source);
+ $(el).find(".openwebrx-ysf-up").text(up);
+ $(el).find(".openwebrx-ysf-down").text(down);
+
+ break;
+ } else {
+ clear_metadata();
}
}
+function hide_digitalvoice_panels() {
+ $(".openwebrx-meta-panel").each(function(_, p){
+ toggle_panel(p.id, false);
+ });
+ clear_metadata();
+}
+
+function clear_metadata() {
+ $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text("");
+ $(".openwebrx-meta-slot").removeClass("active").removeClass("sync");
+ $(".openwebrx-dmr-timeslot-panel").removeClass("muted");
+}
+
function add_problem(what)
{
problems_span=e("openwebrx-problems");
@@ -1630,7 +1695,9 @@ function audio_flush_notused()
function webrx_set_param(what, value)
{
- ws.send("SET "+what+"="+value.toString());
+ params = {};
+ params[what] = value;
+ ws.send(JSON.stringify({"type":"dspcontrol","params":params}));
}
var starting_mute = false;
@@ -1645,7 +1712,7 @@ function parsehash()
if(harr[0]=="mute") toggleMute();
else if(harr[0]=="mod") starting_mod = harr[1];
else if(harr[0]=="sql")
- {
+ {
e("openwebrx-panel-squelch").value=harr[1];
updateSquelch();
}
@@ -1680,16 +1747,18 @@ function audio_preinit()
else if(audio_context.sampleRate>44100*4)
audio_buffer_size = 4096 * 4;
- audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED);
- audio_last_output_buffer = new Float32Array(audio_buffer_size);
+ if (!audio_rebuffer) {
+ audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED);
+ audio_last_output_buffer = new Float32Array(audio_buffer_size);
- //we send our setup packet
- parsehash();
+ //we send our setup packet
+ parsehash();
- audio_calculate_resampling(audio_context.sampleRate);
- audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1);
- ws.send("SET output_rate="+audio_server_output_rate.toString()+" action=start"); //now we'll get AUD packets as well
+ audio_calculate_resampling(audio_context.sampleRate);
+ audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1);
+ }
+ ws.send(JSON.stringify({"type":"dspcontrol","action":"start","params":{"output_rate":audio_server_output_rate}}));
}
function audio_init()
@@ -1751,7 +1820,10 @@ function on_ws_closed()
audio_node.disconnect();
}
catch (dont_care) {}
- divlog("WebSocket has closed unexpectedly. Please reload the page.", 1);
+ audio_initialized = 0;
+ divlog("WebSocket has closed unexpectedly. Attempting to reconnect in 5 seconds...", 1);
+
+ setTimeout(open_websocket, 5000);
}
function on_ws_error(event)
@@ -1763,14 +1835,15 @@ String.prototype.startswith=function(str){ return this.indexOf(str) == 0; }; //h
function open_websocket()
{
- //if(ws_url.startswith("ws://localhost:")&&window.location.hostname!="127.0.0.1"&&window.location.hostname!="localhost")
- //{
- //divlog("Server administrator should set server_hostname correctly, because it is left as \"localhost\". Now guessing hostname from page URL.",1);
- ws_url="ws://"+(window.location.origin.split("://")[1])+"/ws/"; //guess automatically -> now default behaviour
- //}
+ var protocol = 'ws';
+ if (window.location.toString().startsWith('https://')) {
+ protocol = 'wss';
+ }
+
+ ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; //guess automatically -> now default behaviour
if (!("WebSocket" in window))
divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser.");
- ws = new WebSocket(ws_url+client_id);
+ ws = new WebSocket(ws_url);
ws.onopen = on_ws_opened;
ws.onmessage = on_ws_recv;
ws.onclose = on_ws_closed;
@@ -2023,27 +2096,23 @@ function waterfall_add(data)
waterfall_image.data[base+x*4+i] = ((color>>>0)>>((3-i)*8))&0xff;
}*/
- if(mathbox_mode==MATHBOX_MODES.WATERFALL)
- {
+ if (mathbox_mode==MATHBOX_MODES.WATERFALL) {
//Handle mathbox
for(var i=0;i>>0)>>((3-i)*8))&0xff;
- }
+ } else {
+ //Add line to waterfall image
+ oneline_image = canvas_context.createImageData(w,1);
+ for (x=0;x>>0)>>((3-i)*8))&0xff;
+ }
- //Draw image
- canvas_context.putImageData(oneline_image, 0, canvas_actual_line--);
- shift_canvases();
- if(canvas_actual_line<0) add_canvas();
+ //Draw image
+ canvas_context.putImageData(oneline_image, 0, canvas_actual_line--);
+ shift_canvases();
+ if(canvas_actual_line<0) add_canvas();
}
@@ -2265,6 +2334,7 @@ function openwebrx_init()
init_rx_photo();
open_websocket();
secondary_demod_init();
+ digimodes_init();
place_panels(first_show_panel);
window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000);
window.addEventListener("resize",openwebrx_resize);
@@ -2272,7 +2342,26 @@ function openwebrx_init()
//Synchronise volume with slider
updateVolume();
- waterfallColorsDefault();
+
+}
+
+function digimodes_init() {
+ hide_digitalvoice_panels();
+
+ // initialze DMR timeslot muting
+ $('.openwebrx-dmr-timeslot-panel').click(function(e) {
+ $(e.currentTarget).toggleClass("muted");
+ update_dmr_timeslot_filtering();
+ });
+}
+
+function update_dmr_timeslot_filtering() {
+ var filter = $('.openwebrx-dmr-timeslot-panel').map(function(index, el){
+ return (!$(el).hasClass("muted")) << index;
+ }).toArray().reduce(function(acc, v){
+ return acc | v;
+ }, 0);
+ webrx_set_param("dmr_filter", filter);
}
function iosPlayButtonClick()
@@ -2281,6 +2370,7 @@ function iosPlayButtonClick()
audio_init();
e("openwebrx-big-grey").style.opacity=0;
window.setTimeout(function(){ e("openwebrx-big-grey").style.display="none"; },1100);
+ audio_allowed = 1;
}
/*
@@ -2361,6 +2451,7 @@ function pop_bottommost_panel(from)
function toggle_panel(what, on)
{
var item=e(what);
+ if (!item) return;
if(typeof on !== "undefined")
{
if(item.openwebrxHidden && !on) return;
@@ -2424,7 +2515,7 @@ function place_panels(function_apply)
for(i=0;i= 0)
{
if(c.openwebrxHidden)
{
@@ -2491,29 +2582,27 @@ function progressbar_set(obj,val,text,over)
function demodulator_buttons_update()
{
$(".openwebrx-demodulator-button").removeClass("highlighted");
- if(secondary_demod) $("#openwebrx-button-dig").addClass("highlighted");
- else switch(demodulators[0].subtype)
- {
- case "nfm":
- $("#openwebrx-button-nfm").addClass("highlighted");
- break;
- case "am":
- $("#openwebrx-button-am").addClass("highlighted");
- break;
- case "lsb":
- case "usb":
- case "cw":
- if(demodulators[0].high_cut-demodulators[0].low_cut<300)
- $("#openwebrx-button-cw").addClass("highlighted");
- else
- {
- if(demodulators[0].high_cut<0)
- $("#openwebrx-button-lsb").addClass("highlighted");
- else if(demodulators[0].low_cut>0)
- $("#openwebrx-button-usb").addClass("highlighted");
- else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted");
- }
- break;
+ if(secondary_demod) {
+ $("#openwebrx-button-dig").addClass("highlighted");
+ } else switch(demodulators[0].subtype) {
+ case "lsb":
+ case "usb":
+ case "cw":
+ if(demodulators[0].high_cut-demodulators[0].low_cut<300)
+ $("#openwebrx-button-cw").addClass("highlighted");
+ else
+ {
+ if(demodulators[0].high_cut<0)
+ $("#openwebrx-button-lsb").addClass("highlighted");
+ else if(demodulators[0].low_cut>0)
+ $("#openwebrx-button-usb").addClass("highlighted");
+ else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted");
+ }
+ break;
+ default:
+ var mod = demodulators[0].subtype;
+ $("#openwebrx-button-" + mod).addClass("highlighted");
+ break;
}
}
function demodulator_analog_replace_last() { demodulator_analog_replace(last_analog_demodulator_subtype); }
@@ -2616,19 +2705,19 @@ function secondary_demod_init()
function secondary_demod_start(subtype)
{
secondary_demod_canvases_initialized = false;
- ws.send("SET secondary_mod="+subtype);
- secondary_demod = subtype;
+ ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":subtype}}));
+ secondary_demod = subtype;
}
function secondary_demod_set()
{
- ws.send("SET secondary_offset_freq="+secondary_demod_offset_freq.toString());
+ ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_offset_freq":secondary_demod_offset_freq}}));
}
function secondary_demod_stop()
{
- ws.send("SET secondary_mod=off");
- secondary_demod = false;
+ ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":false}}));
+ secondary_demod = false;
secondary_demod_waterfall_queue = [];
}
@@ -2755,7 +2844,7 @@ function secondary_demod_update_channel_freq_from_event(evt)
{
secondary_demod_waiting_for_set = true;
window.setTimeout(()=>{
- ws.send("SET secondary_offset_freq="+Math.floor(secondary_demod_channel_freq));
+ ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_offset_freq":Math.floor(secondary_demod_channel_freq)}}));
//console.log("doneset:", secondary_demod_channel_freq);
secondary_demod_waiting_for_set = false;
}, 50);
@@ -2815,3 +2904,8 @@ function secondary_demod_waterfall_set_zoom(low_cut, high_cut)
secondary_demod_canvases.map((x)=>{$(x).css("left",secondary_demod_canvas_left+"px").css("width",secondary_demod_canvas_width+"px");});
secondary_demod_update_channel_freq_from_event();
}
+
+function sdr_profile_changed() {
+ value = $('#openwebrx-sdr-profiles-listbox').val();
+ ws.send(JSON.stringify({ type:"selectprofile", params:{ profile:value }}));
+}
diff --git a/openwebrx.py b/openwebrx.py
old mode 100755
new mode 100644
index ed5af8c..99b1419
--- a/openwebrx.py
+++ b/openwebrx.py
@@ -1,757 +1,52 @@
-#!/usr/bin/python2
-print "" # python2.7 is required to run OpenWebRX instead of python3. Please run me by: python2 openwebrx.py
-"""
+from http.server import HTTPServer
+from owrx.http import RequestHandler
+from owrx.config import PropertyManager
+from owrx.feature import FeatureDetector
+from owrx.source import SdrService, ClientRegistry
+from socketserver import ThreadingMixIn
+from owrx.sdrhu import SdrHuUpdater
- This file is part of OpenWebRX,
- an open-source SDR receiver software with a web UI.
- Copyright (c) 2013-2015 by Andras Retzler
+import logging
+logging.basicConfig(level = logging.DEBUG, format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-
-"""
-sw_version="v0.17"
-#0.15 (added nmux)
-
-import os
-import code
-import importlib
-import csdr
-import thread
-import time
-import datetime
-import subprocess
-import os
-from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
-from SocketServer import ThreadingMixIn
-import fcntl
-import time
-import md5
-import random
-import threading
-import sys
-import traceback
-from collections import namedtuple
-import Queue
-import ctypes
-
-#import rtl_mus
-import rxws
-import uuid
-import signal
-import socket
-
-try: import sdrhu
-except: sdrhu=False
-avatar_ctime=""
-
-#pypy compatibility
-try: import dl
-except: pass
-try: import __pypy__
-except: pass
-pypy="__pypy__" in globals()
-
-"""
-def import_all_plugins(directory):
- for subdir in os.listdir(directory):
- if os.path.isdir(directory+subdir) and not subdir[0]=="_":
- exact_path=directory+subdir+"/plugin.py"
- if os.path.isfile(exact_path):
- importname=(directory+subdir+"/plugin").replace("/",".")
- print "[openwebrx-import] Found plugin:",importname
- importlib.import_module(importname)
-"""
-
-class MultiThreadHTTPServer(ThreadingMixIn, HTTPServer):
+class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
pass
-def handle_signal(sig, frame):
- global spectrum_dsp
- if sig == signal.SIGUSR1:
- print "[openwebrx] Verbose status information on USR1 signal"
- print
- print "time.time() =", time.time()
- print "clients_mutex.locked() =", clients_mutex.locked()
- print "clients_mutex_locker =", clients_mutex_locker
- if server_fail: print "server_fail = ", server_fail
- print "spectrum_thread_watchdog_last_tick =", spectrum_thread_watchdog_last_tick
- print
- print "clients:",len(clients)
- for client in clients:
- print
- for key in client._fields:
- print "\t%s = %s"%(key,str(getattr(client,key)))
- elif sig == signal.SIGUSR2:
- code.interact(local=globals())
- else:
- print "[openwebrx] Ctrl+C: aborting."
- cleanup_clients(True)
- spectrum_dsp.stop()
- os._exit(1) #not too graceful exit
-
-def access_log(data):
- global logs
- logs.access_log.write("["+datetime.datetime.now().isoformat()+"] "+data+"\n")
- logs.access_log.flush()
-
-receiver_failed=spectrum_thread_watchdog_last_tick=rtl_thread=spectrum_dsp=server_fail=None
def main():
- global clients, clients_mutex, pypy, lock_try_time, avatar_ctime, cfg, logs
- global serverfail, rtl_thread
- print
- print "OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package"
- print "_________________________________________________________________________________________________"
- print
- print "Author contact info: Andras Retzler, HA7ILM "
- print
+ print("""
- no_arguments=len(sys.argv)==1
- if no_arguments: print "[openwebrx-main] Configuration script not specified. I will use: \"config_webrx.py\""
- cfg=__import__("config_webrx" if no_arguments else sys.argv[1])
- for option in ("access_log","csdr_dynamic_bufsize","csdr_print_bufsizes","csdr_through"):
- if not option in dir(cfg): setattr(cfg, option, False) #initialize optional config parameters
+OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package
+_________________________________________________________________________________________________
- #Open log files
- logs = type("logs_class", (object,), {"access_log":open(cfg.access_log if cfg.access_log else "/dev/null","a"), "error_log":""})()
+Author contact info: Andras Retzler, HA7ILM
- #Set signal handler
- signal.signal(signal.SIGINT, handle_signal) #http://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python
- signal.signal(signal.SIGUSR1, handle_signal)
- signal.signal(signal.SIGUSR2, handle_signal)
+ """)
- #Pypy
- if pypy: print "pypy detected (and now something completely different: c code is expected to run at a speed of 3*10^8 m/s?)"
+ pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
- #Change process name to "openwebrx" (to be seen in ps)
- try:
- for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]:
- if os.path.exists(libcpath):
- libc = dl.open(libcpath)
- libc.call("prctl", 15, "openwebrx", 0, 0, 0)
- break
- except:
- pass
-
- #Start rtl thread
- if os.system("csdr 2> /dev/null") == 32512: #check for csdr
- print "[openwebrx-main] You need to install \"csdr\" to run OpenWebRX!\n"
+ featureDetector = FeatureDetector()
+ if not featureDetector.is_available("core"):
+ print("you are missing required dependencies to run openwebrx. "
+ "please check that the following core requirements are installed:")
+ print(", ".join(featureDetector.get_requirements("core")))
return
- if os.system("nmux --help 2> /dev/null") == 32512: #check for nmux
- print "[openwebrx-main] You need to install an up-to-date version of \"csdr\" that contains the \"nmux\" tool to run OpenWebRX! Please upgrade \"csdr\"!\n"
- return
- if cfg.start_rtl_thread:
- nmux_bufcnt = nmux_bufsize = 0
- while nmux_bufsize < cfg.samp_rate/4: nmux_bufsize += 4096
- while nmux_bufsize * nmux_bufcnt < cfg.nmux_memory * 1e6: nmux_bufcnt += 1
- if nmux_bufcnt == 0 or nmux_bufsize == 0:
- print "[openwebrx-main] Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py"
- return
- print "[openwebrx-main] nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)
- cfg.start_rtl_command += "| nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, cfg.iq_server_port)
- rtl_thread=threading.Thread(target = lambda:subprocess.Popen(cfg.start_rtl_command, shell=True), args=())
- rtl_thread.start()
- print "[openwebrx-main] Started rtl_thread: "+cfg.start_rtl_command
- print "[openwebrx-main] Waiting for I/Q server to start..."
- while True:
- testsock=socket.socket()
- try: testsock.connect(("127.0.0.1", cfg.iq_server_port))
- except:
- time.sleep(0.1)
- continue
- testsock.close()
- break
- print "[openwebrx-main] I/Q server started."
- #Initialize clients
- clients=[]
- clients_mutex=threading.Lock()
- lock_try_time=0
+ # Get error messages about unknown / unavailable features as soon as possible
+ SdrService.loadProps()
- #Start watchdog thread
- print "[openwebrx-main] Starting watchdog threads."
- mutex_test_thread=threading.Thread(target = mutex_test_thread_function, args = ())
- mutex_test_thread.start()
- mutex_watchdog_thread=threading.Thread(target = mutex_watchdog_thread_function, args = ())
- mutex_watchdog_thread.start()
+ if "sdrhu_key" in pm and pm["sdrhu_public_listing"]:
+ updater = SdrHuUpdater()
+ updater.start()
+
+ server = ThreadedHttpServer(('0.0.0.0', pm.getPropertyValue("web_port")), RequestHandler)
+ server.serve_forever()
- #Start spectrum thread
- print "[openwebrx-main] Starting spectrum thread."
- spectrum_thread=threading.Thread(target = spectrum_thread_function, args = ())
- spectrum_thread.start()
- #spectrum_watchdog_thread=threading.Thread(target = spectrum_watchdog_thread_function, args = ())
- #spectrum_watchdog_thread.start()
-
- get_cpu_usage()
- bcastmsg_thread=threading.Thread(target = bcastmsg_thread_function, args = ())
- bcastmsg_thread.start()
-
- #threading.Thread(target = measure_thread_function, args = ()).start()
-
- #Start sdr.hu update thread
- if sdrhu and cfg.sdrhu_key and cfg.sdrhu_public_listing:
- print "[openwebrx-main] Starting sdr.hu update thread..."
- avatar_ctime=str(os.path.getctime("htdocs/gfx/openwebrx-avatar.png"))
- sdrhu_thread=threading.Thread(target = sdrhu.run, args = ())
- sdrhu_thread.start()
-
- #Start HTTP thread
- httpd = MultiThreadHTTPServer(('', cfg.web_port), WebRXHandler)
- print('[openwebrx-main] Starting HTTP server.')
- access_log("Starting OpenWebRX...")
- httpd.serve_forever()
-
-
-# This is a debug function below:
-measure_value=0
-def measure_thread_function():
- global measure_value
- while True:
- print "[openwebrx-measure] value is",measure_value
- measure_value=0
- time.sleep(1)
-
-def bcastmsg_thread_function():
- global clients
- while True:
- time.sleep(3)
- try: cpu_usage=get_cpu_usage()
- except: cpu_usage=0
- cma("bcastmsg_thread")
- for i in range(0,len(clients)):
- clients[i].bcastmsg="MSG cpu_usage={0} clients={1}".format(int(cpu_usage*100),len(clients))
- cmr()
-
-def mutex_test_thread_function():
- global clients_mutex, lock_try_time
- while True:
- time.sleep(0.5)
- lock_try_time=time.time()
- clients_mutex.acquire()
- clients_mutex.release()
- lock_try_time=0
-
-def cma(what): #clients_mutex acquire
- global clients_mutex
- global clients_mutex_locker
- if not clients_mutex.locked(): clients_mutex_locker = what
- clients_mutex.acquire()
-
-def cmr():
- global clients_mutex
- global clients_mutex_locker
- clients_mutex_locker = None
- clients_mutex.release()
-
-def mutex_watchdog_thread_function():
- global lock_try_time
- global clients_mutex_locker
- global clients_mutex
- while True:
- if lock_try_time != 0 and time.time()-lock_try_time > 3.0:
- #if 3 seconds pass without unlock
- print "[openwebrx-mutex-watchdog] Mutex unlock timeout. Locker: \""+str(clients_mutex_locker)+"\" Now unlocking..."
- clients_mutex.release()
- time.sleep(0.5)
-
-def spectrum_watchdog_thread_function():
- global spectrum_thread_watchdog_last_tick, receiver_failed
- while True:
- time.sleep(60)
- if spectrum_thread_watchdog_last_tick and time.time()-spectrum_thread_watchdog_last_tick > 60.0:
- print "[openwebrx-spectrum-watchdog] Spectrum timeout. Seems like no I/Q data is coming from the receiver.\nIf you're using RTL-SDR, the receiver hardware may randomly fail under some circumstances:\n1) high temperature,\n2) insufficient current available from the USB port."
- print "[openwebrx-spectrum-watchdog] Deactivating receiver."
- receiver_failed="spectrum"
- return
-
-def check_server():
- global spectrum_dsp, server_fail, rtl_thread
- if server_fail: return server_fail
- #print spectrum_dsp.process.poll()
- if spectrum_dsp and spectrum_dsp.process.poll()!=None: server_fail = "spectrum_thread dsp subprocess failed"
- #if rtl_thread and not rtl_thread.is_alive(): server_fail = "rtl_thread failed"
- if server_fail: print "[openwebrx-check_server] >>>>>>> ERROR:", server_fail
- return server_fail
-
-def apply_csdr_cfg_to_dsp(dsp):
- dsp.csdr_dynamic_bufsize = cfg.csdr_dynamic_bufsize
- dsp.csdr_print_bufsizes = cfg.csdr_print_bufsizes
- dsp.csdr_through = cfg.csdr_through
-
-def spectrum_thread_function():
- global clients, spectrum_dsp, spectrum_thread_watchdog_last_tick
- spectrum_dsp=dsp=csdr.dsp()
- dsp.nc_port=cfg.iq_server_port
- dsp.set_demodulator("fft")
- dsp.set_samp_rate(cfg.samp_rate)
- dsp.set_fft_size(cfg.fft_size)
- dsp.set_fft_fps(cfg.fft_fps)
- dsp.set_fft_averages(int(round(1.0 * cfg.samp_rate / cfg.fft_size / cfg.fft_fps / (1.0 - cfg.fft_voverlap_factor))) if cfg.fft_voverlap_factor>0 else 0)
- dsp.set_fft_compression(cfg.fft_compression)
- dsp.set_format_conversion(cfg.format_conversion)
- apply_csdr_cfg_to_dsp(dsp)
- sleep_sec=0.87/cfg.fft_fps
- print "[openwebrx-spectrum] Spectrum thread initialized successfully."
- dsp.start()
- if cfg.csdr_dynamic_bufsize:
- dsp.read_async(8) #dummy read to skip bufsize & preamble
- print "[openwebrx-spectrum] Note: CSDR_DYNAMIC_BUFSIZE_ON = 1"
- print "[openwebrx-spectrum] Spectrum thread started."
- bytes_to_read=int(dsp.get_fft_bytes_to_read())
- spectrum_thread_counter=0
- while True:
- data=dsp.read_async(bytes_to_read)
- if data is None:
- time.sleep(.01)
- continue
- #print "gotcha",len(data),"bytes of spectrum data via spectrum_thread_function()"
- if spectrum_thread_counter >= cfg.fft_fps:
- spectrum_thread_counter=0
- spectrum_thread_watchdog_last_tick = time.time() #once every second
- else: spectrum_thread_counter+=1
- cma("spectrum_thread")
- correction=0
- for i in range(0,len(clients)):
- i-=correction
- if (clients[i].ws_started):
- if clients[i].spectrum_queue.full():
- print "[openwebrx-spectrum] client spectrum queue full, closing it."
- close_client(i, False)
- correction+=1
- else:
- clients[i].spectrum_queue.put([data]) # add new string by "reference" to all clients
- cmr()
-
-def get_client_by_id(client_id, use_mutex=True):
- global clients
- output=-1
- if use_mutex: cma("get_client_by_id")
- for i in range(0,len(clients)):
- if(clients[i].id==client_id):
- output=i
- break
- if use_mutex: cmr()
- if output==-1:
- raise ClientNotFoundException
- else:
- return output
-
-def log_client(client, what):
- print "[openwebrx-httpd] client {0}#{1} :: {2}".format(client.ip,client.id,what)
-
-def cleanup_clients(end_all=False):
- # - if a client doesn't open websocket for too long time, we drop it
- # - or if end_all is true, we drop all clients
- global clients
- cma("cleanup_clients")
- correction=0
- for i in range(0,len(clients)):
- i-=correction
- #print "cleanup_clients:: len(clients)=", len(clients), "i=", i
- if end_all or ((not clients[i].ws_started) and (time.time()-clients[i].gen_time)>45):
- if not end_all: print "[openwebrx] cleanup_clients :: client timeout to open WebSocket"
- close_client(i, False)
- correction+=1
- cmr()
-
-def generate_client_id(ip):
- #add a client
- global clients
- new_client=namedtuple("ClientStruct", "id gen_time ws_started sprectum_queue ip closed bcastmsg dsp loopstat")
- new_client.id=md5.md5(str(random.random())).hexdigest()
- new_client.gen_time=time.time()
- new_client.ws_started=False # to check whether client has ever tried to open the websocket
- new_client.spectrum_queue=Queue.Queue(1000)
- new_client.ip=ip
- new_client.bcastmsg=""
- new_client.closed=[False] #byref, not exactly sure if required
- new_client.dsp=None
- cma("generate_client_id")
- clients.append(new_client)
- log_client(new_client,"client added. Clients now: {0}".format(len(clients)))
- cmr()
- cleanup_clients()
- return new_client.id
-
-def close_client(i, use_mutex=True):
- global clients
- log_client(clients[i],"client being closed.")
- if use_mutex: cma("close_client")
+if __name__ == "__main__":
try:
- clients[i].dsp.stop()
- except:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- print "[openwebrx] close_client dsp.stop() :: error -",exc_type,exc_value
- traceback.print_tb(exc_traceback)
- clients[i].closed[0]=True
- access_log("Stopped streaming to client: "+clients[i].ip+"#"+str(clients[i].id)+" (users now: "+str(len(clients)-1)+")")
- del clients[i]
- if use_mutex: cmr()
-
-# http://www.codeproject.com/Articles/462525/Simple-HTTP-Server-and-Client-in-Python
-# some ideas are used from the artice above
-
-class WebRXHandler(BaseHTTPRequestHandler):
- def proc_read_thread():
- pass
-
- def send_302(self,what):
- self.send_response(302)
- self.send_header('Content-type','text/html')
- self.send_header("Location", "http://{0}:{1}/{2}".format(cfg.server_hostname,cfg.web_port,what))
- self.end_headers()
- self.wfile.write("Object moved
Please click here to continue.".format(what))
-
-
- def do_GET(self):
- self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
- global dsp_plugin, clients_mutex, clients, avatar_ctime, sw_version, receiver_failed
- rootdir = 'htdocs'
- self.path=self.path.replace("..","")
- path_temp_parts=self.path.split("?")
- self.path=path_temp_parts[0]
- request_param=path_temp_parts[1] if(len(path_temp_parts)>1) else ""
- access_log("GET "+self.path+" from "+self.client_address[0])
- try:
- if self.path=="/":
- self.path="/index.wrx"
- # there's even another cool tip at http://stackoverflow.com/questions/4419650/how-to-implement-timeout-in-basehttpserver-basehttprequesthandler-python
- #if self.path[:5]=="/lock": cma("do_GET /lock/") # to test mutex_watchdog_thread. Do not uncomment in production environment!
- if self.path[:4]=="/ws/":
- print "[openwebrx-ws] Client requested WebSocket connection"
- if receiver_failed: self.send_error(500,"Internal server error")
- try:
- # ========= WebSocket handshake =========
- ws_success=True
- try:
- rxws.handshake(self)
- cma("do_GET /ws/")
- client_i=get_client_by_id(self.path[4:], False)
- myclient=clients[client_i]
- except rxws.WebSocketException: ws_success=False
- except ClientNotFoundException: ws_success=False
- finally:
- if clients_mutex.locked(): cmr()
- if not ws_success:
- self.send_error(400, 'Bad request.')
- return
-
- # ========= Client handshake =========
- if myclient.ws_started:
- print "[openwebrx-httpd] error: second WS connection with the same client id, throwing it."
- self.send_error(400, 'Bad request.') #client already started
- return
- rxws.send(self, "CLIENT DE SERVER openwebrx.py")
- client_ans=rxws.recv(self, True)
- if client_ans[:16]!="SERVER DE CLIENT":
- rxws.send("ERR Bad answer.")
- return
- myclient.ws_started=True
- #send default parameters
- rxws.send(self, "MSG center_freq={0} bandwidth={1} fft_size={2} fft_fps={3} audio_compression={4} fft_compression={5} max_clients={6} setup".format(str(cfg.shown_center_freq),str(cfg.samp_rate),cfg.fft_size,cfg.fft_fps,cfg.audio_compression,cfg.fft_compression,cfg.max_clients))
-
- # ========= Initialize DSP =========
- dsp=csdr.dsp()
- dsp_initialized=False
- dsp.set_audio_compression(cfg.audio_compression)
- dsp.set_fft_compression(cfg.fft_compression) #used by secondary chains
- dsp.set_format_conversion(cfg.format_conversion)
- dsp.set_offset_freq(0)
- dsp.set_bpf(-4000,4000)
- dsp.set_secondary_fft_size(cfg.digimodes_fft_size)
- dsp.nc_port=cfg.iq_server_port
- apply_csdr_cfg_to_dsp(dsp)
- myclient.dsp=dsp
- do_secondary_demod=False
- access_log("Started streaming to client: "+self.client_address[0]+"#"+myclient.id+" (users now: "+str(len(clients))+")")
-
- while True:
- myclient.loopstat=0
- if myclient.closed[0]:
- print "[openwebrx-httpd:ws] client closed by other thread"
- break
-
- # ========= send audio =========
- if dsp_initialized:
- myclient.loopstat=10
- temp_audio_data=dsp.read_async(256)
- if (temp_audio_data is not None):
- myclient.loopstat=11
- rxws.send(self, temp_audio_data, "AUD ")
- else:
- #time.sleep((256.0 * 32) / 11025)
- time.sleep(.01)
-
- # ========= send spectrum =========
- while not myclient.spectrum_queue.empty():
- myclient.loopstat=20
- spectrum_data=myclient.spectrum_queue.get()
- #spectrum_data_mid=len(spectrum_data[0])/2
- #rxws.send(self, spectrum_data[0][spectrum_data_mid:]+spectrum_data[0][:spectrum_data_mid], "FFT ")
- # (it seems GNU Radio exchanges the first and second part of the FFT output, we correct it)
- myclient.loopstat=21
- rxws.send(self, spectrum_data[0],"FFT ")
-
- # ========= send smeter_level =========
- smeter_level=None
- while True:
- try:
- myclient.loopstat=30
- smeter_level=dsp.get_smeter_level()
- if smeter_level == None: break
- except:
- break
- if smeter_level!=None:
- myclient.loopstat=31
- rxws.send(self, "MSG s={0}".format(smeter_level))
-
- # ========= send metadata =========
- metadata = None
- while True:
- try:
- myclient.loopstat=35
- metadata = dsp.get_metadata();
- if metadata == None: break
- rxws.send(self, "MET {0}".format(metadata.rstrip("\n")))
- except:
- break
-
- # ========= send bcastmsg =========
- if myclient.bcastmsg!="":
- myclient.loopstat=40
- rxws.send(self,myclient.bcastmsg)
- myclient.bcastmsg=""
-
- # ========= send secondary =========
- if do_secondary_demod:
- myclient.loopstat=41
- while True:
- try:
- secondary_spectrum_data=dsp.read_secondary_fft(dsp.get_secondary_fft_bytes_to_read())
- if len(secondary_spectrum_data) == 0: break
- # print "len(secondary_spectrum_data)", len(secondary_spectrum_data) #TODO digimodes
- rxws.send(self, secondary_spectrum_data, "FFTS")
- except: break
- myclient.loopstat=42
- while True:
- try:
- myclient.loopstat=422
- secondary_demod_data=dsp.read_secondary_demod(1)
- myclient.loopstat=423
- if len(secondary_demod_data) == 0: break
- # print "len(secondary_demod_data)", len(secondary_demod_data), secondary_demod_data #TODO digimodes
- rxws.send(self, secondary_demod_data, "DAT ")
- except: break
-
- # ========= process commands =========
- while True:
- myclient.loopstat=50
- rdata=rxws.recv(self, False)
- myclient.loopstat=51
- #try:
- if not rdata: break
- elif rdata[:3]=="SET":
- print "[openwebrx-httpd:ws,%d] command: %s"%(client_i,rdata)
- pairs=rdata[4:].split(" ")
- bpf_set=False
- new_bpf=dsp.get_bpf()
- filter_limit=dsp.get_output_rate()/2
- for pair in pairs:
- param_name, param_value = pair.split("=")
- if param_name == "low_cut" and -filter_limit <= int(param_value) <= filter_limit:
- bpf_set=True
- new_bpf[0]=int(param_value)
- elif param_name == "high_cut" and -filter_limit <= int(param_value) <= filter_limit:
- bpf_set=True
- new_bpf[1]=int(param_value)
- elif param_name == "offset_freq" and -cfg.samp_rate/2 <= int(param_value) <= cfg.samp_rate/2:
- myclient.loopstat=510
- dsp.set_offset_freq(int(param_value))
- elif param_name == "squelch_level" and float(param_value) >= 0:
- myclient.loopstat=520
- dsp.set_squelch_level(float(param_value))
- elif param_name=="mod":
- if (dsp.get_demodulator()!=param_value):
- myclient.loopstat=530
- if dsp_initialized: dsp.stop()
- dsp.set_demodulator(param_value)
- if dsp_initialized: dsp.start()
- elif param_name == "output_rate":
- if not dsp_initialized:
- myclient.loopstat=540
- dsp.set_output_rate(int(param_value))
- myclient.loopstat=541
- dsp.set_samp_rate(cfg.samp_rate)
- elif param_name=="action" and param_value=="start":
- if not dsp_initialized:
- myclient.loopstat=550
- dsp.start()
- dsp_initialized=True
- elif param_name=="secondary_mod" and cfg.digimodes_enable:
- if (dsp.get_secondary_demodulator() != param_value):
- if dsp_initialized: dsp.stop()
- if param_value == "off":
- dsp.set_secondary_demodulator(None)
- do_secondary_demod = False
- else:
- dsp.set_secondary_demodulator(param_value)
- do_secondary_demod = True
- rxws.send(self, "MSG secondary_fft_size={0} if_samp_rate={1} secondary_bw={2} secondary_setup".format(cfg.digimodes_fft_size, dsp.if_samp_rate(), dsp.secondary_bw()))
- if dsp_initialized: dsp.start()
- elif param_name=="secondary_offset_freq" and 0 <= int(param_value) <= dsp.if_samp_rate()/2 and cfg.digimodes_enable:
- dsp.set_secondary_offset_freq(int(param_value))
- else:
- print "[openwebrx-httpd:ws] invalid parameter"
- if bpf_set:
- myclient.loopstat=560
- dsp.set_bpf(*new_bpf)
- #code.interact(local=locals())
- except:
- myclient.loopstat=990
- exc_type, exc_value, exc_traceback = sys.exc_info()
- print "[openwebrx-httpd:ws] exception: ",exc_type,exc_value
- traceback.print_tb(exc_traceback) #TODO digimodes
- #if exc_value[0]==32: #"broken pipe", client disconnected
- # pass
- #elif exc_value[0]==11: #"resource unavailable" on recv, client disconnected
- # pass
- #else:
- # print "[openwebrx-httpd] error in /ws/ handler: ",exc_type,exc_value
- # traceback.print_tb(exc_traceback)
-
- #stop dsp for the disconnected client
- myclient.loopstat=991
- try:
- dsp.stop()
- del dsp
- except:
- print "[openwebrx-httpd] error in dsp.stop()"
-
- #delete disconnected client
- myclient.loopstat=992
- try:
- cma("do_GET /ws/ delete disconnected")
- id_to_close=get_client_by_id(myclient.id,False)
- close_client(id_to_close,False)
- except:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- print "[openwebrx-httpd] client cannot be closed: ",exc_type,exc_value
- traceback.print_tb(exc_traceback)
- finally:
- cmr()
- myclient.loopstat=1000
- return
- elif self.path in ("/status", "/status/"):
- #self.send_header('Content-type','text/plain')
- self.send_response(200)
- self.end_headers()
- getbands=lambda: str(int(cfg.shown_center_freq-cfg.samp_rate/2))+"-"+str(int(cfg.shown_center_freq+cfg.samp_rate/2))
- self.wfile.write("status="+("inactive" if receiver_failed else "active")+"\nname="+cfg.receiver_name+"\nsdr_hw="+cfg.receiver_device+"\nop_email="+cfg.receiver_admin+"\nbands="+getbands()+"\nusers="+str(len(clients))+"\nusers_max="+str(cfg.max_clients)+"\navatar_ctime="+avatar_ctime+"\ngps="+str(cfg.receiver_gps)+"\nasl="+str(cfg.receiver_asl)+"\nloc="+cfg.receiver_location+"\nsw_version="+sw_version+"\nantenna="+cfg.receiver_ant+"\n")
- print "[openwebrx-httpd] GET /status/ from",self.client_address[0]
- else:
- f=open(rootdir+self.path)
- data=f.read()
- extension=self.path[(len(self.path)-4):len(self.path)]
- extension=extension[2:] if extension[1]=='.' else extension[1:]
- checkresult=check_server()
- if extension == "wrx" and (checkresult or receiver_failed):
- self.send_302("inactive.html")
- return
- anyStringsPresentInUserAgent=lambda a: reduce(lambda x,y:x or y, map(lambda b:self.headers['user-agent'].count(b), a), False)
- if extension == "wrx" and ( (not anyStringsPresentInUserAgent(("Chrome","Firefox","Googlebot","iPhone","iPad","iPod"))) if 'user-agent' in self.headers.keys() else True ) and (not request_param.count("unsupported")):
- self.send_302("upgrade.html")
- return
- if extension == "wrx":
- cleanup_clients(False)
- if cfg.max_clients<=len(clients):
- self.send_302("retry.html")
- return
- self.send_response(200)
- if(("wrx","html","htm").count(extension)):
- self.send_header('Content-type','text/html')
- elif(extension=="js"):
- self.send_header('Content-type','text/javascript')
- elif(extension=="css"):
- self.send_header('Content-type','text/css')
- self.end_headers()
- if extension == "wrx":
- replace_dictionary=(
- ("%[RX_PHOTO_DESC]",cfg.photo_desc),
- ("%[CLIENT_ID]", generate_client_id(self.client_address[0])) if "%[CLIENT_ID]" in data else "",
- ("%[WS_URL]","ws://"+cfg.server_hostname+":"+str(cfg.web_port)+"/ws/"),
- ("%[RX_TITLE]",cfg.receiver_name),
- ("%[RX_LOC]",cfg.receiver_location),
- ("%[RX_QRA]",cfg.receiver_qra),
- ("%[RX_ASL]",str(cfg.receiver_asl)),
- ("%[RX_GPS]",str(cfg.receiver_gps[0])+","+str(cfg.receiver_gps[1])),
- ("%[RX_PHOTO_HEIGHT]",str(cfg.photo_height)),("%[RX_PHOTO_TITLE]",cfg.photo_title),
- ("%[RX_ADMIN]",cfg.receiver_admin),
- ("%[RX_ANT]",cfg.receiver_ant),
- ("%[RX_DEVICE]",cfg.receiver_device),
- ("%[AUDIO_BUFSIZE]",str(cfg.client_audio_buffer_size)),
- ("%[START_OFFSET_FREQ]",str(cfg.start_freq-cfg.center_freq)),
- ("%[START_MOD]",cfg.start_mod),
- ("%[WATERFALL_COLORS]",cfg.waterfall_colors),
- ("%[WATERFALL_MIN_LEVEL]",str(cfg.waterfall_min_level)),
- ("%[WATERFALL_MAX_LEVEL]",str(cfg.waterfall_max_level)),
- ("%[WATERFALL_AUTO_LEVEL_MARGIN]","[%d,%d]"%cfg.waterfall_auto_level_margin),
- ("%[DIGIMODES_ENABLE]",("true" if cfg.digimodes_enable else "false")),
- ("%[MATHBOX_WATERFALL_FRES]",str(cfg.mathbox_waterfall_frequency_resolution)),
- ("%[MATHBOX_WATERFALL_THIST]",str(cfg.mathbox_waterfall_history_length)),
- ("%[MATHBOX_WATERFALL_COLORS]",cfg.mathbox_waterfall_colors)
- )
- for rule in replace_dictionary:
- while data.find(rule[0])!=-1:
- data=data.replace(rule[0],rule[1])
- self.wfile.write(data)
- f.close()
- return
- except IOError:
- self.send_error(404, 'Invalid path.')
- except:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- print "[openwebrx-httpd] error (@outside):", exc_type, exc_value
- traceback.print_tb(exc_traceback)
-
-
-class ClientNotFoundException(Exception):
- pass
-
-last_worktime=0
-last_idletime=0
-
-def get_cpu_usage():
- global last_worktime, last_idletime
- try:
- f=open("/proc/stat","r")
- except:
- return 0 #Workaround, possibly we're on a Mac
- line=""
- while not "cpu " in line: line=f.readline()
- f.close()
- spl=line.split(" ")
- worktime=int(spl[2])+int(spl[3])+int(spl[4])
- idletime=int(spl[5])
- dworktime=(worktime-last_worktime)
- didletime=(idletime-last_idletime)
- rate=float(dworktime)/(didletime+dworktime)
- last_worktime=worktime
- last_idletime=idletime
- if(last_worktime==0): return 0
- return rate
-
-
-if __name__=="__main__":
- main()
+ main()
+ except KeyboardInterrupt:
+ for c in ClientRegistry.getSharedInstance().clients:
+ c.close()
diff --git a/owrx/config.py b/owrx/config.py
new file mode 100644
index 0000000..6db151e
--- /dev/null
+++ b/owrx/config.py
@@ -0,0 +1,129 @@
+import logging
+logger = logging.getLogger(__name__)
+
+
+class Subscription(object):
+ def __init__(self, subscriptee, subscriber):
+ self.subscriptee = subscriptee
+ self.subscriber = subscriber
+
+ def call(self, *args, **kwargs):
+ self.subscriber(*args, **kwargs)
+
+ def cancel(self):
+ self.subscriptee.unwire(self)
+
+
+class Property(object):
+ def __init__(self, value = None):
+ self.value = value
+ self.subscribers = []
+
+ def getValue(self):
+ return self.value
+
+ def setValue(self, value):
+ if (self.value == value):
+ return self
+ self.value = value
+ for c in self.subscribers:
+ try:
+ c.call(self.value)
+ except Exception as e:
+ logger.exception(e)
+ return self
+
+ def wire(self, callback):
+ sub = Subscription(self, callback)
+ self.subscribers.append(sub)
+ if not self.value is None: sub.call(self.value)
+ return sub
+
+ def unwire(self, sub):
+ try:
+ self.subscribers.remove(sub)
+ except ValueError:
+ # happens when already removed before
+ pass
+ return self
+
+class PropertyManager(object):
+ sharedInstance = None
+ @staticmethod
+ def getSharedInstance():
+ if PropertyManager.sharedInstance is None:
+ PropertyManager.sharedInstance = PropertyManager()
+ return PropertyManager.sharedInstance
+
+ def collect(self, *props):
+ return PropertyManager({name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props})
+
+ def __init__(self, properties = None):
+ self.properties = {}
+ self.subscribers = []
+ if properties is not None:
+ for (name, prop) in properties.items():
+ self.add(name, prop)
+
+ def add(self, name, prop):
+ self.properties[name] = prop
+ def fireCallbacks(value):
+ for c in self.subscribers:
+ try:
+ c.call(name, value)
+ except Exception as e:
+ logger.exception(e)
+ prop.wire(fireCallbacks)
+ return self
+
+ def __contains__(self, name):
+ return self.hasProperty(name)
+
+ def __getitem__(self, name):
+ return self.getPropertyValue(name)
+
+ def __setitem__(self, name, value):
+ if not self.hasProperty(name):
+ self.add(name, Property())
+ self.getProperty(name).setValue(value)
+
+ def __dict__(self):
+ return {k:v.getValue() for k, v in self.properties.items()}
+
+ def hasProperty(self, name):
+ return name in self.properties
+
+ def getProperty(self, name):
+ if not self.hasProperty(name):
+ self.add(name, Property())
+ return self.properties[name]
+
+ def getPropertyValue(self, name):
+ return self.getProperty(name).getValue()
+
+ def wire(self, callback):
+ sub = Subscription(self, callback)
+ self.subscribers.append(sub)
+ return sub
+
+ def unwire(self, sub):
+ try:
+ self.subscribers.remove(sub)
+ except ValueError:
+ # happens when already removed before
+ pass
+ return self
+
+ def defaults(self, other_pm):
+ for (key, p) in self.properties.items():
+ if p.getValue() is None:
+ p.setValue(other_pm[key])
+ return self
+
+ def loadConfig(self, filename):
+ cfg = __import__(filename)
+ for name, value in cfg.__dict__.items():
+ if name.startswith("__"):
+ continue
+ self[name] = value
+ return self
diff --git a/owrx/connection.py b/owrx/connection.py
new file mode 100644
index 0000000..67ee96b
--- /dev/null
+++ b/owrx/connection.py
@@ -0,0 +1,191 @@
+from owrx.config import PropertyManager
+from owrx.source import DspManager, CpuUsageThread, SdrService, ClientRegistry
+from owrx.feature import FeatureDetector
+import json
+
+import logging
+logger = logging.getLogger(__name__)
+
+class OpenWebRxClient(object):
+ config_keys = ["waterfall_colors", "waterfall_min_level", "waterfall_max_level",
+ "waterfall_auto_level_margin", "lfo_offset", "samp_rate", "fft_size", "fft_fps",
+ "audio_compression", "fft_compression", "max_clients", "start_mod",
+ "client_audio_buffer_size", "start_freq", "center_freq", "mathbox_waterfall_colors",
+ "mathbox_waterfall_history_length", "mathbox_waterfall_frequency_resolution"]
+ def __init__(self, conn):
+ self.conn = conn
+
+ self.dsp = None
+ self.sdr = None
+ self.configSub = None
+
+ ClientRegistry.getSharedInstance().addClient(self)
+
+ pm = PropertyManager.getSharedInstance()
+
+ self.setSdr()
+
+ # send receiver info
+ receiver_keys = ["receiver_name", "receiver_location", "receiver_qra", "receiver_asl", "receiver_gps",
+ "photo_title", "photo_desc"]
+ receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys)
+ self.write_receiver_details(receiver_details)
+
+ profiles = [{"name": s.getName() + " " + p["name"], "id":sid + "|" + pid} for (sid, s) in SdrService.getSources().items() for (pid, p) in s.getProfiles().items()]
+ self.write_profiles(profiles)
+
+ features = FeatureDetector().feature_availability()
+ self.write_features(features)
+
+ CpuUsageThread.getSharedInstance().add_client(self)
+
+ def setSdr(self, id = None):
+ next = SdrService.getSource(id)
+ if (next == self.sdr):
+ return
+
+ self.stopDsp()
+
+ if self.configSub is not None:
+ self.configSub.cancel()
+ self.configSub = None
+
+ self.sdr = next
+
+ # send initial config
+ configProps = self.sdr.getProps().collect(*OpenWebRxClient.config_keys).defaults(PropertyManager.getSharedInstance())
+
+ def sendConfig(key, value):
+ config = dict((key, configProps[key]) for key in OpenWebRxClient.config_keys)
+ # TODO mathematical properties? hmmmm
+ config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
+ self.write_config(config)
+
+ self.configSub = configProps.wire(sendConfig)
+ sendConfig(None, None)
+
+ self.sdr.addSpectrumClient(self)
+
+ def startDsp(self):
+ if self.dsp is None:
+ self.dsp = DspManager(self, self.sdr)
+ self.dsp.start()
+
+ def close(self):
+ self.stopDsp()
+ CpuUsageThread.getSharedInstance().remove_client(self)
+ ClientRegistry.getSharedInstance().removeClient(self)
+ if self.configSub is not None:
+ self.configSub.cancel()
+ self.configSub = None
+ self.conn.close()
+ logger.debug("connection closed")
+
+ def stopDsp(self):
+ if self.dsp is not None:
+ self.dsp.stop()
+ self.dsp = None
+ if self.sdr is not None:
+ self.sdr.removeSpectrumClient(self)
+
+ def setParams(self, params):
+ # only the keys in the protected property manager can be overridden from the web
+ protected = self.sdr.getProps().collect("samp_rate", "center_freq", "rf_gain", "type", "if_gain") \
+ .defaults(PropertyManager.getSharedInstance())
+ for key, value in params.items():
+ protected[key] = value
+
+ def setDspProperties(self, params):
+ for key, value in params.items():
+ self.dsp.setProperty(key, value)
+
+ def protected_send(self, data):
+ try:
+ self.conn.send(data)
+ # these exception happen when the socket is closed
+ except OSError:
+ self.close()
+ except ValueError:
+ self.close()
+
+ def write_spectrum_data(self, data):
+ self.protected_send(bytes([0x01]) + data)
+ def write_dsp_data(self, data):
+ self.protected_send(bytes([0x02]) + data)
+ def write_s_meter_level(self, level):
+ self.protected_send({"type":"smeter","value":level})
+ def write_cpu_usage(self, usage):
+ self.protected_send({"type":"cpuusage","value":usage})
+ def write_clients(self, clients):
+ self.protected_send({"type":"clients","value":clients})
+ def write_secondary_fft(self, data):
+ self.protected_send(bytes([0x03]) + data)
+ def write_secondary_demod(self, data):
+ self.protected_send(bytes([0x04]) + data)
+ def write_secondary_dsp_config(self, cfg):
+ self.protected_send({"type":"secondary_config", "value":cfg})
+ def write_config(self, cfg):
+ self.protected_send({"type":"config","value":cfg})
+ def write_receiver_details(self, details):
+ self.protected_send({"type":"receiver_details","value":details})
+ def write_profiles(self, profiles):
+ self.protected_send({"type":"profiles","value":profiles})
+ def write_features(self, features):
+ self.protected_send({"type":"features","value":features})
+ def write_metadata(self, metadata):
+ self.protected_send({"type":"metadata","value":metadata})
+
+class WebSocketMessageHandler(object):
+ def __init__(self):
+ self.handshake = None
+ self.client = None
+ self.dsp = None
+
+ def handleTextMessage(self, conn, message):
+ if (message[:16] == "SERVER DE CLIENT"):
+ # maybe put some more info in there? nothing to store yet.
+ self.handshake = "completed"
+ logger.debug("client connection intitialized")
+
+ self.client = OpenWebRxClient(conn)
+
+ return
+
+ if not self.handshake:
+ logger.warning("not answering client request since handshake is not complete")
+ return
+
+ try:
+ message = json.loads(message)
+ if "type" in message:
+ if message["type"] == "dspcontrol":
+ if "action" in message and message["action"] == "start":
+ self.client.startDsp()
+
+ if "params" in message:
+ params = message["params"]
+ self.client.setDspProperties(params)
+
+ if message["type"] == "config":
+ if "params" in message:
+ self.client.setParams(message["params"])
+ if message["type"] == "setsdr":
+ if "params" in message:
+ self.client.setSdr(message["params"]["sdr"])
+ if message["type"] == "selectprofile":
+ if "params" in message and "profile" in message["params"]:
+ profile = message["params"]["profile"].split("|")
+ self.client.setSdr(profile[0])
+ self.client.sdr.activateProfile(profile[1])
+ else:
+ logger.warning("received message without type: {0}".format(message))
+
+ except json.JSONDecodeError:
+ logger.warning("message is not json: {0}".format(message))
+
+ def handleBinaryMessage(self, conn, data):
+ logger.error("unsupported binary message, discarding")
+
+ def handleClose(self, conn):
+ if self.client:
+ self.client.close()
diff --git a/owrx/controllers.py b/owrx/controllers.py
new file mode 100644
index 0000000..774ba9b
--- /dev/null
+++ b/owrx/controllers.py
@@ -0,0 +1,87 @@
+import os
+import mimetypes
+from datetime import datetime
+from owrx.websocket import WebSocketConnection
+from owrx.config import PropertyManager
+from owrx.source import ClientRegistry
+from owrx.connection import WebSocketMessageHandler
+from owrx.version import openwebrx_version
+
+import logging
+logger = logging.getLogger(__name__)
+
+class Controller(object):
+ def __init__(self, handler, matches):
+ self.handler = handler
+ self.matches = matches
+ def send_response(self, content, code = 200, content_type = "text/html", last_modified: datetime = None, max_age = None):
+ self.handler.send_response(code)
+ if content_type is not None:
+ self.handler.send_header("Content-Type", content_type)
+ if last_modified is not None:
+ self.handler.send_header("Last-Modified", last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT"))
+ if max_age is not None:
+ self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age))
+ self.handler.end_headers()
+ if (type(content) == str):
+ content = content.encode()
+ self.handler.wfile.write(content)
+ def render_template(self, template, **variables):
+ f = open('htdocs/' + template)
+ data = f.read()
+ f.close()
+
+ self.send_response(data)
+
+class StatusController(Controller):
+ def handle_request(self):
+ pm = PropertyManager.getSharedInstance()
+ # TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna
+ vars = {
+ "status": "active",
+ "name": pm["receiver_name"],
+ "op_email": pm["receiver_admin"],
+ "users": ClientRegistry.getSharedInstance().clientCount(),
+ "users_max": pm["max_clients"],
+ "gps": pm["receiver_gps"],
+ "asl": pm["receiver_asl"],
+ "loc": pm["receiver_location"],
+ "sw_version": openwebrx_version,
+ "avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png")
+ }
+ self.send_response("\n".join(["{key}={value}".format(key = key, value = value) for key, value in vars.items()]))
+
+class AssetsController(Controller):
+ def serve_file(self, file, content_type = None):
+ try:
+ modified = datetime.fromtimestamp(os.path.getmtime('htdocs/' + file))
+
+ if "If-Modified-Since" in self.handler.headers:
+ client_modified = datetime.strptime(self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z")
+ if modified <= client_modified:
+ self.send_response("", code = 304)
+ return
+
+ f = open('htdocs/' + file, 'rb')
+ data = f.read()
+ f.close()
+
+ if content_type is None:
+ (content_type, encoding) = mimetypes.MimeTypes().guess_type(file)
+ self.send_response(data, content_type = content_type, last_modified = modified, max_age = 3600)
+ except FileNotFoundError:
+ self.send_response("file not found", code = 404)
+ def handle_request(self):
+ filename = self.matches.group(1)
+ self.serve_file(filename)
+
+class IndexController(AssetsController):
+ def handle_request(self):
+ self.serve_file("index.html", content_type = "text/html")
+
+class WebSocketController(Controller):
+ def handle_request(self):
+ conn = WebSocketConnection(self.handler, WebSocketMessageHandler())
+ conn.send("CLIENT DE SERVER openwebrx.py")
+ # enter read loop
+ conn.read_loop()
diff --git a/owrx/feature.py b/owrx/feature.py
new file mode 100644
index 0000000..38b8e27
--- /dev/null
+++ b/owrx/feature.py
@@ -0,0 +1,122 @@
+import os
+import subprocess
+from functools import reduce
+from operator import and_
+import re
+from distutils.version import LooseVersion
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class UnknownFeatureException(Exception):
+ pass
+
+class FeatureDetector(object):
+ features = {
+ "core": [ "csdr", "nmux", "nc" ],
+ "rtl_sdr": [ "rtl_sdr" ],
+ "sdrplay": [ "rx_tools" ],
+ "hackrf": [ "hackrf_transfer" ],
+ "airspy": [ "airspy_rx" ],
+ "digital_voice_digiham": [ "digiham", "sox" ],
+ "digital_voice_dsd": [ "dsd", "sox", "digiham" ]
+ }
+
+ def feature_availability(self):
+ return {name: self.is_available(name) for name in FeatureDetector.features}
+
+ def is_available(self, feature):
+ return self.has_requirements(self.get_requirements(feature))
+
+ def get_requirements(self, feature):
+ try:
+ return FeatureDetector.features[feature]
+ except KeyError:
+ raise UnknownFeatureException("Feature \"{0}\" is not known.".format(feature))
+
+ def has_requirements(self, requirements):
+ passed = True
+ for requirement in requirements:
+ methodname = "has_" + requirement
+ if hasattr(self, methodname) and callable(getattr(self, methodname)):
+ passed = passed and getattr(self, methodname)()
+ else:
+ logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement))
+ return passed
+
+ def command_is_runnable(self, command):
+ return os.system("{0} 2>/dev/null >/dev/null".format(command)) != 32512
+
+ def has_csdr(self):
+ return self.command_is_runnable("csdr")
+
+ def has_nmux(self):
+ return self.command_is_runnable("nmux --help")
+
+ def has_nc(self):
+ return self.command_is_runnable('nc --help')
+
+ def has_rtl_sdr(self):
+ return self.command_is_runnable("rtl_sdr --help")
+
+ def has_rx_tools(self):
+ return self.command_is_runnable("rx_sdr --help")
+
+ """
+ To use a HackRF, compile the HackRF host tools from its "stdout" branch:
+ git clone https://github.com/mossmann/hackrf/
+ cd hackrf
+ git fetch
+ git checkout origin/stdout
+ cd host
+ mkdir build
+ cd build
+ cmake .. -DINSTALL_UDEV_RULES=ON
+ make
+ sudo make install
+ """
+ def has_hackrf_transfer(self):
+ # TODO i don't have a hackrf, so somebody doublecheck this.
+ # TODO also check if it has the stdout feature
+ return self.command_is_runnable("hackrf_transfer --help")
+
+ def command_exists(self, command):
+ return os.system("which {0}".format(command)) == 0
+
+ """
+ To use DMR and YSF, the digiham package is required. You can find the package and installation instructions here:
+ https://github.com/jketterl/digiham
+
+ Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work.
+ If you have an older verison of digiham installed, please update it along with openwebrx.
+ As of now, we require version 0.2 of digiham.
+ """
+ def has_digiham(self):
+ required_version = LooseVersion("0.2")
+
+ digiham_version_regex = re.compile('^digiham version (.*)$')
+ def check_digiham_version(command):
+ try:
+ process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE)
+ version = LooseVersion(digiham_version_regex.match(process.stdout.readline().decode()).group(1))
+ process.wait(1)
+ return version >= required_version
+ except FileNotFoundError:
+ return False
+ return reduce(and_,
+ map(
+ check_digiham_version,
+ ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator",
+ "digitalvoice_filter"]
+ ),
+ True)
+
+ def has_dsd(self):
+ return self.command_is_runnable("dsd")
+
+ def has_sox(self):
+ return self.command_is_runnable("sox")
+
+ def has_airspy_rx(self):
+ return self.command_is_runnable("airspy_rx --help 2> /dev/null")
diff --git a/owrx/http.py b/owrx/http.py
new file mode 100644
index 0000000..ca7d357
--- /dev/null
+++ b/owrx/http.py
@@ -0,0 +1,42 @@
+from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController
+from http.server import BaseHTTPRequestHandler
+import re
+
+import logging
+logger = logging.getLogger(__name__)
+
+class RequestHandler(BaseHTTPRequestHandler):
+ def __init__(self, request, client_address, server):
+ self.router = Router()
+ super().__init__(request, client_address, server)
+ def do_GET(self):
+ self.router.route(self)
+
+class Router(object):
+ mappings = [
+ {"route": "/", "controller": IndexController},
+ {"route": "/status", "controller": StatusController},
+ {"regex": "/static/(.+)", "controller": AssetsController},
+ {"route": "/ws/", "controller": WebSocketController},
+ {"regex": "(/favicon.ico)", "controller": AssetsController},
+ # backwards compatibility for the sdr.hu portal
+ {"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController}
+ ]
+ def find_controller(self, path):
+ for m in Router.mappings:
+ if "route" in m:
+ if m["route"] == path:
+ return (m["controller"], None)
+ if "regex" in m:
+ regex = re.compile(m["regex"])
+ matches = regex.match(path)
+ if matches:
+ return (m["controller"], matches)
+ def route(self, handler):
+ res = self.find_controller(handler.path)
+ if res is not None:
+ (controller, matches) = res
+ logger.debug("path: {0}, controller: {1}, matches: {2}".format(handler.path, controller, matches))
+ controller(handler, matches).handle_request()
+ else:
+ handler.send_error(404, "Not Found", "The page you requested could not be found.")
diff --git a/owrx/meta.py b/owrx/meta.py
new file mode 100644
index 0000000..ec4966a
--- /dev/null
+++ b/owrx/meta.py
@@ -0,0 +1,81 @@
+from owrx.config import PropertyManager
+from urllib import request
+import json
+from datetime import datetime, timedelta
+import logging
+import threading
+
+logger = logging.getLogger(__name__)
+
+class DmrCache(object):
+ sharedInstance = None
+ @staticmethod
+ def getSharedInstance():
+ if DmrCache.sharedInstance is None:
+ DmrCache.sharedInstance = DmrCache()
+ return DmrCache.sharedInstance
+ def __init__(self):
+ self.cache = {}
+ self.cacheTimeout = timedelta(seconds = 86400)
+ def isValid(self, key):
+ if not key in self.cache: return False
+ entry = self.cache[key]
+ return entry["timestamp"] + self.cacheTimeout > datetime.now()
+ def put(self, key, value):
+ self.cache[key] = {
+ "timestamp": datetime.now(),
+ "data": value
+ }
+ def get(self, key):
+ if not self.isValid(key): return None
+ return self.cache[key]["data"]
+
+
+class DmrMetaEnricher(object):
+ def __init__(self):
+ self.threads = {}
+ def downloadRadioIdData(self, id):
+ cache = DmrCache.getSharedInstance()
+ try:
+ logger.debug("requesting DMR metadata for id=%s", id)
+ res = request.urlopen("https://www.radioid.net/api/dmr/user/?id={0}".format(id), timeout=30).read()
+ data = json.loads(res.decode("utf-8"))
+ cache.put(id, data)
+ except json.JSONDecodeError:
+ cache.put(id, None)
+ del self.threads[id]
+ def enrich(self, meta):
+ if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: return None
+ if not "source" in meta: return None
+ id = meta["source"]
+ cache = DmrCache.getSharedInstance()
+ if not cache.isValid(id):
+ if not id in self.threads:
+ self.threads[id] = threading.Thread(target=self.downloadRadioIdData, args=[id])
+ self.threads[id].start()
+ return None
+ data = cache.get(id)
+ if "count" in data and data["count"] > 0 and "results" in data:
+ return data["results"][0]
+ return None
+
+
+class MetaParser(object):
+ enrichers = {
+ "DMR": DmrMetaEnricher()
+ }
+
+ def __init__(self, handler):
+ self.handler = handler
+
+ def parse(self, meta):
+ fields = meta.split(";")
+ meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""}
+
+ if "protocol" in meta:
+ protocol = meta["protocol"]
+ if protocol in MetaParser.enrichers:
+ additional_data = MetaParser.enrichers[protocol].enrich(meta)
+ if additional_data is not None: meta["additional"] = additional_data
+ self.handler.write_metadata(meta)
+
diff --git a/owrx/sdrhu.py b/owrx/sdrhu.py
new file mode 100644
index 0000000..5f0d7fb
--- /dev/null
+++ b/owrx/sdrhu.py
@@ -0,0 +1,36 @@
+import threading
+import subprocess
+import time
+from owrx.config import PropertyManager
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class SdrHuUpdater(threading.Thread):
+ def __init__(self):
+ self.doRun = True
+ super().__init__(daemon = True)
+
+ def update(self):
+ pm = PropertyManager.getSharedInstance()
+ cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}\" 2>&1".format(**pm.__dict__())
+ logger.debug(cmd)
+ returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()
+ returned=returned[0].decode('utf-8')
+ if "UPDATE:" in returned:
+ retrytime_mins = 20
+ value=returned.split("UPDATE:")[1].split("\n",1)[0]
+ if value.startswith("SUCCESS"):
+ logger.info("Update succeeded!")
+ else:
+ logger.warning("Update failed, your receiver cannot be listed on sdr.hu! Reason: %s", value)
+ else:
+ retrytime_mins = 2
+ logger.warning("wget failed while updating, your receiver cannot be listed on sdr.hu!")
+ return retrytime_mins
+
+ def run(self):
+ while self.doRun:
+ retrytime_mins = self.update()
+ time.sleep(60*retrytime_mins)
diff --git a/owrx/source.py b/owrx/source.py
new file mode 100644
index 0000000..7d62388
--- /dev/null
+++ b/owrx/source.py
@@ -0,0 +1,552 @@
+import subprocess
+from owrx.config import PropertyManager
+from owrx.feature import FeatureDetector, UnknownFeatureException
+from owrx.meta import MetaParser
+import threading
+import csdr
+import time
+import os
+import signal
+import sys
+import socket
+import logging
+
+logger = logging.getLogger(__name__)
+
+class SdrService(object):
+ sdrProps = None
+ sources = {}
+ lastPort = None
+ @staticmethod
+ def getNextPort():
+ pm = PropertyManager.getSharedInstance()
+ (start, end) = pm["iq_port_range"]
+ if SdrService.lastPort is None:
+ SdrService.lastPort = start
+ else:
+ SdrService.lastPort += 1
+ if SdrService.lastPort > end:
+ raise IndexError("no more available ports to start more sdrs")
+ return SdrService.lastPort
+ @staticmethod
+ def loadProps():
+ if SdrService.sdrProps is None:
+ pm = PropertyManager.getSharedInstance()
+ featureDetector = FeatureDetector()
+ def loadIntoPropertyManager(dict: dict):
+ propertyManager = PropertyManager()
+ for (name, value) in dict.items():
+ propertyManager[name] = value
+ return propertyManager
+ def sdrTypeAvailable(value):
+ try:
+ if not featureDetector.is_available(value["type"]):
+ logger.error("The RTL source type \"{0}\" is not available. please check requirements.".format(value["type"]))
+ return False
+ return True
+ except UnknownFeatureException:
+ logger.error("The RTL source type \"{0}\" is invalid. Please check your configuration".format(value["type"]))
+ return False
+ # transform all dictionary items into PropertyManager object, filtering out unavailable ones
+ SdrService.sdrProps = {
+ name: loadIntoPropertyManager(value) for (name, value) in pm["sdrs"].items() if sdrTypeAvailable(value)
+ }
+ logger.info("SDR sources loaded. Availables SDRs: {0}".format(", ".join(map(lambda x: x["name"], SdrService.sdrProps.values()))))
+ @staticmethod
+ def getSource(id = None):
+ SdrService.loadProps()
+ if id is None:
+ # TODO: configure default sdr in config? right now it will pick the first one off the list.
+ id = list(SdrService.sdrProps.keys())[0]
+ sources = SdrService.getSources()
+ return sources[id]
+ @staticmethod
+ def getSources():
+ SdrService.loadProps()
+ for id in SdrService.sdrProps.keys():
+ if not id in SdrService.sources:
+ props = SdrService.sdrProps[id]
+ className = ''.join(x for x in props["type"].title() if x.isalnum()) + "Source"
+ cls = getattr(sys.modules[__name__], className)
+ SdrService.sources[id] = cls(props, SdrService.getNextPort())
+ return SdrService.sources
+
+
+class SdrSource(object):
+ def __init__(self, props, port):
+ self.props = props
+ self.activateProfile()
+ self.rtlProps = self.props.collect(
+ "samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain"
+ ).defaults(PropertyManager.getSharedInstance())
+
+ def restart(name, value):
+ logger.debug("restarting sdr source due to property change: {0} changed to {1}".format(name, value))
+ self.stop()
+ self.start()
+ self.rtlProps.wire(restart)
+ self.port = port
+ self.monitor = None
+ self.clients = []
+ self.spectrumClients = []
+ self.spectrumThread = None
+ self.process = None
+ self.modificationLock = threading.Lock()
+
+ # override this in subclasses
+ def getCommand(self):
+ pass
+
+ # override this in subclasses, if necessary
+ def getFormatConversion(self):
+ return None
+
+ def activateProfile(self, id = None):
+ profiles = self.props["profiles"]
+ if id is None:
+ id = list(profiles.keys())[0]
+ logger.debug("activating profile {0}".format(id))
+ profile = profiles[id]
+ for (key, value) in profile.items():
+ # skip the name, that would overwrite the source name.
+ if key == "name": continue
+ self.props[key] = value
+
+ def getProfiles(self):
+ return self.props["profiles"]
+
+ def getName(self):
+ return self.props["name"]
+
+ def getProps(self):
+ return self.props
+
+ def getPort(self):
+ return self.port
+
+ def start(self):
+ self.modificationLock.acquire()
+ if self.monitor:
+ self.modificationLock.release()
+ return
+
+ props = self.rtlProps
+
+ start_sdr_command = self.getCommand().format(
+ **props.collect("samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain").__dict__()
+ )
+
+ format_conversion = self.getFormatConversion()
+ if format_conversion is not None:
+ start_sdr_command += " | " + format_conversion
+
+ nmux_bufcnt = nmux_bufsize = 0
+ while nmux_bufsize < props["samp_rate"]/4: nmux_bufsize += 4096
+ while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6: nmux_bufcnt += 1
+ if nmux_bufcnt == 0 or nmux_bufsize == 0:
+ logger.error("Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py")
+ self.modificationLock.release()
+ return
+ logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt))
+ cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, self.port)
+ self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp)
+ logger.info("Started rtl source: " + cmd)
+
+ def wait_for_process_to_end():
+ rc = self.process.wait()
+ logger.debug("shut down with RC={0}".format(rc))
+ self.monitor = None
+
+ self.monitor = threading.Thread(target = wait_for_process_to_end)
+ self.monitor.start()
+
+ while True:
+ testsock = socket.socket()
+ try:
+ testsock.connect(("127.0.0.1", self.getPort()))
+ testsock.close()
+ break
+ except:
+ time.sleep(0.1)
+
+ self.modificationLock.release()
+
+ for c in self.clients:
+ c.onSdrAvailable()
+
+ def isAvailable(self):
+ return self.monitor is not None
+
+ def stop(self):
+ for c in self.clients:
+ c.onSdrUnavailable()
+
+ self.modificationLock.acquire()
+
+ if self.process is not None:
+ try:
+ os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
+ except ProcessLookupError:
+ # been killed by something else, ignore
+ pass
+ if self.monitor:
+ self.monitor.join()
+ self.sleepOnRestart()
+ self.modificationLock.release()
+
+ def sleepOnRestart(self):
+ pass
+
+ def addClient(self, c):
+ self.clients.append(c)
+ self.start()
+ def removeClient(self, c):
+ try:
+ self.clients.remove(c)
+ except ValueError:
+ pass
+ if not self.clients:
+ self.stop()
+
+ def addSpectrumClient(self, c):
+ self.spectrumClients.append(c)
+ if self.spectrumThread is None:
+ self.spectrumThread = SpectrumThread(self)
+ self.spectrumThread.start()
+
+ def removeSpectrumClient(self, c):
+ try:
+ self.spectrumClients.remove(c)
+ except ValueError:
+ pass
+ if not self.spectrumClients and self.spectrumThread is not None:
+ self.spectrumThread.stop()
+ self.spectrumThread = None
+
+ def writeSpectrumData(self, data):
+ for c in self.spectrumClients:
+ c.write_spectrum_data(data)
+
+
+class RtlSdrSource(SdrSource):
+ def getCommand(self):
+ return "rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -"
+
+ def getFormatConversion(self):
+ return "csdr convert_u8_f"
+
+class HackrfSource(SdrSource):
+ def getCommand(self):
+ return "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-"
+
+ def getFormatConversion(self):
+ return "csdr convert_s8_f"
+
+class SdrplaySource(SdrSource):
+ def getCommand(self):
+ command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}"
+ gainMap = { "rf_gain" : "RFGR", "if_gain" : "IFGR"}
+ gains = [ "{0}={{{1}}}".format(gainMap[name], name) for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items() if value is not None ]
+ if gains:
+ command += " -g {gains}".format(gains = ",".join(gains))
+ if self.rtlProps["antenna"] is not None:
+ command += " -a \"{antenna}\""
+ command += " -"
+ return command
+
+ def sleepOnRestart(self):
+ time.sleep(1)
+
+class AirspySource(SdrSource):
+ def getCommand(self):
+ frequency = self.props['center_freq'] / 1e6
+ command = "airspy_rx"
+ command += " -f{0}".format(frequency)
+ command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}"
+ return command
+ def getFormatConversion(self):
+ return "csdr convert_s16_f"
+
+class SpectrumThread(csdr.output):
+ def __init__(self, sdrSource):
+ self.sdrSource = sdrSource
+ super().__init__()
+
+ self.props = props = self.sdrSource.props.collect(
+ "samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression",
+ "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through"
+ ).defaults(PropertyManager.getSharedInstance())
+
+ self.dsp = dsp = csdr.dsp(self)
+ dsp.nc_port = self.sdrSource.getPort()
+ dsp.set_demodulator("fft")
+
+ def set_fft_averages(key, value):
+ samp_rate = props["samp_rate"]
+ fft_size = props["fft_size"]
+ fft_fps = props["fft_fps"]
+ fft_voverlap_factor = props["fft_voverlap_factor"]
+
+ dsp.set_fft_averages(int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor))) if fft_voverlap_factor>0 else 0)
+
+ self.subscriptions = [
+ props.getProperty("samp_rate").wire(dsp.set_samp_rate),
+ props.getProperty("fft_size").wire(dsp.set_fft_size),
+ props.getProperty("fft_fps").wire(dsp.set_fft_fps),
+ props.getProperty("fft_compression").wire(dsp.set_fft_compression),
+ props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages)
+ ]
+
+ set_fft_averages(None, None)
+
+ dsp.csdr_dynamic_bufsize = props["csdr_dynamic_bufsize"]
+ dsp.csdr_print_bufsizes = props["csdr_print_bufsizes"]
+ dsp.csdr_through = props["csdr_through"]
+ logger.debug("Spectrum thread initialized successfully.")
+
+ def start(self):
+ self.sdrSource.addClient(self)
+ if self.sdrSource.isAvailable():
+ self.dsp.start()
+
+ def add_output(self, type, read_fn):
+ if type != "audio":
+ logger.error("unsupported output type received by FFT: %s", type)
+ return
+
+ if self.props["csdr_dynamic_bufsize"]:
+ read_fn(8) #dummy read to skip bufsize & preamble
+ logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
+
+ def pipe():
+ run = True
+ while run:
+ data = read_fn()
+ if len(data) == 0:
+ run = False
+ else:
+ self.sdrSource.writeSpectrumData(data)
+
+ threading.Thread(target = pipe).start()
+
+ def stop(self):
+ self.dsp.stop()
+ self.sdrSource.removeClient(self)
+ for c in self.subscriptions:
+ c.cancel()
+ self.subscriptions = []
+
+ def onSdrAvailable(self):
+ self.dsp.start()
+ def onSdrUnavailable(self):
+ self.dsp.stop()
+
+class DspManager(csdr.output):
+ def __init__(self, handler, sdrSource):
+ self.handler = handler
+ self.sdrSource = sdrSource
+ self.metaParser = MetaParser(self.handler)
+
+ self.localProps = self.sdrSource.getProps().collect(
+ "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize",
+ "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality",
+ "dmr_filter"
+ ).defaults(PropertyManager.getSharedInstance())
+
+ self.dsp = csdr.dsp(self)
+ self.dsp.nc_port = self.sdrSource.getPort()
+
+ def set_low_cut(cut):
+ bpf = self.dsp.get_bpf()
+ bpf[0] = cut
+ self.dsp.set_bpf(*bpf)
+
+ def set_high_cut(cut):
+ bpf = self.dsp.get_bpf()
+ bpf[1] = cut
+ self.dsp.set_bpf(*bpf)
+
+ self.subscriptions = [
+ self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression),
+ self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression),
+ self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size),
+ self.localProps.getProperty("samp_rate").wire(self.dsp.set_samp_rate),
+ self.localProps.getProperty("output_rate").wire(self.dsp.set_output_rate),
+ self.localProps.getProperty("offset_freq").wire(self.dsp.set_offset_freq),
+ self.localProps.getProperty("squelch_level").wire(self.dsp.set_squelch_level),
+ self.localProps.getProperty("low_cut").wire(set_low_cut),
+ self.localProps.getProperty("high_cut").wire(set_high_cut),
+ self.localProps.getProperty("mod").wire(self.dsp.set_demodulator),
+ self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality),
+ self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter)
+ ]
+
+ self.dsp.set_offset_freq(0)
+ self.dsp.set_bpf(-4000,4000)
+ self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"]
+ self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"]
+ self.dsp.csdr_through = self.localProps["csdr_through"]
+
+ if (self.localProps["digimodes_enable"]):
+ def set_secondary_mod(mod):
+ if mod == False: mod = None
+ self.dsp.set_secondary_demodulator(mod)
+ if mod is not None:
+ self.handler.write_secondary_dsp_config({
+ "secondary_fft_size":self.localProps["digimodes_fft_size"],
+ "if_samp_rate":self.dsp.if_samp_rate(),
+ "secondary_bw":self.dsp.secondary_bw()
+ })
+ self.subscriptions += [
+ self.localProps.getProperty("secondary_mod").wire(set_secondary_mod),
+ self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq)
+ ]
+
+ self.sdrSource.addClient(self)
+
+ super().__init__()
+
+ def start(self):
+ if self.sdrSource.isAvailable():
+ self.dsp.start()
+
+ def add_output(self, t, read_fn):
+ logger.debug("adding new output of type %s", t)
+ writers = {
+ "audio": self.handler.write_dsp_data,
+ "smeter": self.handler.write_s_meter_level,
+ "secondary_fft": self.handler.write_secondary_fft,
+ "secondary_demod": self.handler.write_secondary_demod,
+ "meta": self.metaParser.parse
+ }
+ write = writers[t]
+
+ def pump(read, write):
+ def copy():
+ run = True
+ while run:
+ data = read()
+ if data is None or (isinstance(data, bytes) and len(data) == 0):
+ logger.warning("zero read on {0}".format(t))
+ run = False
+ else:
+ write(data)
+ return copy
+
+ threading.Thread(target=pump(read_fn, write)).start()
+
+ def stop(self):
+ self.dsp.stop()
+ self.sdrSource.removeClient(self)
+ for sub in self.subscriptions:
+ sub.cancel()
+ self.subscriptions = []
+
+ def setProperty(self, prop, value):
+ self.localProps.getProperty(prop).setValue(value)
+
+ def onSdrAvailable(self):
+ logger.debug("received onSdrAvailable, attempting DspSource restart")
+ self.dsp.start()
+
+ def onSdrUnavailable(self):
+ logger.debug("received onSdrUnavailable, shutting down DspSource")
+ self.dsp.stop()
+
+class CpuUsageThread(threading.Thread):
+ sharedInstance = None
+ @staticmethod
+ def getSharedInstance():
+ if CpuUsageThread.sharedInstance is None:
+ CpuUsageThread.sharedInstance = CpuUsageThread()
+ CpuUsageThread.sharedInstance.start()
+ return CpuUsageThread.sharedInstance
+
+ def __init__(self):
+ self.clients = []
+ self.doRun = True
+ self.last_worktime = 0
+ self.last_idletime = 0
+ super().__init__()
+
+ def run(self):
+ while self.doRun:
+ try:
+ cpu_usage = self.get_cpu_usage()
+ except:
+ cpu_usage = 0
+ for c in self.clients:
+ c.write_cpu_usage(cpu_usage)
+ time.sleep(3)
+ logger.debug("cpu usage thread shut down")
+
+ def get_cpu_usage(self):
+ try:
+ f = open("/proc/stat","r")
+ except:
+ return 0 #Workaround, possibly we're on a Mac
+ line = ""
+ while not "cpu " in line: line=f.readline()
+ f.close()
+ spl = line.split(" ")
+ worktime = int(spl[2]) + int(spl[3]) + int(spl[4])
+ idletime = int(spl[5])
+ dworktime = (worktime - self.last_worktime)
+ didletime = (idletime - self.last_idletime)
+ rate = float(dworktime) / (didletime+dworktime)
+ self.last_worktime = worktime
+ self.last_idletime = idletime
+ if (self.last_worktime==0): return 0
+ return rate
+
+ def add_client(self, c):
+ self.clients.append(c)
+
+ def remove_client(self, c):
+ try:
+ self.clients.remove(c)
+ except ValueError:
+ pass
+ if not self.clients:
+ self.shutdown()
+
+ def shutdown(self):
+ CpuUsageThread.sharedInstance = None
+ self.doRun = False
+
+class TooManyClientsException(Exception):
+ pass
+
+class ClientRegistry(object):
+ sharedInstance = None
+ @staticmethod
+ def getSharedInstance():
+ if ClientRegistry.sharedInstance is None:
+ ClientRegistry.sharedInstance = ClientRegistry()
+ return ClientRegistry.sharedInstance
+
+ def __init__(self):
+ self.clients = []
+ super().__init__()
+
+ def broadcast(self):
+ n = self.clientCount()
+ for c in self.clients:
+ c.write_clients(n)
+
+ def addClient(self, client):
+ pm = PropertyManager.getSharedInstance()
+ if len(self.clients) >= pm["max_clients"]:
+ raise TooManyClientsException()
+ self.clients.append(client)
+ self.broadcast()
+
+ def clientCount(self):
+ return len(self.clients)
+
+ def removeClient(self, client):
+ try:
+ self.clients.remove(client)
+ except ValueError:
+ pass
+ self.broadcast()
\ No newline at end of file
diff --git a/owrx/version.py b/owrx/version.py
new file mode 100644
index 0000000..7437eda
--- /dev/null
+++ b/owrx/version.py
@@ -0,0 +1 @@
+openwebrx_version = "v0.18"
\ No newline at end of file
diff --git a/owrx/websocket.py b/owrx/websocket.py
new file mode 100644
index 0000000..a247b2a
--- /dev/null
+++ b/owrx/websocket.py
@@ -0,0 +1,97 @@
+import base64
+import hashlib
+import json
+
+import logging
+logger = logging.getLogger(__name__)
+
+class WebSocketConnection(object):
+ def __init__(self, handler, messageHandler):
+ self.handler = handler
+ self.messageHandler = messageHandler
+ my_headers = self.handler.headers.items()
+ my_header_keys = list(map(lambda x:x[0],my_headers))
+ h_key_exists = lambda x:my_header_keys.count(x)
+ h_value = lambda x:my_headers[my_header_keys.index(x)][1]
+ if (not h_key_exists("Upgrade")) or not (h_value("Upgrade")=="websocket") or (not h_key_exists("Sec-WebSocket-Key")):
+ raise WebSocketException
+ ws_key = h_value("Sec-WebSocket-Key")
+ shakey = hashlib.sha1()
+ shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key = ws_key).encode())
+ ws_key_toreturn = base64.b64encode(shakey.digest())
+ self.handler.wfile.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format(ws_key_toreturn.decode()).encode())
+
+ def get_header(self, size, opcode):
+ ws_first_byte = 0b10000000 | (opcode & 0x0F)
+ if (size > 125):
+ return bytes([ws_first_byte, 126, (size>>8) & 0xff, size & 0xff])
+ else:
+ # 256 bytes binary message in a single unmasked frame
+ return bytes([ws_first_byte, size])
+
+ def send(self, data):
+ # convenience
+ if (type(data) == dict):
+ # allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway.
+ data = json.dumps(data, allow_nan = False)
+
+ # string-type messages are sent as text frames
+ if (type(data) == str):
+ header = self.get_header(len(data), 1)
+ data_to_send = header + data.encode('utf-8')
+ # anything else as binary
+ else:
+ header = self.get_header(len(data), 2)
+ data_to_send = header + data
+ written = self.handler.wfile.write(data_to_send)
+ if (written != len(data_to_send)):
+ logger.error("incomplete write! closing socket!")
+ self.close()
+ else:
+ self.handler.wfile.flush()
+
+ def read_loop(self):
+ open = True
+ while (open):
+ header = self.handler.rfile.read(2)
+ opcode = header[0] & 0x0F
+ length = header[1] & 0x7F
+ mask = (header[1] & 0x80) >> 7
+ if (length == 126):
+ header = self.handler.rfile.read(2)
+ length = (header[0] << 8) + header[1]
+ if (mask):
+ masking_key = self.handler.rfile.read(4)
+ data = self.handler.rfile.read(length)
+ if (mask):
+ data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)])
+ if (opcode == 1):
+ message = data.decode('utf-8')
+ self.messageHandler.handleTextMessage(self, message)
+ elif (opcode == 2):
+ self.messageHandler.handleBinaryMessage(self, data)
+ elif (opcode == 8):
+ open = False
+ self.messageHandler.handleClose(self)
+ else:
+ logger.warning("unsupported opcode: {0}".format(opcode))
+
+ def close(self):
+ try:
+ header = self.get_header(0, 8)
+ self.handler.wfile.write(header)
+ self.handler.wfile.flush()
+ except ValueError:
+ logger.exception("ValueError while writing close frame:")
+ except OSError:
+ logger.exception("OSError while writing close frame:")
+
+ try:
+ self.handler.finish()
+ self.handler.connection.close()
+ except Exception:
+ logger.exception("while closing connection:")
+
+
+class WebSocketException(Exception):
+ pass
diff --git a/rxws.py b/rxws.py
deleted file mode 100644
index a1f210c..0000000
--- a/rxws.py
+++ /dev/null
@@ -1,171 +0,0 @@
-"""
-rxws: WebSocket methods implemented for OpenWebRX
-
- This file is part of OpenWebRX,
- an open-source SDR receiver software with a web UI.
- Copyright (c) 2013-2015 by Andras Retzler
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-
-"""
-
-import base64
-import sha
-import select
-import code
-
-class WebSocketException(Exception):
- pass
-
-def handshake(myself):
- my_client_id=myself.path[4:]
- my_headers=myself.headers.items()
- my_header_keys=map(lambda x:x[0],my_headers)
- h_key_exists=lambda x:my_header_keys.count(x)
- h_value=lambda x:my_headers[my_header_keys.index(x)][1]
- #print "The Lambdas(tm)"
- #print h_key_exists("upgrade")
- #print h_value("upgrade")
- #print h_key_exists("sec-websocket-key")
- if (not h_key_exists("upgrade")) or not (h_value("upgrade")=="websocket") or (not h_key_exists("sec-websocket-key")):
- raise WebSocketException
- ws_key=h_value("sec-websocket-key")
- ws_key_toreturn=base64.b64encode(sha.new(ws_key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest())
- #A sample list of keys we get: [('origin', 'http://localhost:8073'), ('upgrade', 'websocket'), ('sec-websocket-extensions', 'x-webkit-deflate-frame'), ('sec-websocket-version', '13'), ('host', 'localhost:8073'), ('sec-websocket-key', 't9J1rgy4fc9fg2Hshhnkmg=='), ('connection', 'Upgrade'), ('pragma', 'no-cache'), ('cache-control', 'no-cache')]
- myself.wfile.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "+ws_key_toreturn+"\r\nCQ-CQ-de: HA5KFU\r\n\r\n")
-
-def get_header(size):
- #this does something similar: https://github.com/lemmingzshadow/php-websocket/blob/master/server/lib/WebSocket/Connection.php
- ws_first_byte=0b10000010 # FIN=1, OP=2
- if(size>125):
- ws_second_byte=126 # The following two bytes will indicate frame size
- extended_size=chr((size>>8)&0xff)+chr(size&0xff) #Okay, it uses reverse byte order (little-endian) compared to anything else sent on TCP
- else:
- ws_second_byte=size
- #256 bytes binary message in a single unmasked frame | 0x82 0x7E 0x0100 [256 bytes of binary data]
- extended_size=""
- return chr(ws_first_byte)+chr(ws_second_byte)+extended_size
-
-def code_payload(data, masking_key=""):
- # both encode or decode
- if masking_key=="":
- key = (61, 84, 35, 6)
- else:
- key = [ord(i) for i in masking_key]
- encoded=""
- for i in range(0,len(data)):
- encoded+=chr(ord(data[i])^key[i%4])
- return encoded
-
-def xxdg(data):
- output=""
- for i in range(0,len(data)/8):
- output+=xxd(data[i:i+8])
- if i%2: output+="\n"
- else: output+=" "
- return output
-
-
-def xxd(data):
- #diagnostic purposes only
- output=""
- for d in data:
- output+=hex(ord(d))[2:].zfill(2)+" "
- return output
-
-#for R/W the WebSocket, use recv/send
-#for reading the TCP socket, use readsock
-#for writing the TCP socket, use myself.wfile.write and flush
-
-def readsock(myself,size,blocking):
- #http://thenestofheliopolis.blogspot.hu/2011/01/how-to-implement-non-blocking-two-way.html
- if blocking:
- return myself.rfile.read(size)
- else:
- poll = select.poll()
- poll.register(myself.rfile.fileno(), select.POLLIN or select.POLLPRI)
- fd = poll.poll(0) #timeout is 0
- if len(fd):
- f = fd[0]
- if f[1] > 0:
- return myself.rfile.read(size)
- return ""
-
-
-def recv(myself, blocking=False, debug=False):
- bufsize=70000
- #myself.connection.setblocking(blocking) #umm... we cannot do that with rfile
- if debug: print "ws_recv begin"
- try:
- data=readsock(myself,6,blocking)
- #print "rxws.recv bytes:",xxd(data)
- except:
- if debug: print "ws_recv error"
- return ""
- if debug: print "ws_recv recved"
- if(len(data)==0): return ""
- fin=ord(data[0])&128!=0
- is_text_frame=ord(data[0])&15==1
- length=ord(data[1])&0x7f
- data+=readsock(myself,length,blocking)
- #print "rxws.recv length is ",length," (multiple packets together?) len(data) =",len(data)
- has_one_byte_length=length<125
- masked=ord(data[1])&0x80!=0
- #print "len=", length, len(data)-2
- #print "fin, is_text_frame, has_one_byte_length, masked = ", (fin, is_text_frame, has_one_byte_length, masked)
- #print xxd(data)
- if fin and is_text_frame and has_one_byte_length:
- if masked:
- return code_payload(data[6:], data[2:6])
- else:
- return data[2:]
-
-#Useful links for ideas on WebSockets:
-# http://stackoverflow.com/questions/8125507/how-can-i-send-and-receive-websocket-messages-on-the-server-side
-# https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_server
-# http://tools.ietf.org/html/rfc6455#section-5.2
-
-
-def flush(myself):
- myself.wfile.flush()
- #or the socket, not the rfile:
- #lR,lW,lX = select.select([],[myself.connection,],[],60)
-
-
-def send(myself, data, begin_id="", debug=0):
- base_frame_size=35000 #could guess by MTU?
- debug=0
- #try:
- while True:
- counter=0
- from_end=len(data)-counter
- if from_end+len(begin_id)>base_frame_size:
- data_to_send=begin_id+data[counter:counter+base_frame_size-len(begin_id)]
- header=get_header(len(data_to_send))
- flush(myself)
- myself.wfile.write(header+data_to_send)
- flush(myself)
- if debug: print "rxws.send ==================== #1 if branch :: from={0} to={1} dlen={2} hlen={3}".format(counter,counter+base_frame_size-len(begin_id),len(data_to_send),len(header))
- else:
- data_to_send=begin_id+data[counter:]
- header=get_header(len(data_to_send))
- flush(myself)
- myself.wfile.write(header+data_to_send)
- flush(myself)
- if debug: print "rxws.send :: #2 else branch :: dlen={0} hlen={1}".format(len(data_to_send),len(header))
- #if debug: print "header:\n"+xxdg(header)+"\n\nws data:\n"+xxdg(data_to_send)
- break
- counter+=base_frame_size-len(begin_id)
- #except:
- # pass
diff --git a/sdrhu.py b/sdrhu.py
index d06ae05..3060789 100755
--- a/sdrhu.py
+++ b/sdrhu.py
@@ -20,31 +20,13 @@
"""
-import config_webrx as cfg, time, subprocess
-
-def run(continuously=True):
- if not cfg.sdrhu_key: return
- firsttime="(Your receiver is soon getting listed on sdr.hu!)"
- while True:
- cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://"+cfg.server_hostname+":"+str(cfg.web_port)+"&apikey="+cfg.sdrhu_key+"\" 2>&1"
- print "[openwebrx-sdrhu]", cmd
- returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()
- returned=returned[0]
- #print returned
- if "UPDATE:" in returned:
- retrytime_mins = 20
- value=returned.split("UPDATE:")[1].split("\n",1)[0]
- if value.startswith("SUCCESS"):
- print "[openwebrx-sdrhu] Update succeeded! "+firsttime
- firsttime=""
- else:
- print "[openwebrx-sdrhu] Update failed, your receiver cannot be listed on sdr.hu! Reason:", value
- else:
- retrytime_mins = 2
- print "[openwebrx-sdrhu] wget failed while updating, your receiver cannot be listed on sdr.hu!"
- if not continuously: break
- time.sleep(60*retrytime_mins)
+from owrx.sdrhu import SdrHuUpdater
+from owrx.config import PropertyManager
if __name__=="__main__":
- run(False)
+ pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
+
+ if not "sdrhu_key" in pm:
+ exit(1)
+ SdrHuUpdater().update()