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 ====
|
||||
web_port=8073
|
||||
server_hostname="localhost" # If this contains an incorrect value, the web UI may freeze on load (it can't open websocket)
|
||||
max_clients=20
|
||||
|
||||
# ==== Web GUI configuration ====
|
||||
@ -65,25 +64,24 @@ Website: <a href="http://localhost" target="_blank">http://localhost</a>
|
||||
sdrhu_key = ""
|
||||
# 3. Set this setting to True to enable listing:
|
||||
sdrhu_public_listing = False
|
||||
server_hostname="localhost"
|
||||
|
||||
# ==== DSP/RX settings ====
|
||||
fft_fps=9
|
||||
fft_size=4096 #Should be power of 2
|
||||
fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
|
||||
|
||||
# samp_rate = 250000
|
||||
samp_rate = 2400000
|
||||
center_freq = 144250000
|
||||
rf_gain = 5 #in dB. For an RTL-SDR, rf_gain=0 will set the tuner to auto gain mode, else it will be in manual gain mode.
|
||||
ppm = 0
|
||||
|
||||
audio_compression="adpcm" #valid values: "adpcm", "none"
|
||||
fft_compression="adpcm" #valid values: "adpcm", "none"
|
||||
|
||||
digimodes_enable=True #Decoding digimodes come with higher CPU usage.
|
||||
digimodes_fft_size=1024
|
||||
|
||||
start_rtl_thread=True
|
||||
# determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes
|
||||
# if you're running on a Raspi (up to 3B+) you'll want to leave this on 1
|
||||
digital_voice_unvoiced_quality = 1
|
||||
# enables lookup of DMR ids using the radioid api
|
||||
digital_voice_dmr_id_lookup = True
|
||||
|
||||
"""
|
||||
Note: if you experience audio underruns while CPU usage is 100%, you can:
|
||||
@ -101,81 +99,107 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
|
||||
# Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support #
|
||||
#################################################################################################
|
||||
|
||||
# You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... Some examples of configuration are available here (default is RTL-SDR):
|
||||
# Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf", "airspy"
|
||||
|
||||
# >> RTL-SDR via rtl_sdr
|
||||
start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
|
||||
format_conversion="csdr convert_u8_f"
|
||||
|
||||
#lna_gain=8
|
||||
#rf_amp=1
|
||||
#start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm, rf_amp=rf_amp, lna_gain=lna_gain)
|
||||
#format_conversion="csdr convert_s8_f"
|
||||
"""
|
||||
To use a HackRF, compile the HackRF host tools from its "stdout" branch:
|
||||
git clone https://github.com/mossmann/hackrf/
|
||||
cd hackrf
|
||||
git fetch
|
||||
git checkout origin/stdout
|
||||
cd host
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DINSTALL_UDEV_RULES=ON
|
||||
make
|
||||
sudo make install
|
||||
"""
|
||||
|
||||
# >> Sound card SDR (needs ALSA)
|
||||
# I did not have the chance to properly test it.
|
||||
#samp_rate = 96000
|
||||
#start_rtl_command="arecord -f S16_LE -r {samp_rate} -c2 -".format(samp_rate=samp_rate)
|
||||
#format_conversion="csdr convert_s16_f | csdr gain_ff 30"
|
||||
|
||||
# >> /dev/urandom test signal source
|
||||
# samp_rate = 2400000
|
||||
# start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate)
|
||||
# format_conversion="csdr convert_u8_f"
|
||||
|
||||
# >> Pre-recorded raw I/Q file as signal source
|
||||
# You will have to correctly specify: samp_rate, center_freq, format_conversion in order to correctly play an I/Q file.
|
||||
#start_rtl_command="(while true; do cat my_iq_file.raw; done) | csdr flowcontrol {sr} 20 ".format(sr=samp_rate*2*1.05)
|
||||
#format_conversion="csdr convert_u8_f"
|
||||
|
||||
#>> The rx_sdr command works with a variety of SDR harware: RTL-SDR, HackRF, SDRplay, UHD, Airspy, Red Pitaya, audio devices, etc.
|
||||
# It will auto-detect your SDR hardware if the following tools are installed:
|
||||
# * the vendor provided driver and library,
|
||||
# * the vendor-specific SoapySDR wrapper library,
|
||||
# * and SoapySDR itself.
|
||||
# Check out this article on the OpenWebRX Wiki: https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX/
|
||||
#start_rtl_command="rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
|
||||
#format_conversion=""
|
||||
|
||||
# >> gr-osmosdr signal source using GNU Radio (follow this guide: https://github.com/simonyiszk/openwebrx/wiki/Using-GrOsmoSDR-as-signal-source)
|
||||
#start_rtl_command="cat /tmp/osmocom_fifo"
|
||||
#format_conversion=""
|
||||
sdrs = {
|
||||
"rtlsdr": {
|
||||
"name": "RTL-SDR USB Stick",
|
||||
"type": "rtl_sdr",
|
||||
"ppm": 0,
|
||||
# you can change this if you use an upconverter. formula is:
|
||||
# shown_center_freq = center_freq + lfo_offset
|
||||
# "lfo_offset": 0,
|
||||
"profiles": {
|
||||
"70cm": {
|
||||
"name": "70cm Relais",
|
||||
"center_freq": 438800000,
|
||||
"rf_gain": 30,
|
||||
"samp_rate": 2400000,
|
||||
"start_freq": 439275000,
|
||||
"start_mod": "nfm"
|
||||
},
|
||||
"2m": {
|
||||
"name": "2m komplett",
|
||||
"center_freq": 145000000,
|
||||
"rf_gain": 30,
|
||||
"samp_rate": 2400000,
|
||||
"start_freq": 145725000,
|
||||
"start_mod": "nfm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sdrplay": {
|
||||
"name": "SDRPlay RSP2",
|
||||
"type": "sdrplay",
|
||||
"ppm": 0,
|
||||
"profiles": {
|
||||
"20m": {
|
||||
"name":"20m",
|
||||
"center_freq": 14150000,
|
||||
"rf_gain": 4,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 14070000,
|
||||
"start_mod": "usb",
|
||||
"antenna": "Antenna A"
|
||||
},
|
||||
"30m": {
|
||||
"name":"30m",
|
||||
"center_freq": 10125000,
|
||||
"rf_gain": 4,
|
||||
"samp_rate": 250000,
|
||||
"start_freq": 10142000,
|
||||
"start_mod": "usb"
|
||||
},
|
||||
"40m": {
|
||||
"name":"40m",
|
||||
"center_freq": 7100000,
|
||||
"rf_gain": 4,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 7070000,
|
||||
"start_mod": "usb",
|
||||
"antenna": "Antenna A"
|
||||
},
|
||||
"80m": {
|
||||
"name":"80m",
|
||||
"center_freq": 3650000,
|
||||
"rf_gain": 4,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 3570000,
|
||||
"start_mod": "usb",
|
||||
"antenna": "Antenna A"
|
||||
},
|
||||
"49m": {
|
||||
"name": "49m Broadcast",
|
||||
"center_freq": 6000000,
|
||||
"rf_gain": 4,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 6070000,
|
||||
"start_mod": "am",
|
||||
"antenna": "Antenna A"
|
||||
}
|
||||
}
|
||||
},
|
||||
# this one is just here to test feature detection
|
||||
"test": {
|
||||
"type": "test"
|
||||
}
|
||||
}
|
||||
|
||||
# ==== Misc settings ====
|
||||
|
||||
shown_center_freq = center_freq #you can change this if you use an upconverter
|
||||
|
||||
client_audio_buffer_size = 5
|
||||
#increasing client_audio_buffer_size will:
|
||||
# - also increase the latency
|
||||
# - decrease the chance of audio underruns
|
||||
|
||||
start_freq = center_freq
|
||||
start_mod = "nfm" #nfm, am, lsb, usb, cw
|
||||
|
||||
iq_server_port = 4951 #TCP port for ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
|
||||
|
||||
#access_log = "~/openwebrx_access.log"
|
||||
iq_port_range = [4950, 4960] #TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
|
||||
|
||||
# ==== Color themes ====
|
||||
|
||||
#A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels
|
||||
|
||||
### default theme by teejez:
|
||||
waterfall_colors = "[0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]"
|
||||
waterfall_colors = [0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]
|
||||
waterfall_min_level = -88 #in dB
|
||||
waterfall_max_level = -20
|
||||
waterfall_auto_level_margin = (5, 40)
|
||||
@ -197,7 +221,7 @@ waterfall_auto_level_margin = (5, 40)
|
||||
# 3D view settings
|
||||
mathbox_waterfall_frequency_resolution = 128 #bins
|
||||
mathbox_waterfall_history_length = 10 #seconds
|
||||
mathbox_waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
|
||||
mathbox_waterfall_colors = [0x000000ff,0x2e6893ff,0x69a5d0ff,0x214b69ff,0x9dc4e0ff,0xfff775ff,0xff8a8aff,0xb20000ff]
|
||||
|
||||
# === Experimental settings ===
|
||||
#Warning! The settings below are very experimental.
|
||||
@ -206,11 +230,3 @@ csdr_print_bufsizes = False # This prints the buffer sizes used for csdr proces
|
||||
csdr_through = False # Setting this True will print out how much data is going into the DSP chains.
|
||||
|
||||
nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux.
|
||||
|
||||
#Look up external IP address automatically from icanhazip.com, and use it as [server_hostname]
|
||||
"""
|
||||
print "[openwebrx-config] Detecting external IP address..."
|
||||
import urllib2
|
||||
server_hostname=urllib2.urlopen("http://icanhazip.com").read()[:-1]
|
||||
print "[openwebrx-config] External IP address detected:", server_hostname
|
||||
"""
|
||||
|
431
csdr.py
431
csdr.py
@ -23,13 +23,22 @@ OpenWebRX csdr plugin: do the signal processing with csdr
|
||||
import subprocess
|
||||
import time
|
||||
import os
|
||||
import code
|
||||
import signal
|
||||
import fcntl
|
||||
import threading
|
||||
from functools import partial
|
||||
|
||||
class dsp:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def __init__(self):
|
||||
class output(object):
|
||||
def add_output(self, type, read_fn):
|
||||
pass
|
||||
def reset(self):
|
||||
pass
|
||||
|
||||
class dsp(object):
|
||||
|
||||
def __init__(self, output):
|
||||
self.samp_rate = 250000
|
||||
self.output_rate = 11025 #this is default, and cannot be set at the moment
|
||||
self.fft_size = 1024
|
||||
@ -45,7 +54,6 @@ class dsp:
|
||||
self.fft_compression = "none"
|
||||
self.demodulator = "nfm"
|
||||
self.name = "csdr"
|
||||
self.format_conversion = "csdr convert_u8_f"
|
||||
self.base_bufsize = 512
|
||||
self.nc_port = 4951
|
||||
self.csdr_dynamic_bufsize = False
|
||||
@ -59,62 +67,72 @@ class dsp:
|
||||
self.secondary_fft_size = 1024
|
||||
self.secondary_process_fft = None
|
||||
self.secondary_process_demod = None
|
||||
self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", "iqtee2_pipe"]
|
||||
self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe",
|
||||
"iqtee2_pipe", "dmr_control_pipe"]
|
||||
self.secondary_pipe_names=["secondary_shift_pipe"]
|
||||
self.secondary_offset_freq = 1000
|
||||
self.unvoiced_quality = 1
|
||||
self.modification_lock = threading.Lock()
|
||||
self.output = output
|
||||
|
||||
def chain(self,which):
|
||||
if which in [ "dmr", "dstar", "nxdn", "ysf" ]:
|
||||
self.set_output_rate(48000)
|
||||
else:
|
||||
self.set_output_rate(11025)
|
||||
any_chain_base="nc -v 127.0.0.1 {nc_port} | "
|
||||
if self.csdr_dynamic_bufsize: any_chain_base+="csdr setbuf {start_bufsize} | "
|
||||
if self.csdr_through: any_chain_base+="csdr through | "
|
||||
any_chain_base+=self.format_conversion+(" | " if self.format_conversion!="" else "") ##"csdr flowcontrol {flowcontrol} auto 1.5 10 | "
|
||||
chain ="nc -v 127.0.0.1 {nc_port} | "
|
||||
if self.csdr_dynamic_bufsize: chain += "csdr setbuf {start_bufsize} | "
|
||||
if self.csdr_through: chain +="csdr through | "
|
||||
if which == "fft":
|
||||
fft_chain_base = any_chain_base+"csdr fft_cc {fft_size} {fft_block_size} | " + \
|
||||
chain += "csdr fft_cc {fft_size} {fft_block_size} | " + \
|
||||
("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \
|
||||
"csdr fft_exchange_sides_ff {fft_size}"
|
||||
if self.fft_compression=="adpcm":
|
||||
return fft_chain_base+" | csdr compress_fft_adpcm_f_u8 {fft_size}"
|
||||
else:
|
||||
return fft_chain_base
|
||||
chain_begin=any_chain_base+"csdr shift_addition_cc --fifo {shift_pipe} | csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | "
|
||||
chain += " | csdr compress_fft_adpcm_f_u8 {fft_size}"
|
||||
return chain
|
||||
chain += "csdr shift_addition_cc --fifo {shift_pipe} | "
|
||||
chain += "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | "
|
||||
chain += "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every} | "
|
||||
if self.secondary_demodulator:
|
||||
chain_begin+="csdr tee {iqtee_pipe} | "
|
||||
chain_begin+="csdr tee {iqtee2_pipe} | "
|
||||
chain_end = ""
|
||||
chain += "csdr tee {iqtee_pipe} | "
|
||||
chain += "csdr tee {iqtee2_pipe} | "
|
||||
# safe some cpu cycles... no need to decimate if decimation factor is 1
|
||||
last_decimation_block = "csdr fractional_decimator_ff {last_decimation} | " if self.last_decimation != 1.0 else ""
|
||||
if which == "nfm":
|
||||
chain += "csdr fmdemod_quadri_cf | csdr limit_ff | "
|
||||
chain += last_decimation_block
|
||||
chain += "csdr deemphasis_nfm_ff {output_rate} | csdr convert_f_s16"
|
||||
elif self.isDigitalVoice(which):
|
||||
chain += "csdr fmdemod_quadri_cf | dc_block | "
|
||||
chain += last_decimation_block
|
||||
# dsd modes
|
||||
if which in [ "dstar", "nxdn" ]:
|
||||
chain += "csdr limit_ff | csdr convert_f_s16 | "
|
||||
if which == "dstar":
|
||||
chain += "dsd -fd"
|
||||
elif which == "nxdn":
|
||||
chain += "dsd -fi"
|
||||
chain += " -i - -o - -u {unvoiced_quality} -g -1 | CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f | "
|
||||
max_gain = 5
|
||||
# digiham modes
|
||||
else:
|
||||
chain += "rrc_filter | gfsk_demodulator | "
|
||||
if which == "dmr":
|
||||
chain += "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | "
|
||||
elif which == "ysf":
|
||||
chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | "
|
||||
max_gain = 0.0005
|
||||
chain += "digitalvoice_filter -f | "
|
||||
chain += "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain} | ".format(max_gain=max_gain)
|
||||
chain += "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - "
|
||||
elif which == "am":
|
||||
chain += "csdr amdemod_cf | csdr fastdcblock_ff | "
|
||||
chain += last_decimation_block
|
||||
chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16"
|
||||
elif which == "ssb":
|
||||
chain += "csdr realpart_cf | "
|
||||
chain += last_decimation_block
|
||||
chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16"
|
||||
|
||||
if self.audio_compression=="adpcm":
|
||||
chain_end = " | csdr encode_ima_adpcm_i16_u8"
|
||||
if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr convert_f_s16"+chain_end
|
||||
if which in [ "dstar", "nxdn" ]:
|
||||
c = chain_begin
|
||||
c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16"
|
||||
if which == "dstar":
|
||||
c += " | dsd -fd"
|
||||
elif which == "nxdn":
|
||||
c += " | dsd -fi"
|
||||
c += " -i - -o - -u 2 -g 10"
|
||||
c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --input-buffer 160 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 220"
|
||||
c += chain_end
|
||||
return c
|
||||
elif which == "dmr":
|
||||
c = chain_begin
|
||||
c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16"
|
||||
c += " | rrc_filter | gfsk_demodulator | dmr_decoder --fifo {meta_pipe} | mbe_synthesizer"
|
||||
c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256"
|
||||
c += chain_end
|
||||
return c
|
||||
elif which == "ysf":
|
||||
c = chain_begin
|
||||
c += "csdr fmdemod_quadri_cf | csdr fastdcblock_ff | csdr convert_f_s16"
|
||||
c += " | rrc_filter | gfsk_demodulator | ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y"
|
||||
c += " | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r 11025 -e signed-integer -b 16 -c 1 - | csdr setbuf 256"
|
||||
c += chain_end
|
||||
return c
|
||||
elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end
|
||||
elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end
|
||||
chain += " | csdr encode_ima_adpcm_i16_u8"
|
||||
return chain
|
||||
|
||||
def secondary_chain(self, which):
|
||||
secondary_chain_base="cat {input_pipe} | "
|
||||
@ -122,14 +140,17 @@ class dsp:
|
||||
return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "")
|
||||
elif which == "bpsk31":
|
||||
return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \
|
||||
"csdr bandpass_fir_fft_cc $(csdr '=-(31.25)/{if_samp_rate}') $(csdr '=(31.25)/{if_samp_rate}') $(csdr '=31.25/{if_samp_rate}') | " + \
|
||||
"csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " + \
|
||||
"csdr simple_agc_cc 0.001 0.5 | " + \
|
||||
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \
|
||||
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \
|
||||
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8"
|
||||
|
||||
def set_secondary_demodulator(self, what):
|
||||
if self.get_secondary_demodulator() == what:
|
||||
return
|
||||
self.secondary_demodulator = what
|
||||
self.restart()
|
||||
|
||||
def secondary_fft_block_size(self):
|
||||
return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here
|
||||
@ -139,12 +160,12 @@ class dsp:
|
||||
|
||||
def secondary_bpf_cutoff(self):
|
||||
if self.secondary_demodulator == "bpsk31":
|
||||
return (31.25/2) / self.if_samp_rate()
|
||||
return 31.25 / self.if_samp_rate()
|
||||
return 0
|
||||
|
||||
def secondary_bpf_transition_bw(self):
|
||||
if self.secondary_demodulator == "bpsk31":
|
||||
return (31.25/2) / self.if_samp_rate()
|
||||
return 31.25 / self.if_samp_rate()
|
||||
return 0
|
||||
|
||||
def secondary_samples_per_bits(self):
|
||||
@ -157,51 +178,46 @@ class dsp:
|
||||
return 31.25
|
||||
|
||||
def start_secondary_demodulator(self):
|
||||
if(not self.secondary_demodulator): return
|
||||
print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate()
|
||||
if not self.secondary_demodulator: return
|
||||
logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate())
|
||||
secondary_command_fft=self.secondary_chain("fft")
|
||||
secondary_command_demod=self.secondary_chain(self.secondary_demodulator)
|
||||
self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft)
|
||||
|
||||
secondary_command_fft=secondary_command_fft.format( \
|
||||
input_pipe=self.iqtee_pipe, \
|
||||
secondary_fft_input_size=self.secondary_fft_size, \
|
||||
secondary_fft_size=self.secondary_fft_size, \
|
||||
secondary_fft_block_size=self.secondary_fft_block_size(), \
|
||||
secondary_command_fft=secondary_command_fft.format(
|
||||
input_pipe=self.iqtee_pipe,
|
||||
secondary_fft_input_size=self.secondary_fft_size,
|
||||
secondary_fft_size=self.secondary_fft_size,
|
||||
secondary_fft_block_size=self.secondary_fft_block_size(),
|
||||
)
|
||||
secondary_command_demod=secondary_command_demod.format( \
|
||||
input_pipe=self.iqtee2_pipe, \
|
||||
secondary_shift_pipe=self.secondary_shift_pipe, \
|
||||
secondary_decimation=self.secondary_decimation(), \
|
||||
secondary_samples_per_bits=self.secondary_samples_per_bits(), \
|
||||
secondary_bpf_cutoff=self.secondary_bpf_cutoff(), \
|
||||
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), \
|
||||
secondary_command_demod=secondary_command_demod.format(
|
||||
input_pipe=self.iqtee2_pipe,
|
||||
secondary_shift_pipe=self.secondary_shift_pipe,
|
||||
secondary_decimation=self.secondary_decimation(),
|
||||
secondary_samples_per_bits=self.secondary_samples_per_bits(),
|
||||
secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
|
||||
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(),
|
||||
if_samp_rate=self.if_samp_rate()
|
||||
)
|
||||
|
||||
print "[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft
|
||||
print "[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod
|
||||
#code.interact(local=locals())
|
||||
logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft)
|
||||
logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (demod) = %s", secondary_command_demod)
|
||||
my_env=os.environ.copy()
|
||||
#if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
|
||||
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
|
||||
self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
|
||||
print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)"
|
||||
logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)")
|
||||
self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes
|
||||
print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)" #TODO digimodes
|
||||
logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") #TODO digimodes
|
||||
self.secondary_processes_running = True
|
||||
|
||||
#open control pipes for csdr and send initialization data
|
||||
# print "==========> 1"
|
||||
if self.secondary_shift_pipe != None: #TODO digimodes
|
||||
# print "==========> 2", self.secondary_shift_pipe
|
||||
self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes
|
||||
# print "==========> 3"
|
||||
self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes
|
||||
# print "==========> 4"
|
||||
self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())))
|
||||
self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
|
||||
|
||||
self.set_pipe_nonblocking(self.secondary_process_demod.stdout)
|
||||
self.set_pipe_nonblocking(self.secondary_process_fft.stdout)
|
||||
#open control pipes for csdr and send initialization data
|
||||
if self.secondary_shift_pipe != None: #TODO digimodes
|
||||
self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes
|
||||
self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes
|
||||
|
||||
def set_secondary_offset_freq(self, value):
|
||||
self.secondary_offset_freq=value
|
||||
@ -212,16 +228,20 @@ class dsp:
|
||||
def stop_secondary_demodulator(self):
|
||||
if self.secondary_processes_running == False: return
|
||||
self.try_delete_pipes(self.secondary_pipe_names)
|
||||
if self.secondary_process_fft: os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
|
||||
if self.secondary_process_demod: os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
|
||||
if self.secondary_process_fft:
|
||||
try:
|
||||
os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
# been killed by something else, ignore
|
||||
pass
|
||||
if self.secondary_process_demod:
|
||||
try:
|
||||
os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
# been killed by something else, ignore
|
||||
pass
|
||||
self.secondary_processes_running = False
|
||||
|
||||
def read_secondary_demod(self, size):
|
||||
return self.secondary_process_demod.stdout.read(size)
|
||||
|
||||
def read_secondary_fft(self, size):
|
||||
return self.secondary_process_fft.stdout.read(size)
|
||||
|
||||
def get_secondary_demodulator(self):
|
||||
return self.secondary_demodulator
|
||||
|
||||
@ -244,12 +264,20 @@ class dsp:
|
||||
if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2)
|
||||
|
||||
def set_samp_rate(self,samp_rate):
|
||||
#to change this, restart is required
|
||||
self.samp_rate=samp_rate
|
||||
self.decimation=1
|
||||
while self.samp_rate/(self.decimation+1)>=self.output_rate:
|
||||
self.decimation+=1
|
||||
self.last_decimation=float(self.if_samp_rate())/self.output_rate
|
||||
self.calculate_decimation()
|
||||
if self.running: self.restart()
|
||||
|
||||
def calculate_decimation(self):
|
||||
(self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate())
|
||||
|
||||
def get_decimation(self, input_rate, output_rate):
|
||||
decimation=1
|
||||
while input_rate / (decimation+1) >= output_rate:
|
||||
decimation += 1
|
||||
fraction = float(input_rate / decimation) / output_rate
|
||||
intermediate_rate = input_rate / decimation
|
||||
return (decimation, fraction, intermediate_rate)
|
||||
|
||||
def if_samp_rate(self):
|
||||
return self.samp_rate/self.decimation
|
||||
@ -260,66 +288,86 @@ class dsp:
|
||||
def get_output_rate(self):
|
||||
return self.output_rate
|
||||
|
||||
def get_audio_rate(self):
|
||||
if self.isDigitalVoice():
|
||||
return 48000
|
||||
return self.get_output_rate()
|
||||
|
||||
def isDigitalVoice(self, demodulator = None):
|
||||
if demodulator is None:
|
||||
demodulator = self.get_demodulator()
|
||||
return demodulator in ["dmr", "dstar", "nxdn", "ysf"]
|
||||
|
||||
def set_output_rate(self,output_rate):
|
||||
self.output_rate=output_rate
|
||||
self.set_samp_rate(self.samp_rate) #as it depends on output_rate
|
||||
self.calculate_decimation()
|
||||
|
||||
def set_demodulator(self,demodulator):
|
||||
#to change this, restart is required
|
||||
if (self.demodulator == demodulator): return
|
||||
self.demodulator=demodulator
|
||||
self.calculate_decimation()
|
||||
self.restart()
|
||||
|
||||
def get_demodulator(self):
|
||||
return self.demodulator
|
||||
|
||||
def set_fft_size(self,fft_size):
|
||||
#to change this, restart is required
|
||||
self.fft_size=fft_size
|
||||
self.restart()
|
||||
|
||||
def set_fft_fps(self,fft_fps):
|
||||
#to change this, restart is required
|
||||
self.fft_fps=fft_fps
|
||||
self.restart()
|
||||
|
||||
def set_fft_averages(self,fft_averages):
|
||||
#to change this, restart is required
|
||||
self.fft_averages=fft_averages
|
||||
self.restart()
|
||||
|
||||
def fft_block_size(self):
|
||||
if self.fft_averages == 0: return self.samp_rate/self.fft_fps
|
||||
else: return self.samp_rate/self.fft_fps/self.fft_averages
|
||||
|
||||
def set_format_conversion(self,format_conversion):
|
||||
self.format_conversion=format_conversion
|
||||
|
||||
def set_offset_freq(self,offset_freq):
|
||||
self.offset_freq=offset_freq
|
||||
if self.running:
|
||||
self.modification_lock.acquire()
|
||||
self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate))
|
||||
self.shift_pipe_file.flush()
|
||||
self.modification_lock.release()
|
||||
|
||||
def set_bpf(self,low_cut,high_cut):
|
||||
self.low_cut=low_cut
|
||||
self.high_cut=high_cut
|
||||
if self.running:
|
||||
self.modification_lock.acquire()
|
||||
self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) )
|
||||
self.bpf_pipe_file.flush()
|
||||
self.modification_lock.release()
|
||||
|
||||
def get_bpf(self):
|
||||
return [self.low_cut, self.high_cut]
|
||||
|
||||
def set_squelch_level(self, squelch_level):
|
||||
self.squelch_level=squelch_level
|
||||
#no squelch required on digital voice modes
|
||||
actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level
|
||||
if self.running:
|
||||
self.squelch_pipe_file.write( "%g\n"%(float(self.squelch_level)) )
|
||||
self.modification_lock.acquire()
|
||||
self.squelch_pipe_file.write("%g\n"%(float(actual_squelch)))
|
||||
self.squelch_pipe_file.flush()
|
||||
self.modification_lock.release()
|
||||
|
||||
def get_smeter_level(self):
|
||||
if self.running:
|
||||
line=self.smeter_pipe_file.readline()
|
||||
return float(line[:-1])
|
||||
def set_unvoiced_quality(self, q):
|
||||
self.unvoiced_quality = q
|
||||
self.restart()
|
||||
|
||||
def get_metadata(self):
|
||||
if self.running and self.meta_pipe:
|
||||
return self.meta_pipe_file.readline()
|
||||
def get_unvoiced_quality(self):
|
||||
return self.unvoiced_quality
|
||||
|
||||
def set_dmr_filter(self, filter):
|
||||
if self.dmr_control_pipe_file:
|
||||
self.dmr_control_pipe_file.write("{0}\n".format(filter))
|
||||
self.dmr_control_pipe_file.flush()
|
||||
|
||||
def mkfifo(self,path):
|
||||
try:
|
||||
@ -332,9 +380,7 @@ class dsp:
|
||||
return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate))
|
||||
|
||||
def try_create_pipes(self, pipe_names, command_base):
|
||||
# print "try_create_pipes"
|
||||
for pipe_name in pipe_names:
|
||||
# print "\t"+pipe_name
|
||||
if "{"+pipe_name+"}" in command_base:
|
||||
setattr(self, pipe_name, self.pipe_base_path+pipe_name)
|
||||
self.mkfifo(getattr(self, pipe_name))
|
||||
@ -346,122 +392,107 @@ class dsp:
|
||||
pipe_path = getattr(self,pipe_name,None)
|
||||
if pipe_path:
|
||||
try: os.unlink(pipe_path)
|
||||
except Exception as e: print "[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e
|
||||
|
||||
def set_pipe_nonblocking(self, pipe):
|
||||
flags = fcntl.fcntl(pipe, fcntl.F_GETFL)
|
||||
fcntl.fcntl(pipe, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||
except Exception:
|
||||
logger.exception("try_delete_pipes()")
|
||||
|
||||
def start(self):
|
||||
self.modification_lock.acquire()
|
||||
if (self.running):
|
||||
self.modification_lock.release()
|
||||
return
|
||||
self.running = True
|
||||
|
||||
command_base=self.chain(self.demodulator)
|
||||
|
||||
#create control pipes for csdr
|
||||
self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self))
|
||||
# self.bpf_pipe = self.shift_pipe = self.squelch_pipe = self.smeter_pipe = None
|
||||
|
||||
self.try_create_pipes(self.pipe_names, command_base)
|
||||
|
||||
# if "{bpf_pipe}" in command_base:
|
||||
# self.bpf_pipe=pipe_base_path+"bpf"
|
||||
# self.mkfifo(self.bpf_pipe)
|
||||
# if "{shift_pipe}" in command_base:
|
||||
# self.shift_pipe=pipe_base_path+"shift"
|
||||
# self.mkfifo(self.shift_pipe)
|
||||
# if "{squelch_pipe}" in command_base:
|
||||
# self.squelch_pipe=pipe_base_path+"squelch"
|
||||
# self.mkfifo(self.squelch_pipe)
|
||||
# if "{smeter_pipe}" in command_base:
|
||||
# self.smeter_pipe=pipe_base_path+"smeter"
|
||||
# self.mkfifo(self.smeter_pipe)
|
||||
# if "{iqtee_pipe}" in command_base:
|
||||
# self.iqtee_pipe=pipe_base_path+"iqtee"
|
||||
# self.mkfifo(self.iqtee_pipe)
|
||||
# if "{iqtee2_pipe}" in command_base:
|
||||
# self.iqtee2_pipe=pipe_base_path+"iqtee2"
|
||||
# self.mkfifo(self.iqtee2_pipe)
|
||||
|
||||
#run the command
|
||||
command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, \
|
||||
last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, \
|
||||
bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), \
|
||||
flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, \
|
||||
squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe )
|
||||
command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation,
|
||||
last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages,
|
||||
bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(),
|
||||
flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port,
|
||||
squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe,
|
||||
output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000),
|
||||
unvoiced_quality = self.get_unvoiced_quality(), dmr_control_pipe = self.dmr_control_pipe)
|
||||
|
||||
print "[openwebrx-dsp-plugin:csdr] Command =",command
|
||||
#code.interact(local=locals())
|
||||
logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command)
|
||||
my_env=os.environ.copy()
|
||||
if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
|
||||
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
|
||||
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
|
||||
|
||||
#set stdout to non-blocking to avoid blocking the main loop when no audio was decoded in digital modes
|
||||
self.set_pipe_nonblocking(self.process.stdout)
|
||||
def watch_thread():
|
||||
rc = self.process.wait()
|
||||
logger.debug("dsp thread ended with rc=%d", rc)
|
||||
if (rc == 0 and self.running and not self.modification_lock.locked()):
|
||||
logger.debug("restarting since rc = 0, self.running = true, and no modification")
|
||||
self.restart()
|
||||
|
||||
self.running = True
|
||||
threading.Thread(target = watch_thread).start()
|
||||
|
||||
#open control pipes for csdr and send initialization data
|
||||
if self.bpf_pipe != None:
|
||||
self.bpf_pipe_file=open(self.bpf_pipe,"w")
|
||||
self.set_bpf(self.low_cut,self.high_cut)
|
||||
if self.shift_pipe != None:
|
||||
self.shift_pipe_file=open(self.shift_pipe,"w")
|
||||
self.set_offset_freq(self.offset_freq)
|
||||
if self.squelch_pipe != None:
|
||||
self.squelch_pipe_file=open(self.squelch_pipe,"w")
|
||||
self.set_squelch_level(self.squelch_level)
|
||||
if self.smeter_pipe != None:
|
||||
self.smeter_pipe_file=open(self.smeter_pipe,"r")
|
||||
self.set_pipe_nonblocking(self.smeter_pipe_file)
|
||||
if self.meta_pipe != None:
|
||||
self.meta_pipe_file=open(self.meta_pipe,"r")
|
||||
self.set_pipe_nonblocking(self.meta_pipe_file)
|
||||
self.output.add_output("audio", partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256))
|
||||
|
||||
# open control pipes for csdr
|
||||
if self.bpf_pipe:
|
||||
self.bpf_pipe_file = open(self.bpf_pipe, "w")
|
||||
if self.shift_pipe:
|
||||
self.shift_pipe_file = open(self.shift_pipe, "w")
|
||||
if self.squelch_pipe:
|
||||
self.squelch_pipe_file = open(self.squelch_pipe, "w")
|
||||
self.start_secondary_demodulator()
|
||||
|
||||
def read(self,size):
|
||||
return self.process.stdout.read(size)
|
||||
self.modification_lock.release()
|
||||
|
||||
def read_async(self, size):
|
||||
try:
|
||||
return self.process.stdout.read(size)
|
||||
except IOError:
|
||||
return None
|
||||
# send initial config through the pipes
|
||||
if self.squelch_pipe:
|
||||
self.set_squelch_level(self.squelch_level)
|
||||
if self.shift_pipe:
|
||||
self.set_offset_freq(self.offset_freq)
|
||||
if self.bpf_pipe:
|
||||
self.set_bpf(self.low_cut, self.high_cut)
|
||||
if self.smeter_pipe:
|
||||
self.smeter_pipe_file=open(self.smeter_pipe,"r")
|
||||
def read_smeter():
|
||||
raw = self.smeter_pipe_file.readline()
|
||||
if len(raw) == 0:
|
||||
return None
|
||||
else:
|
||||
return float(raw.rstrip("\n"))
|
||||
self.output.add_output("smeter", read_smeter)
|
||||
if self.meta_pipe != None:
|
||||
# TODO make digiham output unicode and then change this here
|
||||
self.meta_pipe_file=open(self.meta_pipe, "r", encoding="cp437")
|
||||
def read_meta():
|
||||
raw = self.meta_pipe_file.readline()
|
||||
if len(raw) == 0:
|
||||
return None
|
||||
else:
|
||||
return raw.rstrip("\n")
|
||||
self.output.add_output("meta", read_meta)
|
||||
|
||||
if self.dmr_control_pipe:
|
||||
self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w")
|
||||
|
||||
def stop(self):
|
||||
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
||||
self.modification_lock.acquire()
|
||||
self.running = False
|
||||
if hasattr(self, "process"):
|
||||
try:
|
||||
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
# been killed by something else, ignore
|
||||
pass
|
||||
self.stop_secondary_demodulator()
|
||||
#if(self.process.poll()!=None):return # returns None while subprocess is running
|
||||
#while(self.process.poll()==None):
|
||||
# #self.process.kill()
|
||||
# print "killproc",os.getpgid(self.process.pid),self.process.pid
|
||||
# os.killpg(self.process.pid, signal.SIGTERM)
|
||||
#
|
||||
# time.sleep(0.1)
|
||||
|
||||
self.try_delete_pipes(self.pipe_names)
|
||||
|
||||
# if self.bpf_pipe:
|
||||
# try: os.unlink(self.bpf_pipe)
|
||||
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe
|
||||
# if self.shift_pipe:
|
||||
# try: os.unlink(self.shift_pipe)
|
||||
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.shift_pipe
|
||||
# if self.squelch_pipe:
|
||||
# try: os.unlink(self.squelch_pipe)
|
||||
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.squelch_pipe
|
||||
# if self.smeter_pipe:
|
||||
# try: os.unlink(self.smeter_pipe)
|
||||
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.smeter_pipe
|
||||
# if self.iqtee_pipe:
|
||||
# try: os.unlink(self.iqtee_pipe)
|
||||
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee_pipe
|
||||
# if self.iqtee2_pipe:
|
||||
# try: os.unlink(self.iqtee2_pipe)
|
||||
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee2_pipe
|
||||
|
||||
self.running = False
|
||||
self.modification_lock.release()
|
||||
|
||||
def restart(self):
|
||||
if not self.running: return
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
|
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>
|
||||
<head>
|
||||
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
|
||||
<script type="text/javascript">
|
||||
//Global variables
|
||||
var client_id="%[CLIENT_ID]";
|
||||
var ws_url="%[WS_URL]";
|
||||
var rx_photo_height=%[RX_PHOTO_HEIGHT];
|
||||
var audio_buffering_fill_to=%[AUDIO_BUFSIZE];
|
||||
var starting_mod="%[START_MOD]";
|
||||
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" />
|
||||
<script src="static/sdr.js"></script>
|
||||
<script src="static/mathbox-bundle.min.js"></script>
|
||||
<script src="static/openwebrx.js"></script>
|
||||
<script src="static/jquery-3.2.1.min.js"></script>
|
||||
<script src="static/jquery.nanoscroller.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="static/nanoscroller.css" />
|
||||
<link rel="stylesheet" type="text/css" href="static/openwebrx.css" />
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body onload="openwebrx_init();">
|
||||
<div id="webrx-page-container">
|
||||
<div id="webrx-top-container">
|
||||
<div id="webrx-top-photo-clip">
|
||||
<img src="gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/>
|
||||
<div id="webrx-rx-photo-title">%[RX_PHOTO_TITLE]</div>
|
||||
<div id="webrx-rx-photo-desc">%[RX_PHOTO_DESC]</div>
|
||||
</div>
|
||||
<div id="webrx-top-bar-background" class="webrx-top-bar-parts"></div>
|
||||
<div id="webrx-top-bar" class="webrx-top-bar-parts">
|
||||
<a href="https://sdr.hu/openwebrx" target="_blank"><img src="gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a>
|
||||
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a>
|
||||
<img id="webrx-rx-avatar-background" src="gfx/openwebrx-avatar-background.png" onclick="toggle_rx_photo();"/>
|
||||
<img id="webrx-rx-avatar" src="gfx/openwebrx-avatar.png" onclick="toggle_rx_photo();"/>
|
||||
<div id="webrx-rx-title" onclick="toggle_rx_photo();">%[RX_TITLE]</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">
|
||||
<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="gfx/openwebrx-rx-details-arrow.png" /></a>
|
||||
<img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/>
|
||||
<div id="webrx-top-bar" class="webrx-top-bar-parts">
|
||||
<a href="https://sdr.hu/openwebrx" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a>
|
||||
<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-rx-avatar-background">
|
||||
<img id="webrx-rx-avatar" src="static/gfx/openwebrx-avatar.png" onclick="toggle_rx_photo();"/>
|
||||
</div>
|
||||
<div id="webrx-rx-texts">
|
||||
<div id="webrx-rx-title" onclick="toggle_rx_photo();"></div>
|
||||
<div id="webrx-rx-desc" onclick="toggle_rx_photo();"></div>
|
||||
</div>
|
||||
<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-down" onclick="toggle_rx_photo();"><img src="static/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>
|
||||
<section id="openwebrx-main-buttons">
|
||||
<ul>
|
||||
<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 id="webrx-rx-photo-title"></div>
|
||||
<div id="webrx-rx-photo-desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
<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 id="webrx-actual-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-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
|
||||
onclick="demodulator_analog_replace('nfm');">FM</div>
|
||||
@ -102,12 +91,16 @@
|
||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw"
|
||||
onclick="demodulator_analog_replace('cw');">CW</div>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
@ -118,23 +111,23 @@
|
||||
</select>
|
||||
</div>
|
||||
<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()">
|
||||
<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()">
|
||||
</div>
|
||||
<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()">
|
||||
<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()">
|
||||
</div>
|
||||
<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="zoomOutOneStep();" title="Zoom out one step"> <img src="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="zoomOutTotal();" title="Zoom out totally"><img src="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="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="static/gfx/openwebrx-zoom-out.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="static/gfx/openwebrx-zoom-out-total.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>
|
||||
<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-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></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>
|
||||
<br />We're working on the code right now, so the application might fail.
|
||||
</div>
|
||||
@ -175,14 +168,41 @@
|
||||
</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 id="openwebrx-big-grey" onclick="iosPlayButtonClick();">
|
||||
<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
|
||||
</div>
|
||||
</div>
|
@ -155,22 +155,12 @@ input[type=range]:focus::-ms-fill-upper
|
||||
|
||||
.webrx-top-bar-parts
|
||||
{
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width:100%;
|
||||
height:67px;
|
||||
}
|
||||
|
||||
#webrx-top-bar-background
|
||||
{
|
||||
background-color: #808080;
|
||||
opacity: 0.15;
|
||||
filter:alpha(opacity=15);
|
||||
}
|
||||
|
||||
#webrx-top-bar
|
||||
{
|
||||
background: rgba(128, 128, 128, 0.15);
|
||||
margin:0;
|
||||
padding:0;
|
||||
user-select: none;
|
||||
@ -179,20 +169,23 @@ input[type=range]:focus::-ms-fill-upper
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#webrx-top-logo
|
||||
{
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 15px;
|
||||
padding: 12px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#webrx-ha5kfu-top-logo
|
||||
{
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
float: right;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#webrx-top-photo
|
||||
@ -204,79 +197,37 @@ input[type=range]:focus::-ms-fill-upper
|
||||
#webrx-rx-avatar-background
|
||||
{
|
||||
cursor:pointer;
|
||||
position: absolute;
|
||||
left: 285px;
|
||||
top: 6px;
|
||||
background-image: url(gfx/openwebrx-avatar-background.png);
|
||||
background-origin: content-box;
|
||||
background-repeat: no-repeat;
|
||||
float: left;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
#webrx-rx-avatar
|
||||
{
|
||||
cursor:pointer;
|
||||
position: absolute;
|
||||
left: 289px;
|
||||
top: 10px;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
#webrx-top-photo-clip
|
||||
{
|
||||
min-height: 67px;
|
||||
max-height: 350px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/*#webrx-bottom-bar
|
||||
{
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 117px;
|
||||
background-image:url(gfx/webrx-bottom-bar.png);
|
||||
}*/
|
||||
|
||||
#webrx-page-container
|
||||
{
|
||||
min-height:100%;
|
||||
position:relative;
|
||||
}
|
||||
|
||||
/*#webrx-photo-gradient-left
|
||||
{
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
background-image:url(gfx/webrx-photo-gradient-corner.png);
|
||||
width: 59px;
|
||||
height: 92px;
|
||||
|
||||
}
|
||||
|
||||
#webrx-photo-gradient-middle
|
||||
{
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 59px;
|
||||
right: 59px;
|
||||
height: 92px;
|
||||
background-image:url(gfx/webrx-photo-gradient-middle.png);
|
||||
}
|
||||
|
||||
#webrx-photo-gradient-right
|
||||
{
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
background-image:url(gfx/webrx-photo-gradient-corner.png);
|
||||
width: 59px;
|
||||
height: 92px;
|
||||
-webkit-transform:scaleX(-1);
|
||||
-moz-transform:scaleX(-1);
|
||||
-ms-transform:scaleX(-1);
|
||||
-o-transform:scaleX(-1);
|
||||
transform:scaleX(-1);
|
||||
}*/
|
||||
|
||||
#webrx-rx-photo-title
|
||||
{
|
||||
position: absolute;
|
||||
@ -303,10 +254,17 @@ input[type=range]:focus::-ms-fill-upper
|
||||
|
||||
#webrx-rx-photo-desc a
|
||||
{
|
||||
/*color: #007df1;*/
|
||||
color: #5ca8ff;
|
||||
text-shadow: none;
|
||||
/*text-shadow: 0px 0px 7px #fff;*/
|
||||
}
|
||||
|
||||
#webrx-rx-texts {
|
||||
float: left;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#webrx-rx-texts div {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#webrx-rx-title
|
||||
@ -314,9 +272,6 @@ input[type=range]:focus::-ms-fill-upper
|
||||
white-space:nowrap;
|
||||
overflow: hidden;
|
||||
cursor:pointer;
|
||||
position: absolute;
|
||||
left: 350px;
|
||||
top: 13px;
|
||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
||||
color: #909090;
|
||||
font-size: 11pt;
|
||||
@ -330,15 +285,11 @@ input[type=range]:focus::-ms-fill-upper
|
||||
cursor:pointer;
|
||||
font-size: 10pt;
|
||||
color: #909090;
|
||||
position: absolute;
|
||||
left: 350px;
|
||||
top: 34px;
|
||||
}
|
||||
|
||||
#webrx-rx-desc a
|
||||
{
|
||||
color: #909090;
|
||||
/*text-decoration: none;*/
|
||||
}
|
||||
|
||||
#openwebrx-rx-details-arrow
|
||||
@ -718,9 +669,7 @@ img.openwebrx-mirror-img
|
||||
|
||||
#openwebrx-main-buttons
|
||||
{
|
||||
position: absolute;
|
||||
right: 133px;
|
||||
top: 3px;
|
||||
float: right;
|
||||
margin:0;
|
||||
color: white;
|
||||
text-shadow: 0px 0px 4px #000000;
|
||||
@ -841,10 +790,7 @@ img.openwebrx-mirror-img
|
||||
transition: width 500ms, left 500ms;
|
||||
}
|
||||
|
||||
#openwebrx-secondary-demod-listbox
|
||||
{
|
||||
width: 201px;
|
||||
height: 27px;
|
||||
.openwebrx-panel select {
|
||||
border-radius: 5px;
|
||||
background-color: #373737;
|
||||
color: White;
|
||||
@ -856,16 +802,27 @@ img.openwebrx-mirror-img
|
||||
border-color: transparent;
|
||||
border-width: 0px;
|
||||
-moz-appearance: none;
|
||||
padding-left:3px;
|
||||
}
|
||||
|
||||
#openwebrx-secondary-demod-listbox option
|
||||
{
|
||||
.openwebrx-panel select option {
|
||||
border-width: 0px;
|
||||
background-color: #373737;
|
||||
color: White;
|
||||
}
|
||||
|
||||
#openwebrx-secondary-demod-listbox
|
||||
{
|
||||
width: 201px;
|
||||
height: 27px;
|
||||
padding-left:3px;
|
||||
}
|
||||
|
||||
#openwebrx-sdr-profiles-listbox {
|
||||
width: 100%;
|
||||
font-size: 10pt;
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
#openwebrx-cursor-blink
|
||||
{
|
||||
animation: cursor-blink 1s infinite;
|
||||
@ -971,3 +928,88 @@ img.openwebrx-mirror-img
|
||||
border-color: Red;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot {
|
||||
width: 145px;
|
||||
height: 196px;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
|
||||
background-color: #676767;
|
||||
padding: 2px 0;
|
||||
color: #333;
|
||||
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot, .openwebrx-meta-slot.muted:before {
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.muted:before {
|
||||
display: block;
|
||||
content: "";
|
||||
background-image: url("gfx/openwebrx-mute.png");
|
||||
width:100%;
|
||||
height:133px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.active {
|
||||
background-color: #95bbdf;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.sync .openwebrx-dmr-slot:before {
|
||||
content:"";
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: #ABFF00;
|
||||
border-radius: 50%;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot .openwebrx-meta-user-image {
|
||||
width:100%;
|
||||
height:133px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.active .openwebrx-meta-user-image {
|
||||
background-image: url("gfx/openwebrx-directcall.png");
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.active .openwebrx-meta-user-image.group {
|
||||
background-image: url("gfx/openwebrx-groupcall.png");
|
||||
}
|
||||
|
||||
.openwebrx-dmr-timeslot-panel * {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.openwebrx-maps-pin {
|
||||
background-image: url("gfx/google_maps_pin.svg");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ var waterfall_setup_done=0;
|
||||
var waterfall_queue = [];
|
||||
var waterfall_timer;
|
||||
var secondary_fft_size;
|
||||
var audio_allowed;
|
||||
|
||||
/*function fade(something,from,to,time_ms,fps)
|
||||
{
|
||||
@ -79,7 +80,9 @@ is_chrome = /Chrome/.test(navigator.userAgent);
|
||||
|
||||
function init_rx_photo()
|
||||
{
|
||||
e("webrx-top-photo-clip").style.maxHeight=rx_photo_height.toString()+"px";
|
||||
var clip = e("webrx-top-photo-clip");
|
||||
rx_photo_height = clip.clientHeight
|
||||
clip.style.maxHeight=rx_photo_height+"px";
|
||||
window.setTimeout(function() { animate(e("webrx-rx-photo-title"),"opacity","",1,0,1,500,30); },1000);
|
||||
window.setTimeout(function() { animate(e("webrx-rx-photo-desc"),"opacity","",1,0,1,500,30); },1500);
|
||||
window.setTimeout(function() { close_rx_photo() },2500);
|
||||
@ -133,14 +136,14 @@ function toggleMute()
|
||||
if (mute) {
|
||||
mute = false;
|
||||
e("openwebrx-mute-on").id="openwebrx-mute-off";
|
||||
e("openwebrx-mute-img").src="gfx/openwebrx-speaker.png";
|
||||
e("openwebrx-mute-img").src="static/gfx/openwebrx-speaker.png";
|
||||
e("openwebrx-panel-volume").disabled=false;
|
||||
e("openwebrx-panel-volume").style.opacity=1.0;
|
||||
e("openwebrx-panel-volume").value = volumeBeforeMute;
|
||||
} else {
|
||||
mute = true;
|
||||
e("openwebrx-mute-off").id="openwebrx-mute-on";
|
||||
e("openwebrx-mute-img").src="gfx/openwebrx-speaker-muted.png";
|
||||
e("openwebrx-mute-img").src="static/gfx/openwebrx-speaker-muted.png";
|
||||
e("openwebrx-panel-volume").disabled=true;
|
||||
e("openwebrx-panel-volume").style.opacity=0.5;
|
||||
volumeBeforeMute = e("openwebrx-panel-volume").value;
|
||||
@ -160,7 +163,7 @@ function updateSquelch()
|
||||
{
|
||||
var sliderValue=parseInt(e("openwebrx-panel-squelch").value);
|
||||
var outputValue=(sliderValue==parseInt(e("openwebrx-panel-squelch").min))?0:getLinearSmeterValue(sliderValue);
|
||||
ws.send("SET squelch_level="+outputValue.toString());
|
||||
ws.send(JSON.stringify({"type":"dspcontrol","params":{"squelch_level":outputValue}}));
|
||||
}
|
||||
|
||||
function updateWaterfallColors(which)
|
||||
@ -433,8 +436,8 @@ function demodulator_default_analog(offset_frequency,subtype)
|
||||
}
|
||||
else if(subtype=="dmr" || subtype=="ysf")
|
||||
{
|
||||
this.low_cut=-6500;
|
||||
this.high_cut=6500;
|
||||
this.low_cut=-4000;
|
||||
this.high_cut=4000;
|
||||
}
|
||||
else if(subtype=="dstar" || subtype=="nxdn")
|
||||
{
|
||||
@ -470,9 +473,13 @@ function demodulator_default_analog(offset_frequency,subtype)
|
||||
|
||||
this.doset=function(first_time)
|
||||
{ //this function sends demodulator parameters to the server
|
||||
ws.send("SET"+((first_time)?" mod="+this.server_mod:"")+
|
||||
" low_cut="+this.low_cut.toString()+" high_cut="+this.high_cut.toString()+
|
||||
" offset_freq="+this.offset_frequency.toString());
|
||||
params = {
|
||||
"low_cut": this.low_cut,
|
||||
"high_cut": this.high_cut,
|
||||
"offset_freq": this.offset_frequency
|
||||
};
|
||||
if (first_time) params.mod = this.server_mod;
|
||||
ws.send(JSON.stringify({"type":"dspcontrol","params":params}));
|
||||
}
|
||||
this.doset(true); //we set parameters on object creation
|
||||
|
||||
@ -612,6 +619,8 @@ function demodulator_analog_replace(subtype, for_digital)
|
||||
}
|
||||
demodulator_add(new demodulator_default_analog(temp_offset,subtype));
|
||||
demodulator_buttons_update();
|
||||
hide_digitalvoice_panels();
|
||||
toggle_panel("openwebrx-panel-metadata-" + subtype, true);
|
||||
}
|
||||
|
||||
function demodulator_set_offset_frequency(which,to_what)
|
||||
@ -1150,176 +1159,232 @@ function audio_calculate_resampling(targetRate)
|
||||
|
||||
debug_ws_data_received=0;
|
||||
max_clients_num=0;
|
||||
clients_num = 0;
|
||||
|
||||
var COMPRESS_FFT_PAD_N=10; //should be the same as in csdr.c
|
||||
|
||||
function on_ws_recv(evt)
|
||||
{
|
||||
if(!(evt.data instanceof ArrayBuffer)) { divlog("on_ws_recv(): Not ArrayBuffer received...",1); return; }
|
||||
//
|
||||
debug_ws_data_received+=evt.data.byteLength/1000;
|
||||
first4Chars=getFirstChars(evt.data,4);
|
||||
first3Chars=first4Chars.slice(0,3);
|
||||
if(first3Chars=="CLI")
|
||||
{
|
||||
var stringData=arrayBufferToString(evt.data);
|
||||
if(stringData.substring(0,16)=="CLIENT DE SERVER") divlog("Server acknowledged WebSocket connection.");
|
||||
if (typeof evt.data == 'string') {
|
||||
// text messages
|
||||
debug_ws_data_received += evt.data.length / 1000;
|
||||
|
||||
}
|
||||
if(first3Chars=="AUD")
|
||||
{
|
||||
var audio_data;
|
||||
if(audio_compression=="adpcm") audio_data=new Uint8Array(evt.data,4)
|
||||
else audio_data=new Int16Array(evt.data,4);
|
||||
audio_prepare(audio_data);
|
||||
audio_buffer_current_size_debug+=audio_data.length;
|
||||
audio_buffer_all_size_debug+=audio_data.length;
|
||||
if(!(ios||is_chrome) && (audio_initialized==0 && audio_prepared_buffers.length>audio_buffering_fill_to)) audio_init()
|
||||
}
|
||||
else if(first3Chars=="FFT")
|
||||
{
|
||||
//alert("Yupee! Doing FFT");
|
||||
//if(first4Chars=="FFTS") console.log("FFTS");
|
||||
if(fft_compression=="none") waterfall_add_queue(new Float32Array(evt.data,4));
|
||||
else if(fft_compression="adpcm")
|
||||
{
|
||||
fft_codec.reset();
|
||||
if (evt.data.substr(0, 16) == "CLIENT DE SERVER") {
|
||||
divlog("Server acknowledged WebSocket connection.");
|
||||
} else {
|
||||
try {
|
||||
json = JSON.parse(evt.data)
|
||||
switch (json.type) {
|
||||
case "config":
|
||||
config = json.value;
|
||||
window.waterfall_colors = config.waterfall_colors;
|
||||
window.waterfall_min_level_default = config.waterfall_min_level;
|
||||
window.waterfall_max_level_default = config.waterfall_max_level;
|
||||
window.waterfall_auto_level_margin = config.waterfall_auto_level_margin;
|
||||
waterfallColorsDefault();
|
||||
|
||||
window.starting_mod = config.start_mod
|
||||
window.starting_offset_frequency = config.start_offset_frequency;
|
||||
window.audio_buffering_fill_to = config.client_audio_buffer_size;
|
||||
bandwidth = config.samp_rate;
|
||||
center_freq = config.center_freq + config.lfo_offset;
|
||||
fft_size = config.fft_size;
|
||||
fft_fps = config.fft_fps;
|
||||
audio_compression = config.audio_compression;
|
||||
divlog( "Audio stream is "+ ((audio_compression=="adpcm")?"compressed":"uncompressed")+"." )
|
||||
fft_compression = config.fft_compression;
|
||||
divlog( "FFT stream is "+ ((fft_compression=="adpcm")?"compressed":"uncompressed")+"." )
|
||||
max_clients_num = config.max_clients;
|
||||
progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num*0.85);
|
||||
mathbox_waterfall_colors = config.mathbox_waterfall_colors;
|
||||
mathbox_waterfall_frequency_resolution = config.mathbox_waterfall_frequency_resolution;
|
||||
mathbox_waterfall_history_length = config.mathbox_waterfall_history_length;
|
||||
|
||||
var waterfall_i16=fft_codec.decode(new Uint8Array(evt.data,4));
|
||||
var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N);
|
||||
for(var i=0;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();
|
||||
audio_preinit();
|
||||
break;
|
||||
case "bandwidth":
|
||||
bandwidth=parseInt(param[1]);
|
||||
break;
|
||||
case "center_freq":
|
||||
center_freq=parseInt(param[1]); //there was no ; and it was no problem... why?
|
||||
break;
|
||||
case "fft_size":
|
||||
fft_size=parseInt(param[1]);
|
||||
break;
|
||||
case "secondary_fft_size":
|
||||
secondary_fft_size=parseInt(param[1]);
|
||||
break;
|
||||
case "secondary_setup":
|
||||
|
||||
if (audio_allowed && !audio_initialized) audio_init();
|
||||
waterfall_clear();
|
||||
break;
|
||||
case "secondary_config":
|
||||
window.secondary_fft_size = json.value.secondary_fft_size;
|
||||
window.secondary_bw = json.value.secondary_bw;
|
||||
window.if_samp_rate = json.value.if_samp_rate;
|
||||
secondary_demod_init_canvases();
|
||||
break;
|
||||
case "if_samp_rate":
|
||||
if_samp_rate=parseInt(param[1]);
|
||||
break;
|
||||
case "secondary_bw":
|
||||
secondary_bw=parseFloat(param[1]);
|
||||
break;
|
||||
case "fft_fps":
|
||||
fft_fps=parseInt(param[1]);
|
||||
break;
|
||||
case "audio_compression":
|
||||
audio_compression=param[1];
|
||||
divlog( "Audio stream is "+ ((audio_compression=="adpcm")?"compressed":"uncompressed")+"." )
|
||||
break;
|
||||
case "fft_compression":
|
||||
fft_compression=param[1];
|
||||
divlog( "FFT stream is "+ ((fft_compression=="adpcm")?"compressed":"uncompressed")+"." )
|
||||
break;
|
||||
case "cpu_usage":
|
||||
var server_cpu_usage=parseInt(param[1]);
|
||||
progressbar_set(e("openwebrx-bar-server-cpu"),server_cpu_usage/100,"Server CPU ["+param[1]+"%]",server_cpu_usage>85);
|
||||
break;
|
||||
case "clients":
|
||||
var clients_num=parseInt(param[1]);
|
||||
progressbar_set(e("openwebrx-bar-clients"),clients_num/max_clients_num,"Clients ["+param[1]+"]",clients_num>max_clients_num*0.85);
|
||||
break;
|
||||
case "max_clients":
|
||||
max_clients_num=parseInt(param[1]);
|
||||
break;
|
||||
case "s":
|
||||
smeter_level=parseFloat(param[1]);
|
||||
setSmeterAbsoluteValue(smeter_level);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/*}
|
||||
catch(err)
|
||||
{
|
||||
divlog("Received invalid message over WebSocket.");
|
||||
}*/
|
||||
} else if (first3Chars=='MET')
|
||||
{
|
||||
var stringData=arrayBufferToString(evt.data);
|
||||
var metaPanels = Array.prototype.filter.call(document.getElementsByClassName('openwebrx-panel'), function(el) {
|
||||
return el.dataset.panelName == 'metadata';
|
||||
});
|
||||
break;
|
||||
case "receiver_details":
|
||||
var r = json.value;
|
||||
e('webrx-rx-title').innerHTML = r.receiver_name;
|
||||
e('webrx-rx-desc').innerHTML = r.receiver_location + ' | Loc: ' + r.receiver_qra + ', ASL: ' + r.receiver_asl + ' m, <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>';
|
||||
e('webrx-rx-photo-title').innerHTML = r.photo_title;
|
||||
e('webrx-rx-photo-desc').innerHTML = r.photo_desc;
|
||||
break;
|
||||
case "smeter":
|
||||
smeter_level = json.value;
|
||||
setSmeterAbsoluteValue(smeter_level);
|
||||
break;
|
||||
case "cpuusage":
|
||||
var server_cpu_usage = json.value;
|
||||
progressbar_set(e("openwebrx-bar-server-cpu"),server_cpu_usage,"Server CPU [" + Math.round(server_cpu_usage * 100) + "%]",server_cpu_usage>85);
|
||||
break;
|
||||
case "clients":
|
||||
client_num = json.value;
|
||||
progressbar_set(e("openwebrx-bar-clients"), client_num / max_clients_num, "Clients [" + client_num + "]", client_num > max_clients_num*0.85);
|
||||
break;
|
||||
case "profiles":
|
||||
var listbox = e("openwebrx-sdr-profiles-listbox");
|
||||
listbox.innerHTML = json.value.map(function(profile){
|
||||
return '<option value="' + profile.id + '">' + profile.name + "</option>";
|
||||
}).join("");
|
||||
break;
|
||||
case "features":
|
||||
for (var feature in json.value) {
|
||||
$('[data-feature="' + feature + '"')[json.value[feature] ? "show" : "hide"]();
|
||||
}
|
||||
break;
|
||||
case "metadata":
|
||||
update_metadata(json.value);
|
||||
break;
|
||||
default:
|
||||
console.warn('received message of unknown type: ' + json.type);
|
||||
}
|
||||
} catch (e) {
|
||||
// don't lose exception
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
} else if (evt.data instanceof ArrayBuffer) {
|
||||
// binary messages
|
||||
debug_ws_data_received += evt.data.byteLength / 1000;
|
||||
|
||||
var meta = {};
|
||||
stringData.substr(4).split(";").forEach(function(s) {
|
||||
var item = s.split(":");
|
||||
meta[item[0]] = item[1];
|
||||
});
|
||||
type = new Uint8Array(evt.data, 0, 1)[0]
|
||||
data = evt.data.slice(1)
|
||||
|
||||
var update = function(el) {
|
||||
el.innerHTML = "";
|
||||
}
|
||||
if (meta.protocol) switch (meta.protocol) {
|
||||
case 'DMR':
|
||||
if (meta.slot) {
|
||||
var html = 'Timeslot: ' + meta.slot;
|
||||
if (meta.type) html += ' Typ: ' + meta.type;
|
||||
if (meta.source && meta.target) html += ' Source: ' + meta.source + ' Target: ' + meta.target;
|
||||
update = function(el) {
|
||||
var slotEl = el.getElementsByClassName('slot-' + meta.slot);
|
||||
if (!slotEl.length) {
|
||||
slotEl = document.createElement('div');
|
||||
slotEl.className = 'slot-' + meta.slot;
|
||||
el.appendChild(slotEl);
|
||||
} else {
|
||||
slotEl = slotEl[0];
|
||||
}
|
||||
slotEl.innerHTML = html;
|
||||
};
|
||||
switch (type) {
|
||||
case 1:
|
||||
// FFT data
|
||||
if (fft_compression=="none") {
|
||||
waterfall_add_queue(new Float32Array(data));
|
||||
} else if (fft_compression == "adpcm") {
|
||||
fft_codec.reset();
|
||||
|
||||
var waterfall_i16=fft_codec.decode(new Uint8Array(data));
|
||||
var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N);
|
||||
for(var i=0;i<waterfall_i16.length;i++) waterfall_f32[i]=waterfall_i16[i+COMPRESS_FFT_PAD_N]/100;
|
||||
waterfall_add_queue(waterfall_f32);
|
||||
}
|
||||
break;
|
||||
case 'YSF':
|
||||
var strings = [];
|
||||
if (meta.source) strings.push("Source: " + meta.source);
|
||||
if (meta.target) strings.push("Destination: " + meta.target);
|
||||
if (meta.up) strings.push("Up: " + meta.up);
|
||||
if (meta.down) strings.push("Down: " + meta.down);
|
||||
var html = strings.join(' ');
|
||||
update = function(el) {
|
||||
el.innerHTML = html;
|
||||
break;
|
||||
case 2:
|
||||
// audio data
|
||||
var audio_data;
|
||||
if (audio_compression=="adpcm") {
|
||||
audio_data = new Uint8Array(data);
|
||||
} else {
|
||||
audio_data = new Int16Array(data);
|
||||
}
|
||||
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)
|
||||
{
|
||||
problems_span=e("openwebrx-problems");
|
||||
@ -1630,7 +1695,9 @@ function audio_flush_notused()
|
||||
|
||||
function webrx_set_param(what, value)
|
||||
{
|
||||
ws.send("SET "+what+"="+value.toString());
|
||||
params = {};
|
||||
params[what] = value;
|
||||
ws.send(JSON.stringify({"type":"dspcontrol","params":params}));
|
||||
}
|
||||
|
||||
var starting_mute = false;
|
||||
@ -1645,7 +1712,7 @@ function parsehash()
|
||||
if(harr[0]=="mute") toggleMute();
|
||||
else if(harr[0]=="mod") starting_mod = harr[1];
|
||||
else if(harr[0]=="sql")
|
||||
{
|
||||
{
|
||||
e("openwebrx-panel-squelch").value=harr[1];
|
||||
updateSquelch();
|
||||
}
|
||||
@ -1680,16 +1747,18 @@ function audio_preinit()
|
||||
else if(audio_context.sampleRate>44100*4)
|
||||
audio_buffer_size = 4096 * 4;
|
||||
|
||||
audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED);
|
||||
audio_last_output_buffer = new Float32Array(audio_buffer_size);
|
||||
if (!audio_rebuffer) {
|
||||
audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED);
|
||||
audio_last_output_buffer = new Float32Array(audio_buffer_size);
|
||||
|
||||
//we send our setup packet
|
||||
parsehash();
|
||||
//we send our setup packet
|
||||
parsehash();
|
||||
|
||||
audio_calculate_resampling(audio_context.sampleRate);
|
||||
audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1);
|
||||
ws.send("SET output_rate="+audio_server_output_rate.toString()+" action=start"); //now we'll get AUD packets as well
|
||||
audio_calculate_resampling(audio_context.sampleRate);
|
||||
audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1);
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({"type":"dspcontrol","action":"start","params":{"output_rate":audio_server_output_rate}}));
|
||||
}
|
||||
|
||||
function audio_init()
|
||||
@ -1751,7 +1820,10 @@ function on_ws_closed()
|
||||
audio_node.disconnect();
|
||||
}
|
||||
catch (dont_care) {}
|
||||
divlog("WebSocket has closed unexpectedly. Please reload the page.", 1);
|
||||
audio_initialized = 0;
|
||||
divlog("WebSocket has closed unexpectedly. Attempting to reconnect in 5 seconds...", 1);
|
||||
|
||||
setTimeout(open_websocket, 5000);
|
||||
}
|
||||
|
||||
function on_ws_error(event)
|
||||
@ -1763,14 +1835,15 @@ String.prototype.startswith=function(str){ return this.indexOf(str) == 0; }; //h
|
||||
|
||||
function open_websocket()
|
||||
{
|
||||
//if(ws_url.startswith("ws://localhost:")&&window.location.hostname!="127.0.0.1"&&window.location.hostname!="localhost")
|
||||
//{
|
||||
//divlog("Server administrator should set <em>server_hostname</em> correctly, because it is left as <em>\"localhost\"</em>. Now guessing hostname from page URL.",1);
|
||||
ws_url="ws://"+(window.location.origin.split("://")[1])+"/ws/"; //guess automatically -> now default behaviour
|
||||
//}
|
||||
var protocol = 'ws';
|
||||
if (window.location.toString().startsWith('https://')) {
|
||||
protocol = 'wss';
|
||||
}
|
||||
|
||||
ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; //guess automatically -> now default behaviour
|
||||
if (!("WebSocket" in window))
|
||||
divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser.");
|
||||
ws = new WebSocket(ws_url+client_id);
|
||||
ws = new WebSocket(ws_url);
|
||||
ws.onopen = on_ws_opened;
|
||||
ws.onmessage = on_ws_recv;
|
||||
ws.onclose = on_ws_closed;
|
||||
@ -2023,27 +2096,23 @@ function waterfall_add(data)
|
||||
waterfall_image.data[base+x*4+i] = ((color>>>0)>>((3-i)*8))&0xff;
|
||||
}*/
|
||||
|
||||
if(mathbox_mode==MATHBOX_MODES.WATERFALL)
|
||||
{
|
||||
if (mathbox_mode==MATHBOX_MODES.WATERFALL) {
|
||||
//Handle mathbox
|
||||
for(var i=0;i<fft_size;i++) mathbox_data[i+mathbox_data_index*fft_size]=data[i];
|
||||
mathbox_shift();
|
||||
}
|
||||
else
|
||||
{
|
||||
//Add line to waterfall image
|
||||
oneline_image = canvas_context.createImageData(w,1);
|
||||
for(x=0;x<w;x++)
|
||||
{
|
||||
color=waterfall_mkcolor(data[x]);
|
||||
for(i=0;i<4;i++)
|
||||
oneline_image.data[x*4+i] = ((color>>>0)>>((3-i)*8))&0xff;
|
||||
}
|
||||
} else {
|
||||
//Add line to waterfall image
|
||||
oneline_image = canvas_context.createImageData(w,1);
|
||||
for (x=0;x<w;x++) {
|
||||
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
|
||||
canvas_context.putImageData(oneline_image, 0, canvas_actual_line--);
|
||||
shift_canvases();
|
||||
if(canvas_actual_line<0) add_canvas();
|
||||
//Draw image
|
||||
canvas_context.putImageData(oneline_image, 0, canvas_actual_line--);
|
||||
shift_canvases();
|
||||
if(canvas_actual_line<0) add_canvas();
|
||||
}
|
||||
|
||||
|
||||
@ -2265,6 +2334,7 @@ function openwebrx_init()
|
||||
init_rx_photo();
|
||||
open_websocket();
|
||||
secondary_demod_init();
|
||||
digimodes_init();
|
||||
place_panels(first_show_panel);
|
||||
window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000);
|
||||
window.addEventListener("resize",openwebrx_resize);
|
||||
@ -2272,7 +2342,26 @@ function openwebrx_init()
|
||||
|
||||
//Synchronise volume with slider
|
||||
updateVolume();
|
||||
waterfallColorsDefault();
|
||||
|
||||
}
|
||||
|
||||
function digimodes_init() {
|
||||
hide_digitalvoice_panels();
|
||||
|
||||
// initialze DMR timeslot muting
|
||||
$('.openwebrx-dmr-timeslot-panel').click(function(e) {
|
||||
$(e.currentTarget).toggleClass("muted");
|
||||
update_dmr_timeslot_filtering();
|
||||
});
|
||||
}
|
||||
|
||||
function update_dmr_timeslot_filtering() {
|
||||
var filter = $('.openwebrx-dmr-timeslot-panel').map(function(index, el){
|
||||
return (!$(el).hasClass("muted")) << index;
|
||||
}).toArray().reduce(function(acc, v){
|
||||
return acc | v;
|
||||
}, 0);
|
||||
webrx_set_param("dmr_filter", filter);
|
||||
}
|
||||
|
||||
function iosPlayButtonClick()
|
||||
@ -2281,6 +2370,7 @@ function iosPlayButtonClick()
|
||||
audio_init();
|
||||
e("openwebrx-big-grey").style.opacity=0;
|
||||
window.setTimeout(function(){ e("openwebrx-big-grey").style.display="none"; },1100);
|
||||
audio_allowed = 1;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -2361,6 +2451,7 @@ function pop_bottommost_panel(from)
|
||||
function toggle_panel(what, on)
|
||||
{
|
||||
var item=e(what);
|
||||
if (!item) return;
|
||||
if(typeof on !== "undefined")
|
||||
{
|
||||
if(item.openwebrxHidden && !on) return;
|
||||
@ -2424,7 +2515,7 @@ function place_panels(function_apply)
|
||||
for(i=0;i<plist.length;i++)
|
||||
{
|
||||
c=plist[i];
|
||||
if(c.className=="openwebrx-panel")
|
||||
if(c.className.indexOf("openwebrx-panel") >= 0)
|
||||
{
|
||||
if(c.openwebrxHidden)
|
||||
{
|
||||
@ -2491,29 +2582,27 @@ function progressbar_set(obj,val,text,over)
|
||||
function demodulator_buttons_update()
|
||||
{
|
||||
$(".openwebrx-demodulator-button").removeClass("highlighted");
|
||||
if(secondary_demod) $("#openwebrx-button-dig").addClass("highlighted");
|
||||
else switch(demodulators[0].subtype)
|
||||
{
|
||||
case "nfm":
|
||||
$("#openwebrx-button-nfm").addClass("highlighted");
|
||||
break;
|
||||
case "am":
|
||||
$("#openwebrx-button-am").addClass("highlighted");
|
||||
break;
|
||||
case "lsb":
|
||||
case "usb":
|
||||
case "cw":
|
||||
if(demodulators[0].high_cut-demodulators[0].low_cut<300)
|
||||
$("#openwebrx-button-cw").addClass("highlighted");
|
||||
else
|
||||
{
|
||||
if(demodulators[0].high_cut<0)
|
||||
$("#openwebrx-button-lsb").addClass("highlighted");
|
||||
else if(demodulators[0].low_cut>0)
|
||||
$("#openwebrx-button-usb").addClass("highlighted");
|
||||
else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted");
|
||||
}
|
||||
break;
|
||||
if(secondary_demod) {
|
||||
$("#openwebrx-button-dig").addClass("highlighted");
|
||||
} else switch(demodulators[0].subtype) {
|
||||
case "lsb":
|
||||
case "usb":
|
||||
case "cw":
|
||||
if(demodulators[0].high_cut-demodulators[0].low_cut<300)
|
||||
$("#openwebrx-button-cw").addClass("highlighted");
|
||||
else
|
||||
{
|
||||
if(demodulators[0].high_cut<0)
|
||||
$("#openwebrx-button-lsb").addClass("highlighted");
|
||||
else if(demodulators[0].low_cut>0)
|
||||
$("#openwebrx-button-usb").addClass("highlighted");
|
||||
else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
var mod = demodulators[0].subtype;
|
||||
$("#openwebrx-button-" + mod).addClass("highlighted");
|
||||
break;
|
||||
}
|
||||
}
|
||||
function demodulator_analog_replace_last() { demodulator_analog_replace(last_analog_demodulator_subtype); }
|
||||
@ -2616,19 +2705,19 @@ function secondary_demod_init()
|
||||
function secondary_demod_start(subtype)
|
||||
{
|
||||
secondary_demod_canvases_initialized = false;
|
||||
ws.send("SET secondary_mod="+subtype);
|
||||
secondary_demod = subtype;
|
||||
ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":subtype}}));
|
||||
secondary_demod = subtype;
|
||||
}
|
||||
|
||||
function secondary_demod_set()
|
||||
{
|
||||
ws.send("SET secondary_offset_freq="+secondary_demod_offset_freq.toString());
|
||||
ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_offset_freq":secondary_demod_offset_freq}}));
|
||||
}
|
||||
|
||||
function secondary_demod_stop()
|
||||
{
|
||||
ws.send("SET secondary_mod=off");
|
||||
secondary_demod = false;
|
||||
ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":false}}));
|
||||
secondary_demod = false;
|
||||
secondary_demod_waterfall_queue = [];
|
||||
}
|
||||
|
||||
@ -2755,7 +2844,7 @@ function secondary_demod_update_channel_freq_from_event(evt)
|
||||
{
|
||||
secondary_demod_waiting_for_set = true;
|
||||
window.setTimeout(()=>{
|
||||
ws.send("SET secondary_offset_freq="+Math.floor(secondary_demod_channel_freq));
|
||||
ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_offset_freq":Math.floor(secondary_demod_channel_freq)}}));
|
||||
//console.log("doneset:", secondary_demod_channel_freq);
|
||||
secondary_demod_waiting_for_set = false;
|
||||
}, 50);
|
||||
@ -2815,3 +2904,8 @@ function secondary_demod_waterfall_set_zoom(low_cut, high_cut)
|
||||
secondary_demod_canvases.map((x)=>{$(x).css("left",secondary_demod_canvas_left+"px").css("width",secondary_demod_canvas_width+"px");});
|
||||
secondary_demod_update_channel_freq_from_event();
|
||||
}
|
||||
|
||||
function sdr_profile_changed() {
|
||||
value = $('#openwebrx-sdr-profiles-listbox').val();
|
||||
ws.send(JSON.stringify({ type:"selectprofile", params:{ profile:value }}));
|
||||
}
|
||||
|
773
openwebrx.py
Executable file → Normal file
773
openwebrx.py
Executable file → Normal file
@ -1,757 +1,52 @@
|
||||
#!/usr/bin/python2
|
||||
print "" # python2.7 is required to run OpenWebRX instead of python3. Please run me by: python2 openwebrx.py
|
||||
"""
|
||||
from http.server import HTTPServer
|
||||
from owrx.http import RequestHandler
|
||||
from owrx.config import PropertyManager
|
||||
from owrx.feature import FeatureDetector
|
||||
from owrx.source import SdrService, ClientRegistry
|
||||
from socketserver import ThreadingMixIn
|
||||
from owrx.sdrhu import SdrHuUpdater
|
||||
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
import logging
|
||||
logging.basicConfig(level = logging.DEBUG, format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <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):
|
||||
class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
|
||||
pass
|
||||
|
||||
def handle_signal(sig, frame):
|
||||
global spectrum_dsp
|
||||
if sig == signal.SIGUSR1:
|
||||
print "[openwebrx] Verbose status information on USR1 signal"
|
||||
print
|
||||
print "time.time() =", time.time()
|
||||
print "clients_mutex.locked() =", clients_mutex.locked()
|
||||
print "clients_mutex_locker =", clients_mutex_locker
|
||||
if server_fail: print "server_fail = ", server_fail
|
||||
print "spectrum_thread_watchdog_last_tick =", spectrum_thread_watchdog_last_tick
|
||||
print
|
||||
print "clients:",len(clients)
|
||||
for client in clients:
|
||||
print
|
||||
for key in client._fields:
|
||||
print "\t%s = %s"%(key,str(getattr(client,key)))
|
||||
elif sig == signal.SIGUSR2:
|
||||
code.interact(local=globals())
|
||||
else:
|
||||
print "[openwebrx] Ctrl+C: aborting."
|
||||
cleanup_clients(True)
|
||||
spectrum_dsp.stop()
|
||||
os._exit(1) #not too graceful exit
|
||||
|
||||
def access_log(data):
|
||||
global logs
|
||||
logs.access_log.write("["+datetime.datetime.now().isoformat()+"] "+data+"\n")
|
||||
logs.access_log.flush()
|
||||
|
||||
receiver_failed=spectrum_thread_watchdog_last_tick=rtl_thread=spectrum_dsp=server_fail=None
|
||||
|
||||
def main():
|
||||
global clients, clients_mutex, pypy, lock_try_time, avatar_ctime, cfg, logs
|
||||
global serverfail, rtl_thread
|
||||
print
|
||||
print "OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package"
|
||||
print "_________________________________________________________________________________________________"
|
||||
print
|
||||
print "Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>"
|
||||
print
|
||||
print("""
|
||||
|
||||
no_arguments=len(sys.argv)==1
|
||||
if no_arguments: print "[openwebrx-main] Configuration script not specified. I will use: \"config_webrx.py\""
|
||||
cfg=__import__("config_webrx" if no_arguments else sys.argv[1])
|
||||
for option in ("access_log","csdr_dynamic_bufsize","csdr_print_bufsizes","csdr_through"):
|
||||
if not option in dir(cfg): setattr(cfg, option, False) #initialize optional config parameters
|
||||
OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package
|
||||
_________________________________________________________________________________________________
|
||||
|
||||
#Open log files
|
||||
logs = type("logs_class", (object,), {"access_log":open(cfg.access_log if cfg.access_log else "/dev/null","a"), "error_log":""})()
|
||||
Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>
|
||||
|
||||
#Set signal handler
|
||||
signal.signal(signal.SIGINT, handle_signal) #http://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python
|
||||
signal.signal(signal.SIGUSR1, handle_signal)
|
||||
signal.signal(signal.SIGUSR2, handle_signal)
|
||||
""")
|
||||
|
||||
#Pypy
|
||||
if pypy: print "pypy detected (and now something completely different: c code is expected to run at a speed of 3*10^8 m/s?)"
|
||||
pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
|
||||
|
||||
#Change process name to "openwebrx" (to be seen in ps)
|
||||
try:
|
||||
for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]:
|
||||
if os.path.exists(libcpath):
|
||||
libc = dl.open(libcpath)
|
||||
libc.call("prctl", 15, "openwebrx", 0, 0, 0)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
#Start rtl thread
|
||||
if os.system("csdr 2> /dev/null") == 32512: #check for csdr
|
||||
print "[openwebrx-main] You need to install \"csdr\" to run OpenWebRX!\n"
|
||||
featureDetector = FeatureDetector()
|
||||
if not featureDetector.is_available("core"):
|
||||
print("you are missing required dependencies to run openwebrx. "
|
||||
"please check that the following core requirements are installed:")
|
||||
print(", ".join(featureDetector.get_requirements("core")))
|
||||
return
|
||||
if os.system("nmux --help 2> /dev/null") == 32512: #check for nmux
|
||||
print "[openwebrx-main] You need to install an up-to-date version of \"csdr\" that contains the \"nmux\" tool to run OpenWebRX! Please upgrade \"csdr\"!\n"
|
||||
return
|
||||
if cfg.start_rtl_thread:
|
||||
nmux_bufcnt = nmux_bufsize = 0
|
||||
while nmux_bufsize < cfg.samp_rate/4: nmux_bufsize += 4096
|
||||
while nmux_bufsize * nmux_bufcnt < cfg.nmux_memory * 1e6: nmux_bufcnt += 1
|
||||
if nmux_bufcnt == 0 or nmux_bufsize == 0:
|
||||
print "[openwebrx-main] Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py"
|
||||
return
|
||||
print "[openwebrx-main] nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)
|
||||
cfg.start_rtl_command += "| nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, cfg.iq_server_port)
|
||||
rtl_thread=threading.Thread(target = lambda:subprocess.Popen(cfg.start_rtl_command, shell=True), args=())
|
||||
rtl_thread.start()
|
||||
print "[openwebrx-main] Started rtl_thread: "+cfg.start_rtl_command
|
||||
print "[openwebrx-main] Waiting for I/Q server to start..."
|
||||
while True:
|
||||
testsock=socket.socket()
|
||||
try: testsock.connect(("127.0.0.1", cfg.iq_server_port))
|
||||
except:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
testsock.close()
|
||||
break
|
||||
print "[openwebrx-main] I/Q server started."
|
||||
|
||||
#Initialize clients
|
||||
clients=[]
|
||||
clients_mutex=threading.Lock()
|
||||
lock_try_time=0
|
||||
# Get error messages about unknown / unavailable features as soon as possible
|
||||
SdrService.loadProps()
|
||||
|
||||
#Start watchdog thread
|
||||
print "[openwebrx-main] Starting watchdog threads."
|
||||
mutex_test_thread=threading.Thread(target = mutex_test_thread_function, args = ())
|
||||
mutex_test_thread.start()
|
||||
mutex_watchdog_thread=threading.Thread(target = mutex_watchdog_thread_function, args = ())
|
||||
mutex_watchdog_thread.start()
|
||||
if "sdrhu_key" in pm and pm["sdrhu_public_listing"]:
|
||||
updater = SdrHuUpdater()
|
||||
updater.start()
|
||||
|
||||
server = ThreadedHttpServer(('0.0.0.0', pm.getPropertyValue("web_port")), RequestHandler)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
#Start spectrum thread
|
||||
print "[openwebrx-main] Starting spectrum thread."
|
||||
spectrum_thread=threading.Thread(target = spectrum_thread_function, args = ())
|
||||
spectrum_thread.start()
|
||||
#spectrum_watchdog_thread=threading.Thread(target = spectrum_watchdog_thread_function, args = ())
|
||||
#spectrum_watchdog_thread.start()
|
||||
|
||||
get_cpu_usage()
|
||||
bcastmsg_thread=threading.Thread(target = bcastmsg_thread_function, args = ())
|
||||
bcastmsg_thread.start()
|
||||
|
||||
#threading.Thread(target = measure_thread_function, args = ()).start()
|
||||
|
||||
#Start sdr.hu update thread
|
||||
if sdrhu and cfg.sdrhu_key and cfg.sdrhu_public_listing:
|
||||
print "[openwebrx-main] Starting sdr.hu update thread..."
|
||||
avatar_ctime=str(os.path.getctime("htdocs/gfx/openwebrx-avatar.png"))
|
||||
sdrhu_thread=threading.Thread(target = sdrhu.run, args = ())
|
||||
sdrhu_thread.start()
|
||||
|
||||
#Start HTTP thread
|
||||
httpd = MultiThreadHTTPServer(('', cfg.web_port), WebRXHandler)
|
||||
print('[openwebrx-main] Starting HTTP server.')
|
||||
access_log("Starting OpenWebRX...")
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
# This is a debug function below:
|
||||
measure_value=0
|
||||
def measure_thread_function():
|
||||
global measure_value
|
||||
while True:
|
||||
print "[openwebrx-measure] value is",measure_value
|
||||
measure_value=0
|
||||
time.sleep(1)
|
||||
|
||||
def bcastmsg_thread_function():
|
||||
global clients
|
||||
while True:
|
||||
time.sleep(3)
|
||||
try: cpu_usage=get_cpu_usage()
|
||||
except: cpu_usage=0
|
||||
cma("bcastmsg_thread")
|
||||
for i in range(0,len(clients)):
|
||||
clients[i].bcastmsg="MSG cpu_usage={0} clients={1}".format(int(cpu_usage*100),len(clients))
|
||||
cmr()
|
||||
|
||||
def mutex_test_thread_function():
|
||||
global clients_mutex, lock_try_time
|
||||
while True:
|
||||
time.sleep(0.5)
|
||||
lock_try_time=time.time()
|
||||
clients_mutex.acquire()
|
||||
clients_mutex.release()
|
||||
lock_try_time=0
|
||||
|
||||
def cma(what): #clients_mutex acquire
|
||||
global clients_mutex
|
||||
global clients_mutex_locker
|
||||
if not clients_mutex.locked(): clients_mutex_locker = what
|
||||
clients_mutex.acquire()
|
||||
|
||||
def cmr():
|
||||
global clients_mutex
|
||||
global clients_mutex_locker
|
||||
clients_mutex_locker = None
|
||||
clients_mutex.release()
|
||||
|
||||
def mutex_watchdog_thread_function():
|
||||
global lock_try_time
|
||||
global clients_mutex_locker
|
||||
global clients_mutex
|
||||
while True:
|
||||
if lock_try_time != 0 and time.time()-lock_try_time > 3.0:
|
||||
#if 3 seconds pass without unlock
|
||||
print "[openwebrx-mutex-watchdog] Mutex unlock timeout. Locker: \""+str(clients_mutex_locker)+"\" Now unlocking..."
|
||||
clients_mutex.release()
|
||||
time.sleep(0.5)
|
||||
|
||||
def spectrum_watchdog_thread_function():
|
||||
global spectrum_thread_watchdog_last_tick, receiver_failed
|
||||
while True:
|
||||
time.sleep(60)
|
||||
if spectrum_thread_watchdog_last_tick and time.time()-spectrum_thread_watchdog_last_tick > 60.0:
|
||||
print "[openwebrx-spectrum-watchdog] Spectrum timeout. Seems like no I/Q data is coming from the receiver.\nIf you're using RTL-SDR, the receiver hardware may randomly fail under some circumstances:\n1) high temperature,\n2) insufficient current available from the USB port."
|
||||
print "[openwebrx-spectrum-watchdog] Deactivating receiver."
|
||||
receiver_failed="spectrum"
|
||||
return
|
||||
|
||||
def check_server():
|
||||
global spectrum_dsp, server_fail, rtl_thread
|
||||
if server_fail: return server_fail
|
||||
#print spectrum_dsp.process.poll()
|
||||
if spectrum_dsp and spectrum_dsp.process.poll()!=None: server_fail = "spectrum_thread dsp subprocess failed"
|
||||
#if rtl_thread and not rtl_thread.is_alive(): server_fail = "rtl_thread failed"
|
||||
if server_fail: print "[openwebrx-check_server] >>>>>>> ERROR:", server_fail
|
||||
return server_fail
|
||||
|
||||
def apply_csdr_cfg_to_dsp(dsp):
|
||||
dsp.csdr_dynamic_bufsize = cfg.csdr_dynamic_bufsize
|
||||
dsp.csdr_print_bufsizes = cfg.csdr_print_bufsizes
|
||||
dsp.csdr_through = cfg.csdr_through
|
||||
|
||||
def spectrum_thread_function():
|
||||
global clients, spectrum_dsp, spectrum_thread_watchdog_last_tick
|
||||
spectrum_dsp=dsp=csdr.dsp()
|
||||
dsp.nc_port=cfg.iq_server_port
|
||||
dsp.set_demodulator("fft")
|
||||
dsp.set_samp_rate(cfg.samp_rate)
|
||||
dsp.set_fft_size(cfg.fft_size)
|
||||
dsp.set_fft_fps(cfg.fft_fps)
|
||||
dsp.set_fft_averages(int(round(1.0 * cfg.samp_rate / cfg.fft_size / cfg.fft_fps / (1.0 - cfg.fft_voverlap_factor))) if cfg.fft_voverlap_factor>0 else 0)
|
||||
dsp.set_fft_compression(cfg.fft_compression)
|
||||
dsp.set_format_conversion(cfg.format_conversion)
|
||||
apply_csdr_cfg_to_dsp(dsp)
|
||||
sleep_sec=0.87/cfg.fft_fps
|
||||
print "[openwebrx-spectrum] Spectrum thread initialized successfully."
|
||||
dsp.start()
|
||||
if cfg.csdr_dynamic_bufsize:
|
||||
dsp.read_async(8) #dummy read to skip bufsize & preamble
|
||||
print "[openwebrx-spectrum] Note: CSDR_DYNAMIC_BUFSIZE_ON = 1"
|
||||
print "[openwebrx-spectrum] Spectrum thread started."
|
||||
bytes_to_read=int(dsp.get_fft_bytes_to_read())
|
||||
spectrum_thread_counter=0
|
||||
while True:
|
||||
data=dsp.read_async(bytes_to_read)
|
||||
if data is None:
|
||||
time.sleep(.01)
|
||||
continue
|
||||
#print "gotcha",len(data),"bytes of spectrum data via spectrum_thread_function()"
|
||||
if spectrum_thread_counter >= cfg.fft_fps:
|
||||
spectrum_thread_counter=0
|
||||
spectrum_thread_watchdog_last_tick = time.time() #once every second
|
||||
else: spectrum_thread_counter+=1
|
||||
cma("spectrum_thread")
|
||||
correction=0
|
||||
for i in range(0,len(clients)):
|
||||
i-=correction
|
||||
if (clients[i].ws_started):
|
||||
if clients[i].spectrum_queue.full():
|
||||
print "[openwebrx-spectrum] client spectrum queue full, closing it."
|
||||
close_client(i, False)
|
||||
correction+=1
|
||||
else:
|
||||
clients[i].spectrum_queue.put([data]) # add new string by "reference" to all clients
|
||||
cmr()
|
||||
|
||||
def get_client_by_id(client_id, use_mutex=True):
|
||||
global clients
|
||||
output=-1
|
||||
if use_mutex: cma("get_client_by_id")
|
||||
for i in range(0,len(clients)):
|
||||
if(clients[i].id==client_id):
|
||||
output=i
|
||||
break
|
||||
if use_mutex: cmr()
|
||||
if output==-1:
|
||||
raise ClientNotFoundException
|
||||
else:
|
||||
return output
|
||||
|
||||
def log_client(client, what):
|
||||
print "[openwebrx-httpd] client {0}#{1} :: {2}".format(client.ip,client.id,what)
|
||||
|
||||
def cleanup_clients(end_all=False):
|
||||
# - if a client doesn't open websocket for too long time, we drop it
|
||||
# - or if end_all is true, we drop all clients
|
||||
global clients
|
||||
cma("cleanup_clients")
|
||||
correction=0
|
||||
for i in range(0,len(clients)):
|
||||
i-=correction
|
||||
#print "cleanup_clients:: len(clients)=", len(clients), "i=", i
|
||||
if end_all or ((not clients[i].ws_started) and (time.time()-clients[i].gen_time)>45):
|
||||
if not end_all: print "[openwebrx] cleanup_clients :: client timeout to open WebSocket"
|
||||
close_client(i, False)
|
||||
correction+=1
|
||||
cmr()
|
||||
|
||||
def generate_client_id(ip):
|
||||
#add a client
|
||||
global clients
|
||||
new_client=namedtuple("ClientStruct", "id gen_time ws_started sprectum_queue ip closed bcastmsg dsp loopstat")
|
||||
new_client.id=md5.md5(str(random.random())).hexdigest()
|
||||
new_client.gen_time=time.time()
|
||||
new_client.ws_started=False # to check whether client has ever tried to open the websocket
|
||||
new_client.spectrum_queue=Queue.Queue(1000)
|
||||
new_client.ip=ip
|
||||
new_client.bcastmsg=""
|
||||
new_client.closed=[False] #byref, not exactly sure if required
|
||||
new_client.dsp=None
|
||||
cma("generate_client_id")
|
||||
clients.append(new_client)
|
||||
log_client(new_client,"client added. Clients now: {0}".format(len(clients)))
|
||||
cmr()
|
||||
cleanup_clients()
|
||||
return new_client.id
|
||||
|
||||
def close_client(i, use_mutex=True):
|
||||
global clients
|
||||
log_client(clients[i],"client being closed.")
|
||||
if use_mutex: cma("close_client")
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
clients[i].dsp.stop()
|
||||
except:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
print "[openwebrx] close_client dsp.stop() :: error -",exc_type,exc_value
|
||||
traceback.print_tb(exc_traceback)
|
||||
clients[i].closed[0]=True
|
||||
access_log("Stopped streaming to client: "+clients[i].ip+"#"+str(clients[i].id)+" (users now: "+str(len(clients)-1)+")")
|
||||
del clients[i]
|
||||
if use_mutex: cmr()
|
||||
|
||||
# http://www.codeproject.com/Articles/462525/Simple-HTTP-Server-and-Client-in-Python
|
||||
# some ideas are used from the artice above
|
||||
|
||||
class WebRXHandler(BaseHTTPRequestHandler):
|
||||
def proc_read_thread():
|
||||
pass
|
||||
|
||||
def send_302(self,what):
|
||||
self.send_response(302)
|
||||
self.send_header('Content-type','text/html')
|
||||
self.send_header("Location", "http://{0}:{1}/{2}".format(cfg.server_hostname,cfg.web_port,what))
|
||||
self.end_headers()
|
||||
self.wfile.write("<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()
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
for c in ClientRegistry.getSharedInstance().clients:
|
||||
c.close()
|
||||
|
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
|
||||
|
||||
def run(continuously=True):
|
||||
if not cfg.sdrhu_key: return
|
||||
firsttime="(Your receiver is soon getting listed on sdr.hu!)"
|
||||
while True:
|
||||
cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://"+cfg.server_hostname+":"+str(cfg.web_port)+"&apikey="+cfg.sdrhu_key+"\" 2>&1"
|
||||
print "[openwebrx-sdrhu]", cmd
|
||||
returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()
|
||||
returned=returned[0]
|
||||
#print returned
|
||||
if "UPDATE:" in returned:
|
||||
retrytime_mins = 20
|
||||
value=returned.split("UPDATE:")[1].split("\n",1)[0]
|
||||
if value.startswith("SUCCESS"):
|
||||
print "[openwebrx-sdrhu] Update succeeded! "+firsttime
|
||||
firsttime=""
|
||||
else:
|
||||
print "[openwebrx-sdrhu] Update failed, your receiver cannot be listed on sdr.hu! Reason:", value
|
||||
else:
|
||||
retrytime_mins = 2
|
||||
print "[openwebrx-sdrhu] wget failed while updating, your receiver cannot be listed on sdr.hu!"
|
||||
if not continuously: break
|
||||
time.sleep(60*retrytime_mins)
|
||||
from owrx.sdrhu import SdrHuUpdater
|
||||
from owrx.config import PropertyManager
|
||||
|
||||
if __name__=="__main__":
|
||||
run(False)
|
||||
pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
|
||||
|
||||
if not "sdrhu_key" in pm:
|
||||
exit(1)
|
||||
SdrHuUpdater().update()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user