Merge branch 'develop' into docker
This commit is contained in:
commit
72bf698d95
170
config_webrx.py
170
config_webrx.py
@ -36,7 +36,6 @@ config_webrx: configuration options for OpenWebRX
|
|||||||
|
|
||||||
# ==== Server settings ====
|
# ==== Server settings ====
|
||||||
web_port=8073
|
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
|
max_clients=20
|
||||||
|
|
||||||
# ==== Web GUI configuration ====
|
# ==== Web GUI configuration ====
|
||||||
@ -65,25 +64,24 @@ Website: <a href="http://localhost" target="_blank">http://localhost</a>
|
|||||||
sdrhu_key = ""
|
sdrhu_key = ""
|
||||||
# 3. Set this setting to True to enable listing:
|
# 3. Set this setting to True to enable listing:
|
||||||
sdrhu_public_listing = False
|
sdrhu_public_listing = False
|
||||||
|
server_hostname="localhost"
|
||||||
|
|
||||||
# ==== DSP/RX settings ====
|
# ==== DSP/RX settings ====
|
||||||
fft_fps=9
|
fft_fps=9
|
||||||
fft_size=4096 #Should be power of 2
|
fft_size=4096 #Should be power of 2
|
||||||
fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
|
fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
|
||||||
|
|
||||||
# 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"
|
audio_compression="adpcm" #valid values: "adpcm", "none"
|
||||||
fft_compression="adpcm" #valid values: "adpcm", "none"
|
fft_compression="adpcm" #valid values: "adpcm", "none"
|
||||||
|
|
||||||
digimodes_enable=True #Decoding digimodes come with higher CPU usage.
|
digimodes_enable=True #Decoding digimodes come with higher CPU usage.
|
||||||
digimodes_fft_size=1024
|
digimodes_fft_size=1024
|
||||||
|
|
||||||
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:
|
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 #
|
# 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
|
sdrs = {
|
||||||
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)
|
"rtlsdr": {
|
||||||
format_conversion="csdr convert_u8_f"
|
"name": "RTL-SDR USB Stick",
|
||||||
|
"type": "rtl_sdr",
|
||||||
#lna_gain=8
|
"ppm": 0,
|
||||||
#rf_amp=1
|
# you can change this if you use an upconverter. formula is:
|
||||||
#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)
|
# shown_center_freq = center_freq + lfo_offset
|
||||||
#format_conversion="csdr convert_s8_f"
|
# "lfo_offset": 0,
|
||||||
"""
|
"profiles": {
|
||||||
To use a HackRF, compile the HackRF host tools from its "stdout" branch:
|
"70cm": {
|
||||||
git clone https://github.com/mossmann/hackrf/
|
"name": "70cm Relais",
|
||||||
cd hackrf
|
"center_freq": 438800000,
|
||||||
git fetch
|
"rf_gain": 30,
|
||||||
git checkout origin/stdout
|
"samp_rate": 2400000,
|
||||||
cd host
|
"start_freq": 439275000,
|
||||||
mkdir build
|
"start_mod": "nfm"
|
||||||
cd build
|
},
|
||||||
cmake .. -DINSTALL_UDEV_RULES=ON
|
"2m": {
|
||||||
make
|
"name": "2m komplett",
|
||||||
sudo make install
|
"center_freq": 145000000,
|
||||||
"""
|
"rf_gain": 30,
|
||||||
|
"samp_rate": 2400000,
|
||||||
# >> Sound card SDR (needs ALSA)
|
"start_freq": 145725000,
|
||||||
# I did not have the chance to properly test it.
|
"start_mod": "nfm"
|
||||||
#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"
|
},
|
||||||
|
"sdrplay": {
|
||||||
# >> /dev/urandom test signal source
|
"name": "SDRPlay RSP2",
|
||||||
# samp_rate = 2400000
|
"type": "sdrplay",
|
||||||
# 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)
|
"ppm": 0,
|
||||||
# format_conversion="csdr convert_u8_f"
|
"profiles": {
|
||||||
|
"20m": {
|
||||||
# >> Pre-recorded raw I/Q file as signal source
|
"name":"20m",
|
||||||
# You will have to correctly specify: samp_rate, center_freq, format_conversion in order to correctly play an I/Q file.
|
"center_freq": 14150000,
|
||||||
#start_rtl_command="(while true; do cat my_iq_file.raw; done) | csdr flowcontrol {sr} 20 ".format(sr=samp_rate*2*1.05)
|
"rf_gain": 4,
|
||||||
#format_conversion="csdr convert_u8_f"
|
"samp_rate": 500000,
|
||||||
|
"start_freq": 14070000,
|
||||||
#>> The rx_sdr command works with a variety of SDR harware: RTL-SDR, HackRF, SDRplay, UHD, Airspy, Red Pitaya, audio devices, etc.
|
"start_mod": "usb",
|
||||||
# It will auto-detect your SDR hardware if the following tools are installed:
|
"antenna": "Antenna A"
|
||||||
# * the vendor provided driver and library,
|
},
|
||||||
# * the vendor-specific SoapySDR wrapper library,
|
"30m": {
|
||||||
# * and SoapySDR itself.
|
"name":"30m",
|
||||||
# Check out this article on the OpenWebRX Wiki: https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX/
|
"center_freq": 10125000,
|
||||||
#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)
|
"rf_gain": 4,
|
||||||
#format_conversion=""
|
"samp_rate": 250000,
|
||||||
|
"start_freq": 10142000,
|
||||||
# >> gr-osmosdr signal source using GNU Radio (follow this guide: https://github.com/simonyiszk/openwebrx/wiki/Using-GrOsmoSDR-as-signal-source)
|
"start_mod": "usb"
|
||||||
#start_rtl_command="cat /tmp/osmocom_fifo"
|
},
|
||||||
#format_conversion=""
|
"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 ====
|
# ==== Misc settings ====
|
||||||
|
|
||||||
shown_center_freq = center_freq #you can change this if you use an upconverter
|
|
||||||
|
|
||||||
client_audio_buffer_size = 5
|
client_audio_buffer_size = 5
|
||||||
#increasing client_audio_buffer_size will:
|
#increasing client_audio_buffer_size will:
|
||||||
# - also increase the latency
|
# - also increase the latency
|
||||||
# - decrease the chance of audio underruns
|
# - decrease the chance of audio underruns
|
||||||
|
|
||||||
start_freq = center_freq
|
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.
|
||||||
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"
|
|
||||||
|
|
||||||
# ==== Color themes ====
|
# ==== Color themes ====
|
||||||
|
|
||||||
#A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels
|
#A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels
|
||||||
|
|
||||||
### default theme by teejez:
|
### default theme by teejez:
|
||||||
waterfall_colors = "[0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]"
|
waterfall_colors = [0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]
|
||||||
waterfall_min_level = -88 #in dB
|
waterfall_min_level = -88 #in dB
|
||||||
waterfall_max_level = -20
|
waterfall_max_level = -20
|
||||||
waterfall_auto_level_margin = (5, 40)
|
waterfall_auto_level_margin = (5, 40)
|
||||||
@ -197,7 +221,7 @@ waterfall_auto_level_margin = (5, 40)
|
|||||||
# 3D view settings
|
# 3D view settings
|
||||||
mathbox_waterfall_frequency_resolution = 128 #bins
|
mathbox_waterfall_frequency_resolution = 128 #bins
|
||||||
mathbox_waterfall_history_length = 10 #seconds
|
mathbox_waterfall_history_length = 10 #seconds
|
||||||
mathbox_waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
|
mathbox_waterfall_colors = [0x000000ff,0x2e6893ff,0x69a5d0ff,0x214b69ff,0x9dc4e0ff,0xfff775ff,0xff8a8aff,0xb20000ff]
|
||||||
|
|
||||||
# === Experimental settings ===
|
# === Experimental settings ===
|
||||||
#Warning! The settings below are very experimental.
|
#Warning! The settings below are very experimental.
|
||||||
@ -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.
|
csdr_through = False # Setting this True will print out how much data is going into the DSP chains.
|
||||||
|
|
||||||
nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux.
|
nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux.
|
||||||
|
|
||||||
#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
|
|
||||||
"""
|
|
||||||
|
431
csdr.py
431
csdr.py
@ -23,13 +23,22 @@ OpenWebRX csdr plugin: do the signal processing with csdr
|
|||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
import code
|
|
||||||
import signal
|
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.samp_rate = 250000
|
||||||
self.output_rate = 11025 #this is default, and cannot be set at the moment
|
self.output_rate = 11025 #this is default, and cannot be set at the moment
|
||||||
self.fft_size = 1024
|
self.fft_size = 1024
|
||||||
@ -45,7 +54,6 @@ class dsp:
|
|||||||
self.fft_compression = "none"
|
self.fft_compression = "none"
|
||||||
self.demodulator = "nfm"
|
self.demodulator = "nfm"
|
||||||
self.name = "csdr"
|
self.name = "csdr"
|
||||||
self.format_conversion = "csdr convert_u8_f"
|
|
||||||
self.base_bufsize = 512
|
self.base_bufsize = 512
|
||||||
self.nc_port = 4951
|
self.nc_port = 4951
|
||||||
self.csdr_dynamic_bufsize = False
|
self.csdr_dynamic_bufsize = False
|
||||||
@ -59,62 +67,72 @@ class dsp:
|
|||||||
self.secondary_fft_size = 1024
|
self.secondary_fft_size = 1024
|
||||||
self.secondary_process_fft = None
|
self.secondary_process_fft = None
|
||||||
self.secondary_process_demod = None
|
self.secondary_process_demod = None
|
||||||
self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", "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_pipe_names=["secondary_shift_pipe"]
|
||||||
self.secondary_offset_freq = 1000
|
self.secondary_offset_freq = 1000
|
||||||
|
self.unvoiced_quality = 1
|
||||||
|
self.modification_lock = threading.Lock()
|
||||||
|
self.output = output
|
||||||
|
|
||||||
def chain(self,which):
|
def chain(self,which):
|
||||||
if which in [ "dmr", "dstar", "nxdn", "ysf" ]:
|
chain ="nc -v 127.0.0.1 {nc_port} | "
|
||||||
self.set_output_rate(48000)
|
if self.csdr_dynamic_bufsize: chain += "csdr setbuf {start_bufsize} | "
|
||||||
else:
|
if self.csdr_through: chain +="csdr through | "
|
||||||
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 | "
|
|
||||||
if which == "fft":
|
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 logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \
|
||||||
"csdr fft_exchange_sides_ff {fft_size}"
|
"csdr fft_exchange_sides_ff {fft_size}"
|
||||||
if self.fft_compression=="adpcm":
|
if self.fft_compression=="adpcm":
|
||||||
return fft_chain_base+" | csdr compress_fft_adpcm_f_u8 {fft_size}"
|
chain += " | csdr compress_fft_adpcm_f_u8 {fft_size}"
|
||||||
else:
|
return chain
|
||||||
return fft_chain_base
|
chain += "csdr shift_addition_cc --fifo {shift_pipe} | "
|
||||||
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 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:
|
if self.secondary_demodulator:
|
||||||
chain_begin+="csdr tee {iqtee_pipe} | "
|
chain += "csdr tee {iqtee_pipe} | "
|
||||||
chain_begin+="csdr tee {iqtee2_pipe} | "
|
chain += "csdr tee {iqtee2_pipe} | "
|
||||||
chain_end = ""
|
# 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":
|
if self.audio_compression=="adpcm":
|
||||||
chain_end = " | csdr encode_ima_adpcm_i16_u8"
|
chain += " | 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
|
return chain
|
||||||
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
|
|
||||||
|
|
||||||
def secondary_chain(self, which):
|
def secondary_chain(self, which):
|
||||||
secondary_chain_base="cat {input_pipe} | "
|
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 "")
|
return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "")
|
||||||
elif which == "bpsk31":
|
elif which == "bpsk31":
|
||||||
return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \
|
return 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 simple_agc_cc 0.001 0.5 | " + \
|
||||||
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \
|
"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 dbpsk_decoder_c_u8 | " + \
|
||||||
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8"
|
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8"
|
||||||
|
|
||||||
def set_secondary_demodulator(self, what):
|
def set_secondary_demodulator(self, what):
|
||||||
|
if self.get_secondary_demodulator() == what:
|
||||||
|
return
|
||||||
self.secondary_demodulator = what
|
self.secondary_demodulator = what
|
||||||
|
self.restart()
|
||||||
|
|
||||||
def secondary_fft_block_size(self):
|
def secondary_fft_block_size(self):
|
||||||
return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here
|
return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here
|
||||||
@ -139,12 +160,12 @@ class dsp:
|
|||||||
|
|
||||||
def secondary_bpf_cutoff(self):
|
def secondary_bpf_cutoff(self):
|
||||||
if self.secondary_demodulator == "bpsk31":
|
if self.secondary_demodulator == "bpsk31":
|
||||||
return (31.25/2) / self.if_samp_rate()
|
return 31.25 / self.if_samp_rate()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def secondary_bpf_transition_bw(self):
|
def secondary_bpf_transition_bw(self):
|
||||||
if self.secondary_demodulator == "bpsk31":
|
if self.secondary_demodulator == "bpsk31":
|
||||||
return (31.25/2) / self.if_samp_rate()
|
return 31.25 / self.if_samp_rate()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def secondary_samples_per_bits(self):
|
def secondary_samples_per_bits(self):
|
||||||
@ -157,51 +178,46 @@ class dsp:
|
|||||||
return 31.25
|
return 31.25
|
||||||
|
|
||||||
def start_secondary_demodulator(self):
|
def start_secondary_demodulator(self):
|
||||||
if(not self.secondary_demodulator): return
|
if not self.secondary_demodulator: return
|
||||||
print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate()
|
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_fft=self.secondary_chain("fft")
|
||||||
secondary_command_demod=self.secondary_chain(self.secondary_demodulator)
|
secondary_command_demod=self.secondary_chain(self.secondary_demodulator)
|
||||||
self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft)
|
self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft)
|
||||||
|
|
||||||
secondary_command_fft=secondary_command_fft.format( \
|
secondary_command_fft=secondary_command_fft.format(
|
||||||
input_pipe=self.iqtee_pipe, \
|
input_pipe=self.iqtee_pipe,
|
||||||
secondary_fft_input_size=self.secondary_fft_size, \
|
secondary_fft_input_size=self.secondary_fft_size,
|
||||||
secondary_fft_size=self.secondary_fft_size, \
|
secondary_fft_size=self.secondary_fft_size,
|
||||||
secondary_fft_block_size=self.secondary_fft_block_size(), \
|
secondary_fft_block_size=self.secondary_fft_block_size(),
|
||||||
)
|
)
|
||||||
secondary_command_demod=secondary_command_demod.format( \
|
secondary_command_demod=secondary_command_demod.format(
|
||||||
input_pipe=self.iqtee2_pipe, \
|
input_pipe=self.iqtee2_pipe,
|
||||||
secondary_shift_pipe=self.secondary_shift_pipe, \
|
secondary_shift_pipe=self.secondary_shift_pipe,
|
||||||
secondary_decimation=self.secondary_decimation(), \
|
secondary_decimation=self.secondary_decimation(),
|
||||||
secondary_samples_per_bits=self.secondary_samples_per_bits(), \
|
secondary_samples_per_bits=self.secondary_samples_per_bits(),
|
||||||
secondary_bpf_cutoff=self.secondary_bpf_cutoff(), \
|
secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
|
||||||
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), \
|
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(),
|
||||||
if_samp_rate=self.if_samp_rate()
|
if_samp_rate=self.if_samp_rate()
|
||||||
)
|
)
|
||||||
|
|
||||||
print "[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft
|
logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft)
|
||||||
print "[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod
|
logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (demod) = %s", secondary_command_demod)
|
||||||
#code.interact(local=locals())
|
|
||||||
my_env=os.environ.copy()
|
my_env=os.environ.copy()
|
||||||
#if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
|
#if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
|
||||||
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
|
if self.csdr_print_bufsizes: 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)
|
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
|
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
|
self.secondary_processes_running = True
|
||||||
|
|
||||||
#open control pipes for csdr and send initialization data
|
self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())))
|
||||||
# print "==========> 1"
|
self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 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.set_pipe_nonblocking(self.secondary_process_demod.stdout)
|
#open control pipes for csdr and send initialization data
|
||||||
self.set_pipe_nonblocking(self.secondary_process_fft.stdout)
|
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):
|
def set_secondary_offset_freq(self, value):
|
||||||
self.secondary_offset_freq=value
|
self.secondary_offset_freq=value
|
||||||
@ -212,16 +228,20 @@ class dsp:
|
|||||||
def stop_secondary_demodulator(self):
|
def stop_secondary_demodulator(self):
|
||||||
if self.secondary_processes_running == False: return
|
if self.secondary_processes_running == False: return
|
||||||
self.try_delete_pipes(self.secondary_pipe_names)
|
self.try_delete_pipes(self.secondary_pipe_names)
|
||||||
if self.secondary_process_fft: os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
|
if self.secondary_process_fft:
|
||||||
if self.secondary_process_demod: os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
|
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
|
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):
|
def get_secondary_demodulator(self):
|
||||||
return self.secondary_demodulator
|
return self.secondary_demodulator
|
||||||
|
|
||||||
@ -244,12 +264,20 @@ class dsp:
|
|||||||
if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2)
|
if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2)
|
||||||
|
|
||||||
def set_samp_rate(self,samp_rate):
|
def set_samp_rate(self,samp_rate):
|
||||||
#to change this, restart is required
|
|
||||||
self.samp_rate=samp_rate
|
self.samp_rate=samp_rate
|
||||||
self.decimation=1
|
self.calculate_decimation()
|
||||||
while self.samp_rate/(self.decimation+1)>=self.output_rate:
|
if self.running: self.restart()
|
||||||
self.decimation+=1
|
|
||||||
self.last_decimation=float(self.if_samp_rate())/self.output_rate
|
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):
|
def if_samp_rate(self):
|
||||||
return self.samp_rate/self.decimation
|
return self.samp_rate/self.decimation
|
||||||
@ -260,66 +288,86 @@ class dsp:
|
|||||||
def get_output_rate(self):
|
def get_output_rate(self):
|
||||||
return self.output_rate
|
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):
|
def set_output_rate(self,output_rate):
|
||||||
self.output_rate=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):
|
def set_demodulator(self,demodulator):
|
||||||
#to change this, restart is required
|
if (self.demodulator == demodulator): return
|
||||||
self.demodulator=demodulator
|
self.demodulator=demodulator
|
||||||
|
self.calculate_decimation()
|
||||||
|
self.restart()
|
||||||
|
|
||||||
def get_demodulator(self):
|
def get_demodulator(self):
|
||||||
return self.demodulator
|
return self.demodulator
|
||||||
|
|
||||||
def set_fft_size(self,fft_size):
|
def set_fft_size(self,fft_size):
|
||||||
#to change this, restart is required
|
|
||||||
self.fft_size=fft_size
|
self.fft_size=fft_size
|
||||||
|
self.restart()
|
||||||
|
|
||||||
def set_fft_fps(self,fft_fps):
|
def set_fft_fps(self,fft_fps):
|
||||||
#to change this, restart is required
|
|
||||||
self.fft_fps=fft_fps
|
self.fft_fps=fft_fps
|
||||||
|
self.restart()
|
||||||
|
|
||||||
def set_fft_averages(self,fft_averages):
|
def set_fft_averages(self,fft_averages):
|
||||||
#to change this, restart is required
|
|
||||||
self.fft_averages=fft_averages
|
self.fft_averages=fft_averages
|
||||||
|
self.restart()
|
||||||
|
|
||||||
def fft_block_size(self):
|
def fft_block_size(self):
|
||||||
if self.fft_averages == 0: return self.samp_rate/self.fft_fps
|
if self.fft_averages == 0: return self.samp_rate/self.fft_fps
|
||||||
else: return self.samp_rate/self.fft_fps/self.fft_averages
|
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):
|
def set_offset_freq(self,offset_freq):
|
||||||
self.offset_freq=offset_freq
|
self.offset_freq=offset_freq
|
||||||
if self.running:
|
if self.running:
|
||||||
|
self.modification_lock.acquire()
|
||||||
self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate))
|
self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate))
|
||||||
self.shift_pipe_file.flush()
|
self.shift_pipe_file.flush()
|
||||||
|
self.modification_lock.release()
|
||||||
|
|
||||||
def set_bpf(self,low_cut,high_cut):
|
def set_bpf(self,low_cut,high_cut):
|
||||||
self.low_cut=low_cut
|
self.low_cut=low_cut
|
||||||
self.high_cut=high_cut
|
self.high_cut=high_cut
|
||||||
if self.running:
|
if self.running:
|
||||||
|
self.modification_lock.acquire()
|
||||||
self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) )
|
self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) )
|
||||||
self.bpf_pipe_file.flush()
|
self.bpf_pipe_file.flush()
|
||||||
|
self.modification_lock.release()
|
||||||
|
|
||||||
def get_bpf(self):
|
def get_bpf(self):
|
||||||
return [self.low_cut, self.high_cut]
|
return [self.low_cut, self.high_cut]
|
||||||
|
|
||||||
def set_squelch_level(self, squelch_level):
|
def set_squelch_level(self, squelch_level):
|
||||||
self.squelch_level=squelch_level
|
self.squelch_level=squelch_level
|
||||||
|
#no squelch required on digital voice modes
|
||||||
|
actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level
|
||||||
if self.running:
|
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.squelch_pipe_file.flush()
|
||||||
|
self.modification_lock.release()
|
||||||
|
|
||||||
def get_smeter_level(self):
|
def set_unvoiced_quality(self, q):
|
||||||
if self.running:
|
self.unvoiced_quality = q
|
||||||
line=self.smeter_pipe_file.readline()
|
self.restart()
|
||||||
return float(line[:-1])
|
|
||||||
|
|
||||||
def get_metadata(self):
|
def get_unvoiced_quality(self):
|
||||||
if self.running and self.meta_pipe:
|
return self.unvoiced_quality
|
||||||
return self.meta_pipe_file.readline()
|
|
||||||
|
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):
|
def mkfifo(self,path):
|
||||||
try:
|
try:
|
||||||
@ -332,9 +380,7 @@ class dsp:
|
|||||||
return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate))
|
return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate))
|
||||||
|
|
||||||
def try_create_pipes(self, pipe_names, command_base):
|
def try_create_pipes(self, pipe_names, command_base):
|
||||||
# print "try_create_pipes"
|
|
||||||
for pipe_name in pipe_names:
|
for pipe_name in pipe_names:
|
||||||
# print "\t"+pipe_name
|
|
||||||
if "{"+pipe_name+"}" in command_base:
|
if "{"+pipe_name+"}" in command_base:
|
||||||
setattr(self, pipe_name, self.pipe_base_path+pipe_name)
|
setattr(self, pipe_name, self.pipe_base_path+pipe_name)
|
||||||
self.mkfifo(getattr(self, pipe_name))
|
self.mkfifo(getattr(self, pipe_name))
|
||||||
@ -346,122 +392,107 @@ class dsp:
|
|||||||
pipe_path = getattr(self,pipe_name,None)
|
pipe_path = getattr(self,pipe_name,None)
|
||||||
if pipe_path:
|
if pipe_path:
|
||||||
try: os.unlink(pipe_path)
|
try: os.unlink(pipe_path)
|
||||||
except Exception as e: print "[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e
|
except Exception:
|
||||||
|
logger.exception("try_delete_pipes()")
|
||||||
def set_pipe_nonblocking(self, pipe):
|
|
||||||
flags = fcntl.fcntl(pipe, fcntl.F_GETFL)
|
|
||||||
fcntl.fcntl(pipe, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
self.modification_lock.acquire()
|
||||||
|
if (self.running):
|
||||||
|
self.modification_lock.release()
|
||||||
|
return
|
||||||
|
self.running = True
|
||||||
|
|
||||||
command_base=self.chain(self.demodulator)
|
command_base=self.chain(self.demodulator)
|
||||||
|
|
||||||
#create control pipes for csdr
|
#create control pipes for csdr
|
||||||
self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self))
|
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)
|
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
|
#run the command
|
||||||
command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, \
|
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, \
|
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(), \
|
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, \
|
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 )
|
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
|
logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command)
|
||||||
#code.interact(local=locals())
|
|
||||||
my_env=os.environ.copy()
|
my_env=os.environ.copy()
|
||||||
if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
|
if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
|
||||||
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
|
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
|
||||||
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
|
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
|
||||||
|
|
||||||
#set stdout to non-blocking to avoid blocking the main loop when no audio was decoded in digital modes
|
def watch_thread():
|
||||||
self.set_pipe_nonblocking(self.process.stdout)
|
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
|
self.output.add_output("audio", partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256))
|
||||||
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)
|
|
||||||
|
|
||||||
|
# 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()
|
self.start_secondary_demodulator()
|
||||||
|
|
||||||
def read(self,size):
|
self.modification_lock.release()
|
||||||
return self.process.stdout.read(size)
|
|
||||||
|
|
||||||
def read_async(self, size):
|
# send initial config through the pipes
|
||||||
try:
|
if self.squelch_pipe:
|
||||||
return self.process.stdout.read(size)
|
self.set_squelch_level(self.squelch_level)
|
||||||
except IOError:
|
if self.shift_pipe:
|
||||||
return None
|
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):
|
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()
|
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)
|
self.try_delete_pipes(self.pipe_names)
|
||||||
|
|
||||||
# if self.bpf_pipe:
|
self.modification_lock.release()
|
||||||
# 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
|
|
||||||
|
|
||||||
def restart(self):
|
def restart(self):
|
||||||
|
if not self.running: return
|
||||||
self.stop()
|
self.stop()
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
|
77
htdocs/gfx/google_maps_pin.svg
Normal file
77
htdocs/gfx/google_maps_pin.svg
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="5.6444445mm"
|
||||||
|
height="9.847393mm"
|
||||||
|
viewBox="0 0 20 34.892337"
|
||||||
|
id="svg3455"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.91 r13725"
|
||||||
|
sodipodi:docname="Map Pin.svg">
|
||||||
|
<defs
|
||||||
|
id="defs3457" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="12.181359"
|
||||||
|
inkscape:cx="8.4346812"
|
||||||
|
inkscape:cy="14.715224"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1024"
|
||||||
|
inkscape:window-height="705"
|
||||||
|
inkscape:window-x="-4"
|
||||||
|
inkscape:window-y="-4"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata3460">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-814.59595,-274.38623)">
|
||||||
|
<g
|
||||||
|
id="g3477"
|
||||||
|
transform="matrix(1.1855854,0,0,1.1855854,-151.17715,-57.3976)">
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="sscccccsscs"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path4337-3"
|
||||||
|
d="m 817.11249,282.97118 c -1.25816,1.34277 -2.04623,3.29881 -2.01563,5.13867 0.0639,3.84476 1.79693,5.3002 4.56836,10.59179 0.99832,2.32851 2.04027,4.79237 3.03125,8.87305 0.13772,0.60193 0.27203,1.16104 0.33416,1.20948 0.0621,0.0485 0.19644,-0.51262 0.33416,-1.11455 0.99098,-4.08068 2.03293,-6.54258 3.03125,-8.87109 2.77143,-5.29159 4.50444,-6.74704 4.56836,-10.5918 0.0306,-1.83986 -0.75942,-3.79785 -2.01758,-5.14062 -1.43724,-1.53389 -3.60504,-2.66908 -5.91619,-2.71655 -2.31115,-0.0475 -4.4809,1.08773 -5.91814,2.62162 z"
|
||||||
|
style="display:inline;opacity:1;fill:#ff4646;fill-opacity:1;stroke:#d73534;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<circle
|
||||||
|
r="3.0355"
|
||||||
|
cy="288.25278"
|
||||||
|
cx="823.03064"
|
||||||
|
id="path3049"
|
||||||
|
style="display:inline;opacity:1;fill:#590000;fill-opacity:1;stroke-width:0" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 13 KiB |
BIN
htdocs/gfx/openwebrx-directcall.png
Normal file
BIN
htdocs/gfx/openwebrx-directcall.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
BIN
htdocs/gfx/openwebrx-groupcall.png
Normal file
BIN
htdocs/gfx/openwebrx-groupcall.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
BIN
htdocs/gfx/openwebrx-mute.png
Normal file
BIN
htdocs/gfx/openwebrx-mute.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
@ -22,59 +22,44 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
|
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
|
||||||
<script type="text/javascript">
|
<script src="static/sdr.js"></script>
|
||||||
//Global variables
|
<script src="static/mathbox-bundle.min.js"></script>
|
||||||
var client_id="%[CLIENT_ID]";
|
<script src="static/openwebrx.js"></script>
|
||||||
var ws_url="%[WS_URL]";
|
<script src="static/jquery-3.2.1.min.js"></script>
|
||||||
var rx_photo_height=%[RX_PHOTO_HEIGHT];
|
<script src="static/jquery.nanoscroller.js"></script>
|
||||||
var audio_buffering_fill_to=%[AUDIO_BUFSIZE];
|
<link rel="stylesheet" type="text/css" href="static/nanoscroller.css" />
|
||||||
var starting_mod="%[START_MOD]";
|
<link rel="stylesheet" type="text/css" href="static/openwebrx.css" />
|
||||||
var starting_offset_frequency = %[START_OFFSET_FREQ];
|
|
||||||
var waterfall_colors=%[WATERFALL_COLORS];
|
|
||||||
var waterfall_min_level_default=%[WATERFALL_MIN_LEVEL];
|
|
||||||
var waterfall_max_level_default=%[WATERFALL_MAX_LEVEL];
|
|
||||||
var waterfall_auto_level_margin=%[WATERFALL_AUTO_LEVEL_MARGIN];
|
|
||||||
var server_enable_digimodes=%[DIGIMODES_ENABLE];
|
|
||||||
var mathbox_waterfall_frequency_resolution=%[MATHBOX_WATERFALL_FRES];
|
|
||||||
var mathbox_waterfall_history_length=%[MATHBOX_WATERFALL_THIST];
|
|
||||||
var mathbox_waterfall_colors=%[MATHBOX_WATERFALL_COLORS];
|
|
||||||
</script>
|
|
||||||
<script src="sdr.js"></script>
|
|
||||||
<script src="mathbox-bundle.min.js"></script>
|
|
||||||
<script src="openwebrx.js"></script>
|
|
||||||
<script src="jquery-3.2.1.min.js"></script>
|
|
||||||
<script src="jquery.nanoscroller.js"></script>
|
|
||||||
<link rel="stylesheet" type="text/css" href="nanoscroller.css" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="openwebrx.css" />
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
</head>
|
</head>
|
||||||
<body onload="openwebrx_init();">
|
<body onload="openwebrx_init();">
|
||||||
<div id="webrx-page-container">
|
<div id="webrx-page-container">
|
||||||
<div id="webrx-top-container">
|
<div id="webrx-top-container">
|
||||||
<div id="webrx-top-photo-clip">
|
<div id="webrx-top-photo-clip">
|
||||||
<img src="gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/>
|
<img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/>
|
||||||
<div id="webrx-rx-photo-title">%[RX_PHOTO_TITLE]</div>
|
<div id="webrx-top-bar" class="webrx-top-bar-parts">
|
||||||
<div id="webrx-rx-photo-desc">%[RX_PHOTO_DESC]</div>
|
<a href="https://sdr.hu/openwebrx" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a>
|
||||||
</div>
|
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="static/gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a>
|
||||||
<div id="webrx-top-bar-background" class="webrx-top-bar-parts"></div>
|
<div id="webrx-rx-avatar-background">
|
||||||
<div id="webrx-top-bar" class="webrx-top-bar-parts">
|
<img id="webrx-rx-avatar" src="static/gfx/openwebrx-avatar.png" onclick="toggle_rx_photo();"/>
|
||||||
<a href="https://sdr.hu/openwebrx" target="_blank"><img src="gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a>
|
</div>
|
||||||
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a>
|
<div id="webrx-rx-texts">
|
||||||
<img id="webrx-rx-avatar-background" src="gfx/openwebrx-avatar-background.png" onclick="toggle_rx_photo();"/>
|
<div id="webrx-rx-title" onclick="toggle_rx_photo();"></div>
|
||||||
<img id="webrx-rx-avatar" src="gfx/openwebrx-avatar.png" onclick="toggle_rx_photo();"/>
|
<div id="webrx-rx-desc" onclick="toggle_rx_photo();"></div>
|
||||||
<div id="webrx-rx-title" onclick="toggle_rx_photo();">%[RX_TITLE]</div>
|
</div>
|
||||||
<div id="webrx-rx-desc" onclick="toggle_rx_photo();">%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, <a href="https://www.google.hu/maps/place/%[RX_GPS]" target="_blank" onclick="dont_toggle_rx_photo();">[maps]</a></div>
|
<div id="openwebrx-rx-details-arrow">
|
||||||
<div id="openwebrx-rx-details-arrow">
|
<a id="openwebrx-rx-details-arrow-up" onclick="toggle_rx_photo();"><img src="static/gfx/openwebrx-rx-details-arrow-up.png" /></a>
|
||||||
<a id="openwebrx-rx-details-arrow-up" onclick="toggle_rx_photo();"><img src="gfx/openwebrx-rx-details-arrow-up.png" /></a>
|
<a id="openwebrx-rx-details-arrow-down" onclick="toggle_rx_photo();"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a>
|
||||||
<a id="openwebrx-rx-details-arrow-down" onclick="toggle_rx_photo();"><img src="gfx/openwebrx-rx-details-arrow.png" /></a>
|
</div>
|
||||||
|
<section id="openwebrx-main-buttons">
|
||||||
|
<ul>
|
||||||
|
<li onmouseup="toggle_panel('openwebrx-panel-status');"><img src="static/gfx/openwebrx-panel-status.png" /><br/>Status</li>
|
||||||
|
<li onmouseup="toggle_panel('openwebrx-panel-log');"><img src="static/gfx/openwebrx-panel-log.png" /><br/>Log</li>
|
||||||
|
<li onmouseup="toggle_panel('openwebrx-panel-receiver');"><img src="static/gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<section id="openwebrx-main-buttons">
|
<div id="webrx-rx-photo-title"></div>
|
||||||
<ul>
|
<div id="webrx-rx-photo-desc"></div>
|
||||||
<li onmouseup="toggle_panel('openwebrx-panel-status');"><img src="gfx/openwebrx-panel-status.png" /><br/>Status</li>
|
|
||||||
<li onmouseup="toggle_panel('openwebrx-panel-log');"><img src="gfx/openwebrx-panel-log.png" /><br/>Log</li>
|
|
||||||
<li onmouseup="toggle_panel('openwebrx-panel-receiver');"><img src="gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="webrx-main-container">
|
<div id="webrx-main-container">
|
||||||
@ -90,6 +75,10 @@
|
|||||||
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" data-panel-pos="right" data-panel-order="0" data-panel-size="259,115">
|
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" data-panel-pos="right" data-panel-order="0" data-panel-size="259,115">
|
||||||
<div id="webrx-actual-freq">---.--- MHz</div>
|
<div id="webrx-actual-freq">---.--- MHz</div>
|
||||||
<div id="webrx-mouse-freq">---.--- MHz</div>
|
<div id="webrx-mouse-freq">---.--- MHz</div>
|
||||||
|
<div class="openwebrx-panel-line">
|
||||||
|
<select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="openwebrx-panel-line">
|
<div class="openwebrx-panel-line">
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
|
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
|
||||||
onclick="demodulator_analog_replace('nfm');">FM</div>
|
onclick="demodulator_analog_replace('nfm');">FM</div>
|
||||||
@ -102,12 +91,16 @@
|
|||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw"
|
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw"
|
||||||
onclick="demodulator_analog_replace('cw');">CW</div>
|
onclick="demodulator_analog_replace('cw');">CW</div>
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dmr"
|
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dmr"
|
||||||
|
style="display:none;" data-feature="digital_voice_digiham"
|
||||||
onclick="demodulator_analog_replace('dmr');">DMR</div>
|
onclick="demodulator_analog_replace('dmr');">DMR</div>
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dstar"
|
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dstar"
|
||||||
|
style="display:none;" data-feature="digital_voice_dsd"
|
||||||
onclick="demodulator_analog_replace('dstar');">DStar</div>
|
onclick="demodulator_analog_replace('dstar');">DStar</div>
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nxdn"
|
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nxdn"
|
||||||
|
style="display:none;" data-feature="digital_voice_dsd"
|
||||||
onclick="demodulator_analog_replace('nxdn');">NXDN</div>
|
onclick="demodulator_analog_replace('nxdn');">NXDN</div>
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-ysf"
|
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-ysf"
|
||||||
|
style="display:none;" data-feature="digital_voice_digiham"
|
||||||
onclick="demodulator_analog_replace('ysf');">YSF</div>
|
onclick="demodulator_analog_replace('ysf');">YSF</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="openwebrx-panel-line">
|
<div class="openwebrx-panel-line">
|
||||||
@ -118,23 +111,23 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="openwebrx-panel-line">
|
<div class="openwebrx-panel-line">
|
||||||
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
|
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
|
||||||
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
|
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
|
||||||
<div title="Auto-adjust waterfall colors" id="openwebrx-waterfall-colors-auto" class="openwebrx-button" onclick="waterfall_measure_minmax_now=true;"><img src="gfx/openwebrx-waterfall-auto.png" class="openwebrx-sliderbtn-img"></div>
|
<div title="Auto-adjust waterfall colors" id="openwebrx-waterfall-colors-auto" class="openwebrx-button" onclick="waterfall_measure_minmax_now=true;"><img src="static/gfx/openwebrx-waterfall-auto.png" class="openwebrx-sliderbtn-img"></div>
|
||||||
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">
|
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">
|
||||||
</div>
|
</div>
|
||||||
<div class="openwebrx-panel-line">
|
<div class="openwebrx-panel-line">
|
||||||
<div title="Auto-set squelch level" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><img src="gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
|
<div title="Auto-set squelch level" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
|
||||||
<input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()">
|
<input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()">
|
||||||
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div>
|
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="static/gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div>
|
||||||
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
|
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
|
||||||
</div>
|
</div>
|
||||||
<div class="openwebrx-panel-line">
|
<div class="openwebrx-panel-line">
|
||||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInOneStep();" title="Zoom in one step"> <img src="gfx/openwebrx-zoom-in.png" /></div>
|
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInOneStep();" title="Zoom in one step"> <img src="static/gfx/openwebrx-zoom-in.png" /></div>
|
||||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutOneStep();" title="Zoom out one step"> <img src="gfx/openwebrx-zoom-out.png" /></div>
|
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutOneStep();" title="Zoom out one step"> <img src="static/gfx/openwebrx-zoom-out.png" /></div>
|
||||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInTotal();" title="Zoom in totally"><img src="gfx/openwebrx-zoom-in-total.png" /></div>
|
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInTotal();" title="Zoom in totally"><img src="static/gfx/openwebrx-zoom-in-total.png" /></div>
|
||||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutTotal();" title="Zoom out totally"><img src="gfx/openwebrx-zoom-out-total.png" /></div>
|
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutTotal();" title="Zoom out totally"><img src="static/gfx/openwebrx-zoom-out-total.png" /></div>
|
||||||
<div class="openwebrx-button openwebrx-square-button" onclick="mathbox_toggle();" title="Toggle 3D view"><img src="gfx/openwebrx-3d-spectrum.png" /></div>
|
<div class="openwebrx-button openwebrx-square-button" onclick="mathbox_toggle();" title="Toggle 3D view"><img src="static/gfx/openwebrx-3d-spectrum.png" /></div>
|
||||||
<div id="openwebrx-smeter-db">0 dB</div>
|
<div id="openwebrx-smeter-db">0 dB</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="openwebrx-panel-line">
|
<div class="openwebrx-panel-line">
|
||||||
@ -160,7 +153,7 @@
|
|||||||
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div>
|
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||||
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div>
|
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="openwebrx-panel" data-panel-name="client-under-devel" data-panel-pos="none" data-panel-order="0" data-panel-size="245,55" style="background-color: Red;">
|
<div class="openwebrx-panel" data-panel-name="client-under-devel" data-panel-pos="left" data-panel-order="9" data-panel-size="245,55" style="background-color: Red;">
|
||||||
<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
|
<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
|
||||||
<br />We're working on the code right now, so the application might fail.
|
<br />We're working on the code right now, so the application might fail.
|
||||||
</div>
|
</div>
|
||||||
@ -175,14 +168,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="openwebrx-panel" data-panel-name="metadata" data-panel-pos="left" data-panel-order="1" data-panel-size="615,36">
|
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" data-panel-name="metadata-ysf" data-panel-pos="left" data-panel-order="2" data-panel-size="145,220">
|
||||||
|
<div class="openwebrx-meta-frame">
|
||||||
|
<div class="openwebrx-meta-slot">
|
||||||
|
<div class="openwebrx-ysf-mode openwebrx-meta-autoclear"></div>
|
||||||
|
<div class="openwebrx-meta-user-image"></div>
|
||||||
|
<div class="openwebrx-ysf-source openwebrx-meta-autoclear"></div>
|
||||||
|
<div class="openwebrx-ysf-up openwebrx-meta-autoclear"></div>
|
||||||
|
<div class="openwebrx-ysf-down openwebrx-meta-autoclear"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-dmr" data-panel-name="metadata-dmr" data-panel-pos="left" data-panel-order="2" data-panel-size="300,220">
|
||||||
|
<div class="openwebrx-meta-frame">
|
||||||
|
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||||
|
<div class="openwebrx-dmr-slot">Timeslot 1</div>
|
||||||
|
<div class="openwebrx-meta-user-image"></div>
|
||||||
|
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div>
|
||||||
|
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div>
|
||||||
|
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
|
||||||
|
</div>
|
||||||
|
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||||
|
<div class="openwebrx-dmr-slot">Timeslot 2</div>
|
||||||
|
<div class="openwebrx-meta-user-image"></div>
|
||||||
|
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div>
|
||||||
|
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div>
|
||||||
|
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="openwebrx-big-grey" onclick="iosPlayButtonClick();">
|
<div id="openwebrx-big-grey" onclick="iosPlayButtonClick();">
|
||||||
<div id="openwebrx-play-button-text">
|
<div id="openwebrx-play-button-text">
|
||||||
<img id="openwebrx-play-button" src="gfx/openwebrx-play-button.png" />
|
<img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.png" />
|
||||||
<br /><br />Start OpenWebRX
|
<br /><br />Start OpenWebRX
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -155,22 +155,12 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
|
|
||||||
.webrx-top-bar-parts
|
.webrx-top-bar-parts
|
||||||
{
|
{
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
width:100%;
|
|
||||||
height:67px;
|
height:67px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-top-bar-background
|
|
||||||
{
|
|
||||||
background-color: #808080;
|
|
||||||
opacity: 0.15;
|
|
||||||
filter:alpha(opacity=15);
|
|
||||||
}
|
|
||||||
|
|
||||||
#webrx-top-bar
|
#webrx-top-bar
|
||||||
{
|
{
|
||||||
|
background: rgba(128, 128, 128, 0.15);
|
||||||
margin:0;
|
margin:0;
|
||||||
padding:0;
|
padding:0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -179,20 +169,23 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
-khtml-user-select: none;
|
-khtml-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-top-logo
|
#webrx-top-logo
|
||||||
{
|
{
|
||||||
position: absolute;
|
padding: 12px;
|
||||||
top: 12px;
|
float: left;
|
||||||
left: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-ha5kfu-top-logo
|
#webrx-ha5kfu-top-logo
|
||||||
{
|
{
|
||||||
position: absolute;
|
float: right;
|
||||||
top: 15px;
|
padding: 15px;
|
||||||
right: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-top-photo
|
#webrx-top-photo
|
||||||
@ -204,79 +197,37 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
#webrx-rx-avatar-background
|
#webrx-rx-avatar-background
|
||||||
{
|
{
|
||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
position: absolute;
|
background-image: url(gfx/openwebrx-avatar-background.png);
|
||||||
left: 285px;
|
background-origin: content-box;
|
||||||
top: 6px;
|
background-repeat: no-repeat;
|
||||||
|
float: left;
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
padding: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-rx-avatar
|
#webrx-rx-avatar
|
||||||
{
|
{
|
||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
position: absolute;
|
|
||||||
left: 289px;
|
|
||||||
top: 10px;
|
|
||||||
width: 46px;
|
width: 46px;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-top-photo-clip
|
#webrx-top-photo-clip
|
||||||
{
|
{
|
||||||
|
min-height: 67px;
|
||||||
max-height: 350px;
|
max-height: 350px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*#webrx-bottom-bar
|
|
||||||
{
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
width: 100%;
|
|
||||||
height: 117px;
|
|
||||||
background-image:url(gfx/webrx-bottom-bar.png);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
#webrx-page-container
|
#webrx-page-container
|
||||||
{
|
{
|
||||||
min-height:100%;
|
min-height:100%;
|
||||||
position:relative;
|
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
|
#webrx-rx-photo-title
|
||||||
{
|
{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -303,10 +254,17 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
|
|
||||||
#webrx-rx-photo-desc a
|
#webrx-rx-photo-desc a
|
||||||
{
|
{
|
||||||
/*color: #007df1;*/
|
|
||||||
color: #5ca8ff;
|
color: #5ca8ff;
|
||||||
text-shadow: none;
|
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
|
#webrx-rx-title
|
||||||
@ -314,9 +272,6 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
white-space:nowrap;
|
white-space:nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
position: absolute;
|
|
||||||
left: 350px;
|
|
||||||
top: 13px;
|
|
||||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
||||||
color: #909090;
|
color: #909090;
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
@ -330,15 +285,11 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
color: #909090;
|
color: #909090;
|
||||||
position: absolute;
|
|
||||||
left: 350px;
|
|
||||||
top: 34px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-rx-desc a
|
#webrx-rx-desc a
|
||||||
{
|
{
|
||||||
color: #909090;
|
color: #909090;
|
||||||
/*text-decoration: none;*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#openwebrx-rx-details-arrow
|
#openwebrx-rx-details-arrow
|
||||||
@ -718,9 +669,7 @@ img.openwebrx-mirror-img
|
|||||||
|
|
||||||
#openwebrx-main-buttons
|
#openwebrx-main-buttons
|
||||||
{
|
{
|
||||||
position: absolute;
|
float: right;
|
||||||
right: 133px;
|
|
||||||
top: 3px;
|
|
||||||
margin:0;
|
margin:0;
|
||||||
color: white;
|
color: white;
|
||||||
text-shadow: 0px 0px 4px #000000;
|
text-shadow: 0px 0px 4px #000000;
|
||||||
@ -841,10 +790,7 @@ img.openwebrx-mirror-img
|
|||||||
transition: width 500ms, left 500ms;
|
transition: width 500ms, left 500ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
#openwebrx-secondary-demod-listbox
|
.openwebrx-panel select {
|
||||||
{
|
|
||||||
width: 201px;
|
|
||||||
height: 27px;
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: #373737;
|
background-color: #373737;
|
||||||
color: White;
|
color: White;
|
||||||
@ -856,16 +802,27 @@ img.openwebrx-mirror-img
|
|||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
border-width: 0px;
|
border-width: 0px;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
padding-left:3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#openwebrx-secondary-demod-listbox option
|
.openwebrx-panel select option {
|
||||||
{
|
|
||||||
border-width: 0px;
|
border-width: 0px;
|
||||||
background-color: #373737;
|
background-color: #373737;
|
||||||
color: White;
|
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
|
#openwebrx-cursor-blink
|
||||||
{
|
{
|
||||||
animation: cursor-blink 1s infinite;
|
animation: cursor-blink 1s infinite;
|
||||||
@ -971,3 +928,88 @@ img.openwebrx-mirror-img
|
|||||||
border-color: Red;
|
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;
|
||||||
|
}
|
||||||
|
@ -52,6 +52,7 @@ var waterfall_setup_done=0;
|
|||||||
var waterfall_queue = [];
|
var waterfall_queue = [];
|
||||||
var waterfall_timer;
|
var waterfall_timer;
|
||||||
var secondary_fft_size;
|
var secondary_fft_size;
|
||||||
|
var audio_allowed;
|
||||||
|
|
||||||
/*function fade(something,from,to,time_ms,fps)
|
/*function fade(something,from,to,time_ms,fps)
|
||||||
{
|
{
|
||||||
@ -79,7 +80,9 @@ is_chrome = /Chrome/.test(navigator.userAgent);
|
|||||||
|
|
||||||
function init_rx_photo()
|
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-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() { animate(e("webrx-rx-photo-desc"),"opacity","",1,0,1,500,30); },1500);
|
||||||
window.setTimeout(function() { close_rx_photo() },2500);
|
window.setTimeout(function() { close_rx_photo() },2500);
|
||||||
@ -133,14 +136,14 @@ function toggleMute()
|
|||||||
if (mute) {
|
if (mute) {
|
||||||
mute = false;
|
mute = false;
|
||||||
e("openwebrx-mute-on").id="openwebrx-mute-off";
|
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").disabled=false;
|
||||||
e("openwebrx-panel-volume").style.opacity=1.0;
|
e("openwebrx-panel-volume").style.opacity=1.0;
|
||||||
e("openwebrx-panel-volume").value = volumeBeforeMute;
|
e("openwebrx-panel-volume").value = volumeBeforeMute;
|
||||||
} else {
|
} else {
|
||||||
mute = true;
|
mute = true;
|
||||||
e("openwebrx-mute-off").id="openwebrx-mute-on";
|
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").disabled=true;
|
||||||
e("openwebrx-panel-volume").style.opacity=0.5;
|
e("openwebrx-panel-volume").style.opacity=0.5;
|
||||||
volumeBeforeMute = e("openwebrx-panel-volume").value;
|
volumeBeforeMute = e("openwebrx-panel-volume").value;
|
||||||
@ -160,7 +163,7 @@ function updateSquelch()
|
|||||||
{
|
{
|
||||||
var sliderValue=parseInt(e("openwebrx-panel-squelch").value);
|
var sliderValue=parseInt(e("openwebrx-panel-squelch").value);
|
||||||
var outputValue=(sliderValue==parseInt(e("openwebrx-panel-squelch").min))?0:getLinearSmeterValue(sliderValue);
|
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)
|
function updateWaterfallColors(which)
|
||||||
@ -433,8 +436,8 @@ function demodulator_default_analog(offset_frequency,subtype)
|
|||||||
}
|
}
|
||||||
else if(subtype=="dmr" || subtype=="ysf")
|
else if(subtype=="dmr" || subtype=="ysf")
|
||||||
{
|
{
|
||||||
this.low_cut=-6500;
|
this.low_cut=-4000;
|
||||||
this.high_cut=6500;
|
this.high_cut=4000;
|
||||||
}
|
}
|
||||||
else if(subtype=="dstar" || subtype=="nxdn")
|
else if(subtype=="dstar" || subtype=="nxdn")
|
||||||
{
|
{
|
||||||
@ -470,9 +473,13 @@ function demodulator_default_analog(offset_frequency,subtype)
|
|||||||
|
|
||||||
this.doset=function(first_time)
|
this.doset=function(first_time)
|
||||||
{ //this function sends demodulator parameters to the server
|
{ //this function sends demodulator parameters to the server
|
||||||
ws.send("SET"+((first_time)?" mod="+this.server_mod:"")+
|
params = {
|
||||||
" low_cut="+this.low_cut.toString()+" high_cut="+this.high_cut.toString()+
|
"low_cut": this.low_cut,
|
||||||
" offset_freq="+this.offset_frequency.toString());
|
"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
|
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_add(new demodulator_default_analog(temp_offset,subtype));
|
||||||
demodulator_buttons_update();
|
demodulator_buttons_update();
|
||||||
|
hide_digitalvoice_panels();
|
||||||
|
toggle_panel("openwebrx-panel-metadata-" + subtype, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function demodulator_set_offset_frequency(which,to_what)
|
function demodulator_set_offset_frequency(which,to_what)
|
||||||
@ -1150,176 +1159,232 @@ function audio_calculate_resampling(targetRate)
|
|||||||
|
|
||||||
debug_ws_data_received=0;
|
debug_ws_data_received=0;
|
||||||
max_clients_num=0;
|
max_clients_num=0;
|
||||||
|
clients_num = 0;
|
||||||
|
|
||||||
var COMPRESS_FFT_PAD_N=10; //should be the same as in csdr.c
|
var COMPRESS_FFT_PAD_N=10; //should be the same as in csdr.c
|
||||||
|
|
||||||
function on_ws_recv(evt)
|
function on_ws_recv(evt)
|
||||||
{
|
{
|
||||||
if(!(evt.data instanceof ArrayBuffer)) { divlog("on_ws_recv(): Not ArrayBuffer received...",1); return; }
|
if (typeof evt.data == 'string') {
|
||||||
//
|
// text messages
|
||||||
debug_ws_data_received+=evt.data.byteLength/1000;
|
debug_ws_data_received += evt.data.length / 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 (evt.data.substr(0, 16) == "CLIENT DE SERVER") {
|
||||||
if(first3Chars=="AUD")
|
divlog("Server acknowledged WebSocket connection.");
|
||||||
{
|
} else {
|
||||||
var audio_data;
|
try {
|
||||||
if(audio_compression=="adpcm") audio_data=new Uint8Array(evt.data,4)
|
json = JSON.parse(evt.data)
|
||||||
else audio_data=new Int16Array(evt.data,4);
|
switch (json.type) {
|
||||||
audio_prepare(audio_data);
|
case "config":
|
||||||
audio_buffer_current_size_debug+=audio_data.length;
|
config = json.value;
|
||||||
audio_buffer_all_size_debug+=audio_data.length;
|
window.waterfall_colors = config.waterfall_colors;
|
||||||
if(!(ios||is_chrome) && (audio_initialized==0 && audio_prepared_buffers.length>audio_buffering_fill_to)) audio_init()
|
window.waterfall_min_level_default = config.waterfall_min_level;
|
||||||
}
|
window.waterfall_max_level_default = config.waterfall_max_level;
|
||||||
else if(first3Chars=="FFT")
|
window.waterfall_auto_level_margin = config.waterfall_auto_level_margin;
|
||||||
{
|
waterfallColorsDefault();
|
||||||
//alert("Yupee! Doing FFT");
|
|
||||||
//if(first4Chars=="FFTS") console.log("FFTS");
|
window.starting_mod = config.start_mod
|
||||||
if(fft_compression=="none") waterfall_add_queue(new Float32Array(evt.data,4));
|
window.starting_offset_frequency = config.start_offset_frequency;
|
||||||
else if(fft_compression="adpcm")
|
window.audio_buffering_fill_to = config.client_audio_buffer_size;
|
||||||
{
|
bandwidth = config.samp_rate;
|
||||||
fft_codec.reset();
|
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;i<waterfall_i16.length;i++) waterfall_f32[i]=waterfall_i16[i+COMPRESS_FFT_PAD_N]/100;
|
|
||||||
if(first4Chars=="FFTS") secondary_demod_waterfall_add_queue(waterfall_f32); //TODO digimodes
|
|
||||||
else waterfall_add_queue(waterfall_f32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(first3Chars=="DAT")
|
|
||||||
{
|
|
||||||
//secondary_demod_push_binary_data(new Uint8Array(evt.data,4));
|
|
||||||
secondary_demod_push_data(arrayBufferToString(evt.data).substring(4));
|
|
||||||
//console.log("DAT");
|
|
||||||
}
|
|
||||||
else if(first3Chars=="MSG")
|
|
||||||
{
|
|
||||||
/*try
|
|
||||||
{*/
|
|
||||||
var stringData=arrayBufferToString(evt.data);
|
|
||||||
params=stringData.substring(4).split(" ");
|
|
||||||
for(i=0;i<params.length;i++)
|
|
||||||
{
|
|
||||||
param=params[i].split("=");
|
|
||||||
switch(param[0])
|
|
||||||
{
|
|
||||||
case "setup":
|
|
||||||
waterfall_init();
|
waterfall_init();
|
||||||
audio_preinit();
|
audio_preinit();
|
||||||
break;
|
|
||||||
case "bandwidth":
|
if (audio_allowed && !audio_initialized) audio_init();
|
||||||
bandwidth=parseInt(param[1]);
|
waterfall_clear();
|
||||||
break;
|
break;
|
||||||
case "center_freq":
|
case "secondary_config":
|
||||||
center_freq=parseInt(param[1]); //there was no ; and it was no problem... why?
|
window.secondary_fft_size = json.value.secondary_fft_size;
|
||||||
break;
|
window.secondary_bw = json.value.secondary_bw;
|
||||||
case "fft_size":
|
window.if_samp_rate = json.value.if_samp_rate;
|
||||||
fft_size=parseInt(param[1]);
|
|
||||||
break;
|
|
||||||
case "secondary_fft_size":
|
|
||||||
secondary_fft_size=parseInt(param[1]);
|
|
||||||
break;
|
|
||||||
case "secondary_setup":
|
|
||||||
secondary_demod_init_canvases();
|
secondary_demod_init_canvases();
|
||||||
break;
|
break;
|
||||||
case "if_samp_rate":
|
case "receiver_details":
|
||||||
if_samp_rate=parseInt(param[1]);
|
var r = json.value;
|
||||||
break;
|
e('webrx-rx-title').innerHTML = r.receiver_name;
|
||||||
case "secondary_bw":
|
e('webrx-rx-desc').innerHTML = r.receiver_location + ' | Loc: ' + r.receiver_qra + ', ASL: ' + r.receiver_asl + ' m, <a href="https://www.google.hu/maps/place/' + r.receiver_gps[0] + ',' + r.receiver_gps[1] + '" target="_blank" onclick="dont_toggle_rx_photo();">[maps]</a>';
|
||||||
secondary_bw=parseFloat(param[1]);
|
e('webrx-rx-photo-title').innerHTML = r.photo_title;
|
||||||
break;
|
e('webrx-rx-photo-desc').innerHTML = r.photo_desc;
|
||||||
case "fft_fps":
|
break;
|
||||||
fft_fps=parseInt(param[1]);
|
case "smeter":
|
||||||
break;
|
smeter_level = json.value;
|
||||||
case "audio_compression":
|
setSmeterAbsoluteValue(smeter_level);
|
||||||
audio_compression=param[1];
|
break;
|
||||||
divlog( "Audio stream is "+ ((audio_compression=="adpcm")?"compressed":"uncompressed")+"." )
|
case "cpuusage":
|
||||||
break;
|
var server_cpu_usage = json.value;
|
||||||
case "fft_compression":
|
progressbar_set(e("openwebrx-bar-server-cpu"),server_cpu_usage,"Server CPU [" + Math.round(server_cpu_usage * 100) + "%]",server_cpu_usage>85);
|
||||||
fft_compression=param[1];
|
break;
|
||||||
divlog( "FFT stream is "+ ((fft_compression=="adpcm")?"compressed":"uncompressed")+"." )
|
case "clients":
|
||||||
break;
|
client_num = json.value;
|
||||||
case "cpu_usage":
|
progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num*0.85);
|
||||||
var server_cpu_usage=parseInt(param[1]);
|
break;
|
||||||
progressbar_set(e("openwebrx-bar-server-cpu"),server_cpu_usage/100,"Server CPU ["+param[1]+"%]",server_cpu_usage>85);
|
case "profiles":
|
||||||
break;
|
var listbox = e("openwebrx-sdr-profiles-listbox");
|
||||||
case "clients":
|
listbox.innerHTML = json.value.map(function(profile){
|
||||||
var clients_num=parseInt(param[1]);
|
return '<option value="' + profile.id + '">' + profile.name + "</option>";
|
||||||
progressbar_set(e("openwebrx-bar-clients"),clients_num/max_clients_num,"Clients ["+param[1]+"]",clients_num>max_clients_num*0.85);
|
}).join("");
|
||||||
break;
|
break;
|
||||||
case "max_clients":
|
case "features":
|
||||||
max_clients_num=parseInt(param[1]);
|
for (var feature in json.value) {
|
||||||
break;
|
$('[data-feature="' + feature + '"')[json.value[feature] ? "show" : "hide"]();
|
||||||
case "s":
|
}
|
||||||
smeter_level=parseFloat(param[1]);
|
break;
|
||||||
setSmeterAbsoluteValue(smeter_level);
|
case "metadata":
|
||||||
break;
|
update_metadata(json.value);
|
||||||
}
|
break;
|
||||||
}
|
default:
|
||||||
/*}
|
console.warn('received message of unknown type: ' + json.type);
|
||||||
catch(err)
|
}
|
||||||
{
|
} catch (e) {
|
||||||
divlog("Received invalid message over WebSocket.");
|
// don't lose exception
|
||||||
}*/
|
console.error(e)
|
||||||
} else if (first3Chars=='MET')
|
}
|
||||||
{
|
}
|
||||||
var stringData=arrayBufferToString(evt.data);
|
} else if (evt.data instanceof ArrayBuffer) {
|
||||||
var metaPanels = Array.prototype.filter.call(document.getElementsByClassName('openwebrx-panel'), function(el) {
|
// binary messages
|
||||||
return el.dataset.panelName == 'metadata';
|
debug_ws_data_received += evt.data.byteLength / 1000;
|
||||||
});
|
|
||||||
|
|
||||||
var meta = {};
|
type = new Uint8Array(evt.data, 0, 1)[0]
|
||||||
stringData.substr(4).split(";").forEach(function(s) {
|
data = evt.data.slice(1)
|
||||||
var item = s.split(":");
|
|
||||||
meta[item[0]] = item[1];
|
|
||||||
});
|
|
||||||
|
|
||||||
var update = function(el) {
|
switch (type) {
|
||||||
el.innerHTML = "";
|
case 1:
|
||||||
}
|
// FFT data
|
||||||
if (meta.protocol) switch (meta.protocol) {
|
if (fft_compression=="none") {
|
||||||
case 'DMR':
|
waterfall_add_queue(new Float32Array(data));
|
||||||
if (meta.slot) {
|
} else if (fft_compression == "adpcm") {
|
||||||
var html = 'Timeslot: ' + meta.slot;
|
fft_codec.reset();
|
||||||
if (meta.type) html += ' Typ: ' + meta.type;
|
|
||||||
if (meta.source && meta.target) html += ' Source: ' + meta.source + ' Target: ' + meta.target;
|
var waterfall_i16=fft_codec.decode(new Uint8Array(data));
|
||||||
update = function(el) {
|
var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N);
|
||||||
var slotEl = el.getElementsByClassName('slot-' + meta.slot);
|
for(var i=0;i<waterfall_i16.length;i++) waterfall_f32[i]=waterfall_i16[i+COMPRESS_FFT_PAD_N]/100;
|
||||||
if (!slotEl.length) {
|
waterfall_add_queue(waterfall_f32);
|
||||||
slotEl = document.createElement('div');
|
|
||||||
slotEl.className = 'slot-' + meta.slot;
|
|
||||||
el.appendChild(slotEl);
|
|
||||||
} else {
|
|
||||||
slotEl = slotEl[0];
|
|
||||||
}
|
|
||||||
slotEl.innerHTML = html;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'YSF':
|
case 2:
|
||||||
var strings = [];
|
// audio data
|
||||||
if (meta.source) strings.push("Source: " + meta.source);
|
var audio_data;
|
||||||
if (meta.target) strings.push("Destination: " + meta.target);
|
if (audio_compression=="adpcm") {
|
||||||
if (meta.up) strings.push("Up: " + meta.up);
|
audio_data = new Uint8Array(data);
|
||||||
if (meta.down) strings.push("Down: " + meta.down);
|
} else {
|
||||||
var html = strings.join(' ');
|
audio_data = new Int16Array(data);
|
||||||
update = function(el) {
|
|
||||||
el.innerHTML = html;
|
|
||||||
}
|
}
|
||||||
break;
|
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()
|
||||||
|
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<waterfall_i16.length;i++) waterfall_f32[i]=waterfall_i16[i+COMPRESS_FFT_PAD_N]/100;
|
||||||
|
secondary_demod_waterfall_add_queue(waterfall_f32); //TODO digimodes
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// secondary demod
|
||||||
|
secondary_demod_push_data(arrayBufferToString(data));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn('unknown type of binary message: ' + type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_metadata(meta) {
|
||||||
|
if (meta.protocol) switch (meta.protocol) {
|
||||||
|
case 'DMR':
|
||||||
|
if (meta.slot) {
|
||||||
|
var el = $("#openwebrx-panel-metadata-dmr .openwebrx-dmr-timeslot-panel").get(meta.slot);
|
||||||
|
var id = "";
|
||||||
|
var name = "";
|
||||||
|
var target = "";
|
||||||
|
var group = false;
|
||||||
|
$(el)[meta.sync ? "addClass" : "removeClass"]("sync");
|
||||||
|
if (meta.sync && meta.sync == "voice") {
|
||||||
|
id = (meta.additional && meta.additional.callsign) || meta.source || "";
|
||||||
|
name = (meta.additional && meta.additional.fname) || "";
|
||||||
|
if (meta.type == "group") {
|
||||||
|
target = "Talkgroup: ";
|
||||||
|
group = true;
|
||||||
|
}
|
||||||
|
if (meta.type == "direct") target = "Direct: ";
|
||||||
|
target += meta.target || "";
|
||||||
|
$(el).addClass("active");
|
||||||
|
} else {
|
||||||
|
$(el).removeClass("active");
|
||||||
|
}
|
||||||
|
$(el).find(".openwebrx-dmr-id").text(id);
|
||||||
|
$(el).find(".openwebrx-dmr-name").text(name);
|
||||||
|
$(el).find(".openwebrx-dmr-target").text(target);
|
||||||
|
$(el).find(".openwebrx-meta-user-image")[group ? "addClass" : "removeClass"]("group");
|
||||||
|
} else {
|
||||||
|
clear_metadata();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'YSF':
|
||||||
|
var el = $("#openwebrx-panel-metadata-ysf");
|
||||||
|
|
||||||
|
var mode = " "
|
||||||
|
var source = "";
|
||||||
|
var up = "";
|
||||||
|
var down = "";
|
||||||
|
if (meta.mode && meta.mode != "") {
|
||||||
|
mode = "Mode: " + meta.mode;
|
||||||
|
source = meta.source || "";
|
||||||
|
if (meta.lat && meta.lon) {
|
||||||
|
source = "<a class=\"openwebrx-maps-pin\" href=\"https://www.google.com/maps/search/?api=1&query=" + meta.lat + "," + meta.lon + "\" target=\"_blank\"></a>" + 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)
|
function add_problem(what)
|
||||||
{
|
{
|
||||||
problems_span=e("openwebrx-problems");
|
problems_span=e("openwebrx-problems");
|
||||||
@ -1630,7 +1695,9 @@ function audio_flush_notused()
|
|||||||
|
|
||||||
function webrx_set_param(what, value)
|
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;
|
var starting_mute = false;
|
||||||
@ -1645,7 +1712,7 @@ function parsehash()
|
|||||||
if(harr[0]=="mute") toggleMute();
|
if(harr[0]=="mute") toggleMute();
|
||||||
else if(harr[0]=="mod") starting_mod = harr[1];
|
else if(harr[0]=="mod") starting_mod = harr[1];
|
||||||
else if(harr[0]=="sql")
|
else if(harr[0]=="sql")
|
||||||
{
|
{
|
||||||
e("openwebrx-panel-squelch").value=harr[1];
|
e("openwebrx-panel-squelch").value=harr[1];
|
||||||
updateSquelch();
|
updateSquelch();
|
||||||
}
|
}
|
||||||
@ -1680,16 +1747,18 @@ function audio_preinit()
|
|||||||
else if(audio_context.sampleRate>44100*4)
|
else if(audio_context.sampleRate>44100*4)
|
||||||
audio_buffer_size = 4096 * 4;
|
audio_buffer_size = 4096 * 4;
|
||||||
|
|
||||||
audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED);
|
if (!audio_rebuffer) {
|
||||||
audio_last_output_buffer = new Float32Array(audio_buffer_size);
|
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
|
//we send our setup packet
|
||||||
parsehash();
|
parsehash();
|
||||||
|
|
||||||
audio_calculate_resampling(audio_context.sampleRate);
|
audio_calculate_resampling(audio_context.sampleRate);
|
||||||
audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1);
|
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
|
}
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({"type":"dspcontrol","action":"start","params":{"output_rate":audio_server_output_rate}}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function audio_init()
|
function audio_init()
|
||||||
@ -1751,7 +1820,10 @@ function on_ws_closed()
|
|||||||
audio_node.disconnect();
|
audio_node.disconnect();
|
||||||
}
|
}
|
||||||
catch (dont_care) {}
|
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)
|
function on_ws_error(event)
|
||||||
@ -1763,14 +1835,15 @@ String.prototype.startswith=function(str){ return this.indexOf(str) == 0; }; //h
|
|||||||
|
|
||||||
function open_websocket()
|
function open_websocket()
|
||||||
{
|
{
|
||||||
//if(ws_url.startswith("ws://localhost:")&&window.location.hostname!="127.0.0.1"&&window.location.hostname!="localhost")
|
var protocol = 'ws';
|
||||||
//{
|
if (window.location.toString().startsWith('https://')) {
|
||||||
//divlog("Server administrator should set <em>server_hostname</em> correctly, because it is left as <em>\"localhost\"</em>. Now guessing hostname from page URL.",1);
|
protocol = 'wss';
|
||||||
ws_url="ws://"+(window.location.origin.split("://")[1])+"/ws/"; //guess automatically -> now default behaviour
|
}
|
||||||
//}
|
|
||||||
|
ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; //guess automatically -> now default behaviour
|
||||||
if (!("WebSocket" in window))
|
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.");
|
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.onopen = on_ws_opened;
|
||||||
ws.onmessage = on_ws_recv;
|
ws.onmessage = on_ws_recv;
|
||||||
ws.onclose = on_ws_closed;
|
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;
|
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
|
//Handle mathbox
|
||||||
for(var i=0;i<fft_size;i++) mathbox_data[i+mathbox_data_index*fft_size]=data[i];
|
for(var i=0;i<fft_size;i++) mathbox_data[i+mathbox_data_index*fft_size]=data[i];
|
||||||
mathbox_shift();
|
mathbox_shift();
|
||||||
}
|
} else {
|
||||||
else
|
//Add line to waterfall image
|
||||||
{
|
oneline_image = canvas_context.createImageData(w,1);
|
||||||
//Add line to waterfall image
|
for (x=0;x<w;x++) {
|
||||||
oneline_image = canvas_context.createImageData(w,1);
|
color=waterfall_mkcolor(data[x]);
|
||||||
for(x=0;x<w;x++)
|
for(i=0;i<4;i++)
|
||||||
{
|
oneline_image.data[x*4+i] = ((color>>>0)>>((3-i)*8))&0xff;
|
||||||
color=waterfall_mkcolor(data[x]);
|
}
|
||||||
for(i=0;i<4;i++)
|
|
||||||
oneline_image.data[x*4+i] = ((color>>>0)>>((3-i)*8))&0xff;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Draw image
|
//Draw image
|
||||||
canvas_context.putImageData(oneline_image, 0, canvas_actual_line--);
|
canvas_context.putImageData(oneline_image, 0, canvas_actual_line--);
|
||||||
shift_canvases();
|
shift_canvases();
|
||||||
if(canvas_actual_line<0) add_canvas();
|
if(canvas_actual_line<0) add_canvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -2265,6 +2334,7 @@ function openwebrx_init()
|
|||||||
init_rx_photo();
|
init_rx_photo();
|
||||||
open_websocket();
|
open_websocket();
|
||||||
secondary_demod_init();
|
secondary_demod_init();
|
||||||
|
digimodes_init();
|
||||||
place_panels(first_show_panel);
|
place_panels(first_show_panel);
|
||||||
window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000);
|
window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000);
|
||||||
window.addEventListener("resize",openwebrx_resize);
|
window.addEventListener("resize",openwebrx_resize);
|
||||||
@ -2272,7 +2342,26 @@ function openwebrx_init()
|
|||||||
|
|
||||||
//Synchronise volume with slider
|
//Synchronise volume with slider
|
||||||
updateVolume();
|
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()
|
function iosPlayButtonClick()
|
||||||
@ -2281,6 +2370,7 @@ function iosPlayButtonClick()
|
|||||||
audio_init();
|
audio_init();
|
||||||
e("openwebrx-big-grey").style.opacity=0;
|
e("openwebrx-big-grey").style.opacity=0;
|
||||||
window.setTimeout(function(){ e("openwebrx-big-grey").style.display="none"; },1100);
|
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)
|
function toggle_panel(what, on)
|
||||||
{
|
{
|
||||||
var item=e(what);
|
var item=e(what);
|
||||||
|
if (!item) return;
|
||||||
if(typeof on !== "undefined")
|
if(typeof on !== "undefined")
|
||||||
{
|
{
|
||||||
if(item.openwebrxHidden && !on) return;
|
if(item.openwebrxHidden && !on) return;
|
||||||
@ -2424,7 +2515,7 @@ function place_panels(function_apply)
|
|||||||
for(i=0;i<plist.length;i++)
|
for(i=0;i<plist.length;i++)
|
||||||
{
|
{
|
||||||
c=plist[i];
|
c=plist[i];
|
||||||
if(c.className=="openwebrx-panel")
|
if(c.className.indexOf("openwebrx-panel") >= 0)
|
||||||
{
|
{
|
||||||
if(c.openwebrxHidden)
|
if(c.openwebrxHidden)
|
||||||
{
|
{
|
||||||
@ -2491,29 +2582,27 @@ function progressbar_set(obj,val,text,over)
|
|||||||
function demodulator_buttons_update()
|
function demodulator_buttons_update()
|
||||||
{
|
{
|
||||||
$(".openwebrx-demodulator-button").removeClass("highlighted");
|
$(".openwebrx-demodulator-button").removeClass("highlighted");
|
||||||
if(secondary_demod) $("#openwebrx-button-dig").addClass("highlighted");
|
if(secondary_demod) {
|
||||||
else switch(demodulators[0].subtype)
|
$("#openwebrx-button-dig").addClass("highlighted");
|
||||||
{
|
} else switch(demodulators[0].subtype) {
|
||||||
case "nfm":
|
case "lsb":
|
||||||
$("#openwebrx-button-nfm").addClass("highlighted");
|
case "usb":
|
||||||
break;
|
case "cw":
|
||||||
case "am":
|
if(demodulators[0].high_cut-demodulators[0].low_cut<300)
|
||||||
$("#openwebrx-button-am").addClass("highlighted");
|
$("#openwebrx-button-cw").addClass("highlighted");
|
||||||
break;
|
else
|
||||||
case "lsb":
|
{
|
||||||
case "usb":
|
if(demodulators[0].high_cut<0)
|
||||||
case "cw":
|
$("#openwebrx-button-lsb").addClass("highlighted");
|
||||||
if(demodulators[0].high_cut-demodulators[0].low_cut<300)
|
else if(demodulators[0].low_cut>0)
|
||||||
$("#openwebrx-button-cw").addClass("highlighted");
|
$("#openwebrx-button-usb").addClass("highlighted");
|
||||||
else
|
else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted");
|
||||||
{
|
}
|
||||||
if(demodulators[0].high_cut<0)
|
break;
|
||||||
$("#openwebrx-button-lsb").addClass("highlighted");
|
default:
|
||||||
else if(demodulators[0].low_cut>0)
|
var mod = demodulators[0].subtype;
|
||||||
$("#openwebrx-button-usb").addClass("highlighted");
|
$("#openwebrx-button-" + mod).addClass("highlighted");
|
||||||
else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted");
|
break;
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function demodulator_analog_replace_last() { demodulator_analog_replace(last_analog_demodulator_subtype); }
|
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)
|
function secondary_demod_start(subtype)
|
||||||
{
|
{
|
||||||
secondary_demod_canvases_initialized = false;
|
secondary_demod_canvases_initialized = false;
|
||||||
ws.send("SET secondary_mod="+subtype);
|
ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":subtype}}));
|
||||||
secondary_demod = subtype;
|
secondary_demod = subtype;
|
||||||
}
|
}
|
||||||
|
|
||||||
function secondary_demod_set()
|
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()
|
function secondary_demod_stop()
|
||||||
{
|
{
|
||||||
ws.send("SET secondary_mod=off");
|
ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":false}}));
|
||||||
secondary_demod = false;
|
secondary_demod = false;
|
||||||
secondary_demod_waterfall_queue = [];
|
secondary_demod_waterfall_queue = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2755,7 +2844,7 @@ function secondary_demod_update_channel_freq_from_event(evt)
|
|||||||
{
|
{
|
||||||
secondary_demod_waiting_for_set = true;
|
secondary_demod_waiting_for_set = true;
|
||||||
window.setTimeout(()=>{
|
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);
|
//console.log("doneset:", secondary_demod_channel_freq);
|
||||||
secondary_demod_waiting_for_set = false;
|
secondary_demod_waiting_for_set = false;
|
||||||
}, 50);
|
}, 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_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();
|
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 }}));
|
||||||
|
}
|
||||||
|
773
openwebrx.py
Executable file → Normal file
773
openwebrx.py
Executable file → Normal file
@ -1,757 +1,52 @@
|
|||||||
#!/usr/bin/python2
|
from http.server import HTTPServer
|
||||||
print "" # python2.7 is required to run OpenWebRX instead of python3. Please run me by: python2 openwebrx.py
|
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,
|
import logging
|
||||||
an open-source SDR receiver software with a web UI.
|
logging.basicConfig(level = logging.DEBUG, format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
|
||||||
|
|
||||||
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,
|
class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
|
||||||
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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
"""
|
|
||||||
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):
|
|
||||||
pass
|
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():
|
def main():
|
||||||
global clients, clients_mutex, pypy, lock_try_time, avatar_ctime, cfg, logs
|
print("""
|
||||||
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 <randras@sdr.hu>"
|
|
||||||
print
|
|
||||||
|
|
||||||
no_arguments=len(sys.argv)==1
|
OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package
|
||||||
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
|
|
||||||
|
|
||||||
#Open log files
|
Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>
|
||||||
logs = type("logs_class", (object,), {"access_log":open(cfg.access_log if cfg.access_log else "/dev/null","a"), "error_log":""})()
|
|
||||||
|
|
||||||
#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
|
pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
|
||||||
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?)"
|
|
||||||
|
|
||||||
#Change process name to "openwebrx" (to be seen in ps)
|
featureDetector = FeatureDetector()
|
||||||
try:
|
if not featureDetector.is_available("core"):
|
||||||
for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]:
|
print("you are missing required dependencies to run openwebrx. "
|
||||||
if os.path.exists(libcpath):
|
"please check that the following core requirements are installed:")
|
||||||
libc = dl.open(libcpath)
|
print(", ".join(featureDetector.get_requirements("core")))
|
||||||
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"
|
|
||||||
return
|
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
|
# Get error messages about unknown / unavailable features as soon as possible
|
||||||
clients=[]
|
SdrService.loadProps()
|
||||||
clients_mutex=threading.Lock()
|
|
||||||
lock_try_time=0
|
|
||||||
|
|
||||||
#Start watchdog thread
|
if "sdrhu_key" in pm and pm["sdrhu_public_listing"]:
|
||||||
print "[openwebrx-main] Starting watchdog threads."
|
updater = SdrHuUpdater()
|
||||||
mutex_test_thread=threading.Thread(target = mutex_test_thread_function, args = ())
|
updater.start()
|
||||||
mutex_test_thread.start()
|
|
||||||
mutex_watchdog_thread=threading.Thread(target = mutex_watchdog_thread_function, args = ())
|
server = ThreadedHttpServer(('0.0.0.0', pm.getPropertyValue("web_port")), RequestHandler)
|
||||||
mutex_watchdog_thread.start()
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
#Start spectrum thread
|
if __name__ == "__main__":
|
||||||
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")
|
|
||||||
try:
|
try:
|
||||||
clients[i].dsp.stop()
|
main()
|
||||||
except:
|
except KeyboardInterrupt:
|
||||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
for c in ClientRegistry.getSharedInstance().clients:
|
||||||
print "[openwebrx] close_client dsp.stop() :: error -",exc_type,exc_value
|
c.close()
|
||||||
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("<html><body><h1>Object moved</h1>Please <a href=\"/{0}\">click here</a> to continue.</body></html>".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()
|
|
||||||
|
129
owrx/config.py
Normal file
129
owrx/config.py
Normal file
@ -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
|
191
owrx/connection.py
Normal file
191
owrx/connection.py
Normal file
@ -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()
|
87
owrx/controllers.py
Normal file
87
owrx/controllers.py
Normal file
@ -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()
|
122
owrx/feature.py
Normal file
122
owrx/feature.py
Normal file
@ -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")
|
42
owrx/http.py
Normal file
42
owrx/http.py
Normal file
@ -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.")
|
81
owrx/meta.py
Normal file
81
owrx/meta.py
Normal file
@ -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)
|
||||||
|
|
36
owrx/sdrhu.py
Normal file
36
owrx/sdrhu.py
Normal file
@ -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)
|
552
owrx/source.py
Normal file
552
owrx/source.py
Normal file
@ -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()
|
1
owrx/version.py
Normal file
1
owrx/version.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
openwebrx_version = "v0.18"
|
97
owrx/websocket.py
Normal file
97
owrx/websocket.py
Normal file
@ -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
|
171
rxws.py
171
rxws.py
@ -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 <randras@sdr.hu>
|
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
32
sdrhu.py
32
sdrhu.py
@ -20,31 +20,13 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import config_webrx as cfg, time, subprocess
|
from owrx.sdrhu import SdrHuUpdater
|
||||||
|
from owrx.config import PropertyManager
|
||||||
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)
|
|
||||||
|
|
||||||
if __name__=="__main__":
|
if __name__=="__main__":
|
||||||
run(False)
|
pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
|
||||||
|
|
||||||
|
if not "sdrhu_key" in pm:
|
||||||
|
exit(1)
|
||||||
|
SdrHuUpdater().update()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user