Merge branch 'server_rework' into server_rework_dsd

This commit is contained in:
Jakob Ketterl 2019-05-13 17:46:02 +02:00
commit 823995d4ba
17 changed files with 1652 additions and 1483 deletions

View File

@ -36,7 +36,6 @@ config_webrx: configuration options for OpenWebRX
# ==== Server settings ==== # ==== Server settings ====
web_port=8073 web_port=8073
server_hostname="localhost" # If this contains an incorrect value, the web UI may freeze on load (it can't open websocket)
max_clients=20 max_clients=20
# ==== Web GUI configuration ==== # ==== Web GUI configuration ====
@ -65,26 +64,19 @@ Website: <a href="http://localhost" target="_blank">http://localhost</a>
sdrhu_key = "" sdrhu_key = ""
# 3. Set this setting to True to enable listing: # 3. Set this setting to True to enable listing:
sdrhu_public_listing = False sdrhu_public_listing = False
server_hostname="localhost"
# ==== DSP/RX settings ==== # ==== DSP/RX settings ====
fft_fps=9 fft_fps=9
fft_size=4096 #Should be power of 2 fft_size=4096 #Should be power of 2
fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
# samp_rate = 250000
samp_rate = 2400000
center_freq = 144250000
rf_gain = 5 #in dB. For an RTL-SDR, rf_gain=0 will set the tuner to auto gain mode, else it will be in manual gain mode.
ppm = 0
audio_compression="adpcm" #valid values: "adpcm", "none" audio_compression="adpcm" #valid values: "adpcm", "none"
fft_compression="adpcm" #valid values: "adpcm", "none" fft_compression="adpcm" #valid values: "adpcm", "none"
digimodes_enable=True #Decoding digimodes come with higher CPU usage. digimodes_enable=True #Decoding digimodes come with higher CPU usage.
digimodes_fft_size=1024 digimodes_fft_size=1024
start_rtl_thread=True
""" """
Note: if you experience audio underruns while CPU usage is 100%, you can: Note: if you experience audio underruns while CPU usage is 100%, you can:
- decrease `samp_rate`, - decrease `samp_rate`,
@ -101,81 +93,95 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
# Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support # # Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support #
################################################################################################# #################################################################################################
# You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... Some examples of configuration are available here (default is RTL-SDR): # Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf"
# >> RTL-SDR via rtl_sdr sdrs = {
start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) "rtlsdr": {
format_conversion="csdr convert_u8_f" "name": "RTL-SDR USB Stick",
"type": "rtl_sdr",
#lna_gain=8 "ppm": 0,
#rf_amp=1 # you can change this if you use an upconverter. formula is:
#start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm, rf_amp=rf_amp, lna_gain=lna_gain) # shown_center_freq = center_freq + lfo_offset
#format_conversion="csdr convert_s8_f" # "lfo_offset": 0,
""" "profiles": {
To use a HackRF, compile the HackRF host tools from its "stdout" branch: "70cm": {
git clone https://github.com/mossmann/hackrf/ "name": "70cm Relais",
cd hackrf "center_freq": 438800000,
git fetch "rf_gain": 30,
git checkout origin/stdout "samp_rate": 2400000,
cd host "start_freq": 439275000,
mkdir build "start_mod": "nfm"
cd build },
cmake .. -DINSTALL_UDEV_RULES=ON "2m": {
make "name": "2m komplett",
sudo make install "center_freq": 145000000,
""" "rf_gain": 30,
"samp_rate": 2400000,
# >> Sound card SDR (needs ALSA) "start_freq": 145725000,
# I did not have the chance to properly test it. "start_mod": "nfm"
#samp_rate = 96000 }
#start_rtl_command="arecord -f S16_LE -r {samp_rate} -c2 -".format(samp_rate=samp_rate) }
#format_conversion="csdr convert_s16_f | csdr gain_ff 30" },
"sdrplay": {
# >> /dev/urandom test signal source "name": "SDRPlay RSP2",
# samp_rate = 2400000 "type": "sdrplay",
# start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate) "ppm": 0,
# format_conversion="csdr convert_u8_f" "profiles": {
"20m": {
# >> Pre-recorded raw I/Q file as signal source "name":"20m",
# You will have to correctly specify: samp_rate, center_freq, format_conversion in order to correctly play an I/Q file. "center_freq": 14150000,
#start_rtl_command="(while true; do cat my_iq_file.raw; done) | csdr flowcontrol {sr} 20 ".format(sr=samp_rate*2*1.05) "rf_gain": 40,
#format_conversion="csdr convert_u8_f" "samp_rate": 500000,
"start_freq": 14070000,
#>> The rx_sdr command works with a variety of SDR harware: RTL-SDR, HackRF, SDRplay, UHD, Airspy, Red Pitaya, audio devices, etc. "start_mod": "usb"
# It will auto-detect your SDR hardware if the following tools are installed: },
# * the vendor provided driver and library, "40m": {
# * the vendor-specific SoapySDR wrapper library, "name":"40m",
# * and SoapySDR itself. "center_freq": 7100000,
# Check out this article on the OpenWebRX Wiki: https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX/ "rf_gain": 40,
#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) "samp_rate": 500000,
#format_conversion="" "start_freq": 7070000,
"start_mod": "usb"
# >> 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" "80m": {
#format_conversion="" "name":"80m",
"center_freq": 3650000,
"rf_gain": 40,
"samp_rate": 500000,
"start_freq": 3570000,
"start_mod": "usb"
},
"49m": {
"name": "49m Broadcast",
"center_freq": 6000000,
"rf_gain": 40,
"samp_rate": 500000,
"start_freq": 6070000,
"start_mod": "am"
}
}
},
# this one is just here to test feature detection
"test": {
"type": "test"
}
}
# ==== Misc settings ==== # ==== Misc settings ====
shown_center_freq = center_freq #you can change this if you use an upconverter
client_audio_buffer_size = 5 client_audio_buffer_size = 5
#increasing client_audio_buffer_size will: #increasing client_audio_buffer_size will:
# - also increase the latency # - also increase the latency
# - decrease the chance of audio underruns # - decrease the chance of audio underruns
start_freq = center_freq iq_port_range = [4950, 4960] #TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
start_mod = "nfm" #nfm, am, lsb, usb, cw
iq_server_port = 4951 #TCP port for ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
#access_log = "~/openwebrx_access.log"
# ==== Color themes ==== # ==== Color themes ====
#A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels #A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels
### default theme by teejez: ### default theme by teejez:
waterfall_colors = "[0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]" waterfall_colors = [0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]
waterfall_min_level = -88 #in dB waterfall_min_level = -88 #in dB
waterfall_max_level = -20 waterfall_max_level = -20
waterfall_auto_level_margin = (5, 40) waterfall_auto_level_margin = (5, 40)
@ -197,7 +203,7 @@ waterfall_auto_level_margin = (5, 40)
# 3D view settings # 3D view settings
mathbox_waterfall_frequency_resolution = 128 #bins mathbox_waterfall_frequency_resolution = 128 #bins
mathbox_waterfall_history_length = 10 #seconds mathbox_waterfall_history_length = 10 #seconds
mathbox_waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" mathbox_waterfall_colors = [0x000000ff,0x2e6893ff,0x69a5d0ff,0x214b69ff,0x9dc4e0ff,0xfff775ff,0xff8a8aff,0xb20000ff]
# === Experimental settings === # === Experimental settings ===
#Warning! The settings below are very experimental. #Warning! The settings below are very experimental.
@ -206,11 +212,3 @@ csdr_print_bufsizes = False # This prints the buffer sizes used for csdr proces
csdr_through = False # Setting this True will print out how much data is going into the DSP chains. csdr_through = False # Setting this True will print out how much data is going into the DSP chains.
nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux. nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux.
#Look up external IP address automatically from icanhazip.com, and use it as [server_hostname]
"""
print "[openwebrx-config] Detecting external IP address..."
import urllib2
server_hostname=urllib2.urlopen("http://icanhazip.com").read()[:-1]
print "[openwebrx-config] External IP address detected:", server_hostname
"""

230
csdr.py
View File

@ -23,9 +23,11 @@ OpenWebRX csdr plugin: do the signal processing with csdr
import subprocess import subprocess
import time import time
import os import os
import code
import signal import signal
import fcntl import threading
import logging
logger = logging.getLogger(__name__)
class dsp: class dsp:
@ -45,7 +47,6 @@ class dsp:
self.fft_compression = "none" self.fft_compression = "none"
self.demodulator = "nfm" self.demodulator = "nfm"
self.name = "csdr" self.name = "csdr"
self.format_conversion = "csdr convert_u8_f"
self.base_bufsize = 512 self.base_bufsize = 512
self.nc_port = 4951 self.nc_port = 4951
self.csdr_dynamic_bufsize = False self.csdr_dynamic_bufsize = False
@ -62,6 +63,7 @@ class dsp:
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"]
self.secondary_pipe_names=["secondary_shift_pipe"] self.secondary_pipe_names=["secondary_shift_pipe"]
self.secondary_offset_freq = 1000 self.secondary_offset_freq = 1000
self.modification_lock = threading.Lock()
def chain(self,which): def chain(self,which):
if which in [ "dmr", "dstar", "nxdn", "ysf" ]: if which in [ "dmr", "dstar", "nxdn", "ysf" ]:
@ -71,7 +73,6 @@ class dsp:
any_chain_base="nc -v 127.0.0.1 {nc_port} | " 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_dynamic_bufsize: any_chain_base+="csdr setbuf {start_bufsize} | "
if self.csdr_through: any_chain_base+="csdr through | " if self.csdr_through: any_chain_base+="csdr through | "
any_chain_base+=self.format_conversion+(" | " if self.format_conversion!="" else "") ##"csdr flowcontrol {flowcontrol} auto 1.5 10 | "
if which == "fft": if which == "fft":
fft_chain_base = any_chain_base+"csdr fft_cc {fft_size} {fft_block_size} | " + \ fft_chain_base = any_chain_base+"csdr fft_cc {fft_size} {fft_block_size} | " + \
("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \ ("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \
@ -122,7 +123,7 @@ class dsp:
return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "") return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "")
elif which == "bpsk31": elif which == "bpsk31":
return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \ return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \
"csdr bandpass_fir_fft_cc $(csdr '=-(31.25)/{if_samp_rate}') $(csdr '=(31.25)/{if_samp_rate}') $(csdr '=31.25/{if_samp_rate}') | " + \ "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " + \
"csdr simple_agc_cc 0.001 0.5 | " + \ "csdr simple_agc_cc 0.001 0.5 | " + \
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \
@ -139,12 +140,12 @@ class dsp:
def secondary_bpf_cutoff(self): def secondary_bpf_cutoff(self):
if self.secondary_demodulator == "bpsk31": if self.secondary_demodulator == "bpsk31":
return (31.25/2) / self.if_samp_rate() return 31.25 / self.if_samp_rate()
return 0 return 0
def secondary_bpf_transition_bw(self): def secondary_bpf_transition_bw(self):
if self.secondary_demodulator == "bpsk31": if self.secondary_demodulator == "bpsk31":
return (31.25/2) / self.if_samp_rate() return 31.25 / self.if_samp_rate()
return 0 return 0
def secondary_samples_per_bits(self): def secondary_samples_per_bits(self):
@ -157,51 +158,43 @@ class dsp:
return 31.25 return 31.25
def start_secondary_demodulator(self): def start_secondary_demodulator(self):
if(not self.secondary_demodulator): return if not self.secondary_demodulator: return
print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate() logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate())
secondary_command_fft=self.secondary_chain("fft") secondary_command_fft=self.secondary_chain("fft")
secondary_command_demod=self.secondary_chain(self.secondary_demodulator) secondary_command_demod=self.secondary_chain(self.secondary_demodulator)
self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft) self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft)
secondary_command_fft=secondary_command_fft.format( \ secondary_command_fft=secondary_command_fft.format(
input_pipe=self.iqtee_pipe, \ input_pipe=self.iqtee_pipe,
secondary_fft_input_size=self.secondary_fft_size, \ secondary_fft_input_size=self.secondary_fft_size,
secondary_fft_size=self.secondary_fft_size, \ secondary_fft_size=self.secondary_fft_size,
secondary_fft_block_size=self.secondary_fft_block_size(), \ secondary_fft_block_size=self.secondary_fft_block_size(),
) )
secondary_command_demod=secondary_command_demod.format( \ secondary_command_demod=secondary_command_demod.format(
input_pipe=self.iqtee2_pipe, \ input_pipe=self.iqtee2_pipe,
secondary_shift_pipe=self.secondary_shift_pipe, \ secondary_shift_pipe=self.secondary_shift_pipe,
secondary_decimation=self.secondary_decimation(), \ secondary_decimation=self.secondary_decimation(),
secondary_samples_per_bits=self.secondary_samples_per_bits(), \ secondary_samples_per_bits=self.secondary_samples_per_bits(),
secondary_bpf_cutoff=self.secondary_bpf_cutoff(), \ secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), \ secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(),
if_samp_rate=self.if_samp_rate() if_samp_rate=self.if_samp_rate()
) )
print "[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft)
print "[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (demod) = %s", secondary_command_demod)
#code.interact(local=locals())
my_env=os.environ.copy() my_env=os.environ.copy()
#if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; #if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)" logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)")
self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes
print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)" #TODO digimodes logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") #TODO digimodes
self.secondary_processes_running = True self.secondary_processes_running = True
#open control pipes for csdr and send initialization data #open control pipes for csdr and send initialization data
# print "==========> 1"
if self.secondary_shift_pipe != None: #TODO digimodes 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 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 self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes
# print "==========> 4"
self.set_pipe_nonblocking(self.secondary_process_demod.stdout)
self.set_pipe_nonblocking(self.secondary_process_fft.stdout)
def set_secondary_offset_freq(self, value): def set_secondary_offset_freq(self, value):
self.secondary_offset_freq=value self.secondary_offset_freq=value
@ -212,8 +205,18 @@ class dsp:
def stop_secondary_demodulator(self): def stop_secondary_demodulator(self):
if self.secondary_processes_running == False: return if self.secondary_processes_running == False: return
self.try_delete_pipes(self.secondary_pipe_names) self.try_delete_pipes(self.secondary_pipe_names)
if self.secondary_process_fft: os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM) if self.secondary_process_fft:
if self.secondary_process_demod: os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM) try:
os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
except ProcessLookupError:
# been killed by something else, ignore
pass
if self.secondary_process_demod:
try:
os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
except ProcessLookupError:
# been killed by something else, ignore
pass
self.secondary_processes_running = False self.secondary_processes_running = False
def read_secondary_demod(self, size): def read_secondary_demod(self, size):
@ -244,8 +247,11 @@ class dsp:
if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2) if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2)
def set_samp_rate(self,samp_rate): def set_samp_rate(self,samp_rate):
#to change this, restart is required
self.samp_rate=samp_rate self.samp_rate=samp_rate
self.calculate_decimation()
if self.running: self.restart()
def calculate_decimation(self):
self.decimation=1 self.decimation=1
while self.samp_rate/(self.decimation+1)>=self.output_rate: while self.samp_rate/(self.decimation+1)>=self.output_rate:
self.decimation+=1 self.decimation+=1
@ -262,46 +268,48 @@ class dsp:
def set_output_rate(self,output_rate): def set_output_rate(self,output_rate):
self.output_rate=output_rate self.output_rate=output_rate
self.set_samp_rate(self.samp_rate) #as it depends on output_rate self.calculate_decimation()
def set_demodulator(self,demodulator): def set_demodulator(self,demodulator):
#to change this, restart is required if (self.demodulator == demodulator): return
self.demodulator=demodulator self.demodulator=demodulator
self.restart()
def get_demodulator(self): def get_demodulator(self):
return self.demodulator return self.demodulator
def set_fft_size(self,fft_size): def set_fft_size(self,fft_size):
#to change this, restart is required
self.fft_size=fft_size self.fft_size=fft_size
self.restart()
def set_fft_fps(self,fft_fps): def set_fft_fps(self,fft_fps):
#to change this, restart is required
self.fft_fps=fft_fps self.fft_fps=fft_fps
self.restart()
def set_fft_averages(self,fft_averages): def set_fft_averages(self,fft_averages):
#to change this, restart is required
self.fft_averages=fft_averages self.fft_averages=fft_averages
self.restart()
def fft_block_size(self): def fft_block_size(self):
if self.fft_averages == 0: return self.samp_rate/self.fft_fps if self.fft_averages == 0: return self.samp_rate/self.fft_fps
else: return self.samp_rate/self.fft_fps/self.fft_averages else: return self.samp_rate/self.fft_fps/self.fft_averages
def set_format_conversion(self,format_conversion):
self.format_conversion=format_conversion
def set_offset_freq(self,offset_freq): def set_offset_freq(self,offset_freq):
self.offset_freq=offset_freq self.offset_freq=offset_freq
if self.running: if self.running:
self.modification_lock.acquire()
self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate)) self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate))
self.shift_pipe_file.flush() self.shift_pipe_file.flush()
self.modification_lock.release()
def set_bpf(self,low_cut,high_cut): def set_bpf(self,low_cut,high_cut):
self.low_cut=low_cut self.low_cut=low_cut
self.high_cut=high_cut self.high_cut=high_cut
if self.running: if self.running:
self.modification_lock.acquire()
self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) ) self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) )
self.bpf_pipe_file.flush() self.bpf_pipe_file.flush()
self.modification_lock.release()
def get_bpf(self): def get_bpf(self):
return [self.low_cut, self.high_cut] return [self.low_cut, self.high_cut]
@ -309,13 +317,20 @@ class dsp:
def set_squelch_level(self, squelch_level): def set_squelch_level(self, squelch_level):
self.squelch_level=squelch_level self.squelch_level=squelch_level
if self.running: if self.running:
self.modification_lock.acquire()
self.squelch_pipe_file.write( "%g\n"%(float(self.squelch_level)) ) self.squelch_pipe_file.write( "%g\n"%(float(self.squelch_level)) )
self.squelch_pipe_file.flush() self.squelch_pipe_file.flush()
self.modification_lock.release()
def get_smeter_level(self): def get_smeter_level(self):
if self.running: if self.running:
line=self.smeter_pipe_file.readline() line=self.smeter_pipe_file.readline()
try:
return float(line[:-1]) return float(line[:-1])
except ValueError:
return 0
else:
time.sleep(1)
def get_metadata(self): def get_metadata(self):
if self.running and self.meta_pipe: if self.running and self.meta_pipe:
@ -332,9 +347,7 @@ class dsp:
return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate)) return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate))
def try_create_pipes(self, pipe_names, command_base): def try_create_pipes(self, pipe_names, command_base):
# print "try_create_pipes"
for pipe_name in pipe_names: for pipe_name in pipe_names:
# print "\t"+pipe_name
if "{"+pipe_name+"}" in command_base: if "{"+pipe_name+"}" in command_base:
setattr(self, pipe_name, self.pipe_base_path+pipe_name) setattr(self, pipe_name, self.pipe_base_path+pipe_name)
self.mkfifo(getattr(self, pipe_name)) self.mkfifo(getattr(self, pipe_name))
@ -346,122 +359,89 @@ class dsp:
pipe_path = getattr(self,pipe_name,None) pipe_path = getattr(self,pipe_name,None)
if pipe_path: if pipe_path:
try: os.unlink(pipe_path) try: os.unlink(pipe_path)
except Exception as e: print "[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e except Exception:
logger.exception("try_delete_pipes()")
def set_pipe_nonblocking(self, pipe):
flags = fcntl.fcntl(pipe, fcntl.F_GETFL)
fcntl.fcntl(pipe, fcntl.F_SETFL, flags | os.O_NONBLOCK)
def start(self): def start(self):
self.modification_lock.acquire()
if (self.running):
self.modification_lock.release()
return
self.running = True
command_base=self.chain(self.demodulator) command_base=self.chain(self.demodulator)
#create control pipes for csdr #create control pipes for csdr
self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self)) self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self))
# self.bpf_pipe = self.shift_pipe = self.squelch_pipe = self.smeter_pipe = None
self.try_create_pipes(self.pipe_names, command_base) self.try_create_pipes(self.pipe_names, command_base)
# if "{bpf_pipe}" in command_base:
# self.bpf_pipe=pipe_base_path+"bpf"
# self.mkfifo(self.bpf_pipe)
# if "{shift_pipe}" in command_base:
# self.shift_pipe=pipe_base_path+"shift"
# self.mkfifo(self.shift_pipe)
# if "{squelch_pipe}" in command_base:
# self.squelch_pipe=pipe_base_path+"squelch"
# self.mkfifo(self.squelch_pipe)
# if "{smeter_pipe}" in command_base:
# self.smeter_pipe=pipe_base_path+"smeter"
# self.mkfifo(self.smeter_pipe)
# if "{iqtee_pipe}" in command_base:
# self.iqtee_pipe=pipe_base_path+"iqtee"
# self.mkfifo(self.iqtee_pipe)
# if "{iqtee2_pipe}" in command_base:
# self.iqtee2_pipe=pipe_base_path+"iqtee2"
# self.mkfifo(self.iqtee2_pipe)
#run the command #run the command
command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, \ command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation,
last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, \ last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages,
bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), \ bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(),
flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, \ flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port,
squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe ) squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe )
print "[openwebrx-dsp-plugin:csdr] Command =",command logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command)
#code.interact(local=locals())
my_env=os.environ.copy() my_env=os.environ.copy()
if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
#set stdout to non-blocking to avoid blocking the main loop when no audio was decoded in digital modes def watch_thread():
self.set_pipe_nonblocking(self.process.stdout) rc = self.process.wait()
logger.debug("dsp thread ended with rc=%d", rc)
if (rc == 0 and self.running and not self.modification_lock.locked()):
logger.debug("restarting since rc = 0, self.running = true, and no modification")
self.restart()
self.running = True threading.Thread(target = watch_thread).start()
#open control pipes for csdr and send initialization data # open control pipes for csdr
if self.bpf_pipe != None: if self.bpf_pipe != None:
self.bpf_pipe_file=open(self.bpf_pipe,"w") self.bpf_pipe_file=open(self.bpf_pipe,"w")
self.set_bpf(self.low_cut,self.high_cut) if self.shift_pipe:
if self.shift_pipe != None:
self.shift_pipe_file=open(self.shift_pipe,"w") self.shift_pipe_file=open(self.shift_pipe,"w")
self.set_offset_freq(self.offset_freq) if self.squelch_pipe:
if self.squelch_pipe != None:
self.squelch_pipe_file=open(self.squelch_pipe,"w") 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.start_secondary_demodulator() self.start_secondary_demodulator()
self.modification_lock.release()
# 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")
if self.meta_pipe != None:
self.meta_pipe_file=open(self.meta_pipe,"r")
def read(self,size): def read(self,size):
return self.process.stdout.read(size) return self.process.stdout.read(size)
def read_async(self, size):
try:
return self.process.stdout.read(size)
except IOError:
return None
def stop(self): def stop(self):
self.modification_lock.acquire()
self.running = False
if hasattr(self, "process"):
try:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
except ProcessLookupError:
# been killed by something else, ignore
pass
self.stop_secondary_demodulator() self.stop_secondary_demodulator()
#if(self.process.poll()!=None):return # returns None while subprocess is running
#while(self.process.poll()==None):
# #self.process.kill()
# print "killproc",os.getpgid(self.process.pid),self.process.pid
# os.killpg(self.process.pid, signal.SIGTERM)
#
# time.sleep(0.1)
self.try_delete_pipes(self.pipe_names) self.try_delete_pipes(self.pipe_names)
# if self.bpf_pipe: self.modification_lock.release()
# try: os.unlink(self.bpf_pipe)
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe
# if self.shift_pipe:
# try: os.unlink(self.shift_pipe)
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.shift_pipe
# if self.squelch_pipe:
# try: os.unlink(self.squelch_pipe)
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.squelch_pipe
# if self.smeter_pipe:
# try: os.unlink(self.smeter_pipe)
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.smeter_pipe
# if self.iqtee_pipe:
# try: os.unlink(self.iqtee_pipe)
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee_pipe
# if self.iqtee2_pipe:
# try: os.unlink(self.iqtee2_pipe)
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee2_pipe
self.running = False
def restart(self): def restart(self):
if not self.running: return
self.stop() self.stop()
self.start() self.start()

View File

@ -22,60 +22,45 @@
<html> <html>
<head> <head>
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title> <title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
<script type="text/javascript"> <script src="static/sdr.js"></script>
//Global variables <script src="static/mathbox-bundle.min.js"></script>
var client_id="%[CLIENT_ID]"; <script src="static/openwebrx.js"></script>
var ws_url="%[WS_URL]"; <script src="static/jquery-3.2.1.min.js"></script>
var rx_photo_height=%[RX_PHOTO_HEIGHT]; <script src="static/jquery.nanoscroller.js"></script>
var audio_buffering_fill_to=%[AUDIO_BUFSIZE]; <link rel="stylesheet" type="text/css" href="static/nanoscroller.css" />
var starting_mod="%[START_MOD]"; <link rel="stylesheet" type="text/css" href="static/openwebrx.css" />
var starting_offset_frequency = %[START_OFFSET_FREQ];
var waterfall_colors=%[WATERFALL_COLORS];
var waterfall_min_level_default=%[WATERFALL_MIN_LEVEL];
var waterfall_max_level_default=%[WATERFALL_MAX_LEVEL];
var waterfall_auto_level_margin=%[WATERFALL_AUTO_LEVEL_MARGIN];
var server_enable_digimodes=%[DIGIMODES_ENABLE];
var mathbox_waterfall_frequency_resolution=%[MATHBOX_WATERFALL_FRES];
var mathbox_waterfall_history_length=%[MATHBOX_WATERFALL_THIST];
var mathbox_waterfall_colors=%[MATHBOX_WATERFALL_COLORS];
</script>
<script src="sdr.js"></script>
<script src="mathbox-bundle.min.js"></script>
<script src="openwebrx.js"></script>
<script src="jquery-3.2.1.min.js"></script>
<script src="jquery.nanoscroller.js"></script>
<link rel="stylesheet" type="text/css" href="nanoscroller.css" />
<link rel="stylesheet" type="text/css" href="openwebrx.css" />
<meta charset="utf-8"> <meta charset="utf-8">
</head> </head>
<body onload="openwebrx_init();"> <body onload="openwebrx_init();">
<div id="webrx-page-container"> <div id="webrx-page-container">
<div id="webrx-top-container"> <div id="webrx-top-container">
<div id="webrx-top-photo-clip"> <div id="webrx-top-photo-clip">
<img src="gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/> <img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/>
<div id="webrx-rx-photo-title">%[RX_PHOTO_TITLE]</div>
<div id="webrx-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"> <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="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="gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-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>
<img id="webrx-rx-avatar-background" src="gfx/openwebrx-avatar-background.png" onclick="toggle_rx_photo();"/> <div id="webrx-rx-avatar-background">
<img id="webrx-rx-avatar" src="gfx/openwebrx-avatar.png" onclick="toggle_rx_photo();"/> <img id="webrx-rx-avatar" src="static/gfx/openwebrx-avatar.png" onclick="toggle_rx_photo();"/>
<div id="webrx-rx-title" onclick="toggle_rx_photo();">%[RX_TITLE]</div> </div>
<div id="webrx-rx-desc" onclick="toggle_rx_photo();">%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, <a href="https://www.google.hu/maps/place/%[RX_GPS]" target="_blank" onclick="dont_toggle_rx_photo();">[maps]</a></div> <div id="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"> <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-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="gfx/openwebrx-rx-details-arrow.png" /></a> <a id="openwebrx-rx-details-arrow-down" onclick="toggle_rx_photo();"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a>
</div> </div>
<section id="openwebrx-main-buttons"> <section id="openwebrx-main-buttons">
<ul> <ul>
<li onmouseup="toggle_panel('openwebrx-panel-status');"><img src="gfx/openwebrx-panel-status.png" /><br/>Status</li> <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="gfx/openwebrx-panel-log.png" /><br/>Log</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="gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li> <li onmouseup="toggle_panel('openwebrx-panel-receiver');"><img src="static/gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li>
</ul> </ul>
</section> </section>
</div> </div>
<div id="webrx-rx-photo-title"></div>
<div id="webrx-rx-photo-desc"></div>
</div>
</div> </div>
<div id="webrx-main-container"> <div id="webrx-main-container">
<div id="openwebrx-scale-container"> <div id="openwebrx-scale-container">
@ -90,6 +75,10 @@
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" data-panel-pos="right" data-panel-order="0" data-panel-size="259,115"> <div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" data-panel-pos="right" data-panel-order="0" data-panel-size="259,115">
<div id="webrx-actual-freq">---.--- MHz</div> <div id="webrx-actual-freq">---.--- MHz</div>
<div id="webrx-mouse-freq">---.--- MHz</div> <div id="webrx-mouse-freq">---.--- MHz</div>
<div class="openwebrx-panel-line">
<select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();">
</select>
</div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm" <div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
onclick="demodulator_analog_replace('nfm');">FM</div> onclick="demodulator_analog_replace('nfm');">FM</div>
@ -118,23 +107,23 @@
</select> </select>
</div> </div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div> <div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()"> <input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
<div title="Auto-adjust waterfall colors" id="openwebrx-waterfall-colors-auto" class="openwebrx-button" onclick="waterfall_measure_minmax_now=true;"><img src="gfx/openwebrx-waterfall-auto.png" class="openwebrx-sliderbtn-img"></div> <div title="Auto-adjust waterfall colors" id="openwebrx-waterfall-colors-auto" class="openwebrx-button" onclick="waterfall_measure_minmax_now=true;"><img src="static/gfx/openwebrx-waterfall-auto.png" class="openwebrx-sliderbtn-img"></div>
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()"> <input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">
</div> </div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">
<div title="Auto-set squelch level" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><img src="gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div> <div title="Auto-set squelch level" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
<input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()"> <input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()">
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div> <div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="static/gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div>
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()"> <input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
</div> </div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInOneStep();" title="Zoom in one step"> <img src="gfx/openwebrx-zoom-in.png" /></div> <div class="openwebrx-button openwebrx-square-button" onclick="zoomInOneStep();" title="Zoom in one step"> <img src="static/gfx/openwebrx-zoom-in.png" /></div>
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutOneStep();" title="Zoom out one step"> <img src="gfx/openwebrx-zoom-out.png" /></div> <div class="openwebrx-button openwebrx-square-button" onclick="zoomOutOneStep();" title="Zoom out one step"> <img src="static/gfx/openwebrx-zoom-out.png" /></div>
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInTotal();" title="Zoom in totally"><img src="gfx/openwebrx-zoom-in-total.png" /></div> <div class="openwebrx-button openwebrx-square-button" onclick="zoomInTotal();" title="Zoom in totally"><img src="static/gfx/openwebrx-zoom-in-total.png" /></div>
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutTotal();" title="Zoom out totally"><img src="gfx/openwebrx-zoom-out-total.png" /></div> <div class="openwebrx-button openwebrx-square-button" onclick="zoomOutTotal();" title="Zoom out totally"><img src="static/gfx/openwebrx-zoom-out-total.png" /></div>
<div class="openwebrx-button openwebrx-square-button" onclick="mathbox_toggle();" title="Toggle 3D view"><img src="gfx/openwebrx-3d-spectrum.png" /></div> <div class="openwebrx-button openwebrx-square-button" onclick="mathbox_toggle();" title="Toggle 3D view"><img src="static/gfx/openwebrx-3d-spectrum.png" /></div>
<div id="openwebrx-smeter-db">0 dB</div> <div id="openwebrx-smeter-db">0 dB</div>
</div> </div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">
@ -160,7 +149,7 @@
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div>
</div> </div>
<div class="openwebrx-panel" data-panel-name="client-under-devel" data-panel-pos="none" data-panel-order="0" data-panel-size="245,55" style="background-color: Red;"> <div class="openwebrx-panel" data-panel-name="client-under-devel" data-panel-pos="left" data-panel-order="0" data-panel-size="245,55" style="background-color: Red;">
<span style="font-size: 15pt; font-weight: bold;">Under construction</span> <span style="font-size: 15pt; font-weight: bold;">Under construction</span>
<br />We're working on the code right now, so the application might fail. <br />We're working on the code right now, so the application might fail.
</div> </div>
@ -182,7 +171,7 @@
</div> </div>
<div id="openwebrx-big-grey" onclick="iosPlayButtonClick();"> <div id="openwebrx-big-grey" onclick="iosPlayButtonClick();">
<div id="openwebrx-play-button-text"> <div id="openwebrx-play-button-text">
<img id="openwebrx-play-button" src="gfx/openwebrx-play-button.png" /> <img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.png" />
<br /><br />Start OpenWebRX <br /><br />Start OpenWebRX
</div> </div>
</div> </div>

View File

@ -155,22 +155,12 @@ input[type=range]:focus::-ms-fill-upper
.webrx-top-bar-parts .webrx-top-bar-parts
{ {
position: absolute;
top: 0px;
left: 0px;
width:100%;
height:67px; height:67px;
} }
#webrx-top-bar-background
{
background-color: #808080;
opacity: 0.15;
filter:alpha(opacity=15);
}
#webrx-top-bar #webrx-top-bar
{ {
background: rgba(128, 128, 128, 0.15);
margin:0; margin:0;
padding:0; padding:0;
user-select: none; user-select: none;
@ -179,20 +169,23 @@ input[type=range]:focus::-ms-fill-upper
-khtml-user-select: none; -khtml-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
overflow: hidden;
position: absolute;
left: 0;
top: 0;
right: 0;
} }
#webrx-top-logo #webrx-top-logo
{ {
position: absolute; padding: 12px;
top: 12px; float: left;
left: 15px;
} }
#webrx-ha5kfu-top-logo #webrx-ha5kfu-top-logo
{ {
position: absolute; float: right;
top: 15px; padding: 15px;
right: 15px;
} }
#webrx-top-photo #webrx-top-photo
@ -204,79 +197,37 @@ input[type=range]:focus::-ms-fill-upper
#webrx-rx-avatar-background #webrx-rx-avatar-background
{ {
cursor:pointer; cursor:pointer;
position: absolute; background-image: url(gfx/openwebrx-avatar-background.png);
left: 285px; background-origin: content-box;
top: 6px; background-repeat: no-repeat;
float: left;
width: 54px;
height: 54px;
padding: 7px;
} }
#webrx-rx-avatar #webrx-rx-avatar
{ {
cursor:pointer; cursor:pointer;
position: absolute;
left: 289px;
top: 10px;
width: 46px; width: 46px;
height: 46px; height: 46px;
padding: 4px;
} }
#webrx-top-photo-clip #webrx-top-photo-clip
{ {
min-height: 67px;
max-height: 350px; max-height: 350px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
/*#webrx-bottom-bar
{
position: absolute;
bottom: 0px;
width: 100%;
height: 117px;
background-image:url(gfx/webrx-bottom-bar.png);
}*/
#webrx-page-container #webrx-page-container
{ {
min-height:100%; min-height:100%;
position:relative; position:relative;
} }
/*#webrx-photo-gradient-left
{
position: absolute;
bottom: 0px;
left: 0px;
background-image:url(gfx/webrx-photo-gradient-corner.png);
width: 59px;
height: 92px;
}
#webrx-photo-gradient-middle
{
position: absolute;
bottom: 0px;
left: 59px;
right: 59px;
height: 92px;
background-image:url(gfx/webrx-photo-gradient-middle.png);
}
#webrx-photo-gradient-right
{
position: absolute;
bottom: 0px;
right: 0px;
background-image:url(gfx/webrx-photo-gradient-corner.png);
width: 59px;
height: 92px;
-webkit-transform:scaleX(-1);
-moz-transform:scaleX(-1);
-ms-transform:scaleX(-1);
-o-transform:scaleX(-1);
transform:scaleX(-1);
}*/
#webrx-rx-photo-title #webrx-rx-photo-title
{ {
position: absolute; position: absolute;
@ -303,10 +254,17 @@ input[type=range]:focus::-ms-fill-upper
#webrx-rx-photo-desc a #webrx-rx-photo-desc a
{ {
/*color: #007df1;*/
color: #5ca8ff; color: #5ca8ff;
text-shadow: none; text-shadow: none;
/*text-shadow: 0px 0px 7px #fff;*/ }
#webrx-rx-texts {
float: left;
padding: 10px;
}
#webrx-rx-texts div {
padding: 3px;
} }
#webrx-rx-title #webrx-rx-title
@ -314,9 +272,6 @@ input[type=range]:focus::-ms-fill-upper
white-space:nowrap; white-space:nowrap;
overflow: hidden; overflow: hidden;
cursor:pointer; cursor:pointer;
position: absolute;
left: 350px;
top: 13px;
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
color: #909090; color: #909090;
font-size: 11pt; font-size: 11pt;
@ -330,15 +285,11 @@ input[type=range]:focus::-ms-fill-upper
cursor:pointer; cursor:pointer;
font-size: 10pt; font-size: 10pt;
color: #909090; color: #909090;
position: absolute;
left: 350px;
top: 34px;
} }
#webrx-rx-desc a #webrx-rx-desc a
{ {
color: #909090; color: #909090;
/*text-decoration: none;*/
} }
#openwebrx-rx-details-arrow #openwebrx-rx-details-arrow
@ -718,9 +669,7 @@ img.openwebrx-mirror-img
#openwebrx-main-buttons #openwebrx-main-buttons
{ {
position: absolute; float: right;
right: 133px;
top: 3px;
margin:0; margin:0;
color: white; color: white;
text-shadow: 0px 0px 4px #000000; text-shadow: 0px 0px 4px #000000;
@ -841,10 +790,7 @@ img.openwebrx-mirror-img
transition: width 500ms, left 500ms; transition: width 500ms, left 500ms;
} }
#openwebrx-secondary-demod-listbox .openwebrx-panel select {
{
width: 201px;
height: 27px;
border-radius: 5px; border-radius: 5px;
background-color: #373737; background-color: #373737;
color: White; color: White;
@ -856,16 +802,27 @@ img.openwebrx-mirror-img
border-color: transparent; border-color: transparent;
border-width: 0px; border-width: 0px;
-moz-appearance: none; -moz-appearance: none;
padding-left:3px;
} }
#openwebrx-secondary-demod-listbox option .openwebrx-panel select option {
{
border-width: 0px; border-width: 0px;
background-color: #373737; background-color: #373737;
color: White; color: White;
} }
#openwebrx-secondary-demod-listbox
{
width: 201px;
height: 27px;
padding-left:3px;
}
#openwebrx-sdr-profiles-listbox {
width: 100%;
font-size: 10pt;
height: 27px;
}
#openwebrx-cursor-blink #openwebrx-cursor-blink
{ {
animation: cursor-blink 1s infinite; animation: cursor-blink 1s infinite;

View File

@ -52,6 +52,7 @@ var waterfall_setup_done=0;
var waterfall_queue = []; var waterfall_queue = [];
var waterfall_timer; var waterfall_timer;
var secondary_fft_size; var secondary_fft_size;
var audio_allowed;
/*function fade(something,from,to,time_ms,fps) /*function fade(something,from,to,time_ms,fps)
{ {
@ -79,7 +80,9 @@ is_chrome = /Chrome/.test(navigator.userAgent);
function init_rx_photo() function init_rx_photo()
{ {
e("webrx-top-photo-clip").style.maxHeight=rx_photo_height.toString()+"px"; var clip = e("webrx-top-photo-clip");
rx_photo_height = clip.clientHeight
clip.style.maxHeight=rx_photo_height+"px";
window.setTimeout(function() { animate(e("webrx-rx-photo-title"),"opacity","",1,0,1,500,30); },1000); window.setTimeout(function() { animate(e("webrx-rx-photo-title"),"opacity","",1,0,1,500,30); },1000);
window.setTimeout(function() { animate(e("webrx-rx-photo-desc"),"opacity","",1,0,1,500,30); },1500); window.setTimeout(function() { animate(e("webrx-rx-photo-desc"),"opacity","",1,0,1,500,30); },1500);
window.setTimeout(function() { close_rx_photo() },2500); window.setTimeout(function() { close_rx_photo() },2500);
@ -133,14 +136,14 @@ function toggleMute()
if (mute) { if (mute) {
mute = false; mute = false;
e("openwebrx-mute-on").id="openwebrx-mute-off"; e("openwebrx-mute-on").id="openwebrx-mute-off";
e("openwebrx-mute-img").src="gfx/openwebrx-speaker.png"; e("openwebrx-mute-img").src="static/gfx/openwebrx-speaker.png";
e("openwebrx-panel-volume").disabled=false; e("openwebrx-panel-volume").disabled=false;
e("openwebrx-panel-volume").style.opacity=1.0; e("openwebrx-panel-volume").style.opacity=1.0;
e("openwebrx-panel-volume").value = volumeBeforeMute; e("openwebrx-panel-volume").value = volumeBeforeMute;
} else { } else {
mute = true; mute = true;
e("openwebrx-mute-off").id="openwebrx-mute-on"; e("openwebrx-mute-off").id="openwebrx-mute-on";
e("openwebrx-mute-img").src="gfx/openwebrx-speaker-muted.png"; e("openwebrx-mute-img").src="static/gfx/openwebrx-speaker-muted.png";
e("openwebrx-panel-volume").disabled=true; e("openwebrx-panel-volume").disabled=true;
e("openwebrx-panel-volume").style.opacity=0.5; e("openwebrx-panel-volume").style.opacity=0.5;
volumeBeforeMute = e("openwebrx-panel-volume").value; volumeBeforeMute = e("openwebrx-panel-volume").value;
@ -160,7 +163,7 @@ function updateSquelch()
{ {
var sliderValue=parseInt(e("openwebrx-panel-squelch").value); var sliderValue=parseInt(e("openwebrx-panel-squelch").value);
var outputValue=(sliderValue==parseInt(e("openwebrx-panel-squelch").min))?0:getLinearSmeterValue(sliderValue); var outputValue=(sliderValue==parseInt(e("openwebrx-panel-squelch").min))?0:getLinearSmeterValue(sliderValue);
ws.send("SET squelch_level="+outputValue.toString()); ws.send(JSON.stringify({"type":"dspcontrol","params":{"squelch_level":outputValue}}));
} }
function updateWaterfallColors(which) function updateWaterfallColors(which)
@ -470,9 +473,13 @@ function demodulator_default_analog(offset_frequency,subtype)
this.doset=function(first_time) this.doset=function(first_time)
{ //this function sends demodulator parameters to the server { //this function sends demodulator parameters to the server
ws.send("SET"+((first_time)?" mod="+this.server_mod:"")+ params = {
" low_cut="+this.low_cut.toString()+" high_cut="+this.high_cut.toString()+ "low_cut": this.low_cut,
" offset_freq="+this.offset_frequency.toString()); "high_cut": this.high_cut,
"offset_freq": this.offset_frequency
};
if (first_time) params.mod = this.server_mod;
ws.send(JSON.stringify({"type":"dspcontrol","params":params}));
} }
this.doset(true); //we set parameters on object creation this.doset(true); //we set parameters on object creation
@ -1155,169 +1162,140 @@ var COMPRESS_FFT_PAD_N=10; //should be the same as in csdr.c
function on_ws_recv(evt) function on_ws_recv(evt)
{ {
if(!(evt.data instanceof ArrayBuffer)) { divlog("on_ws_recv(): Not ArrayBuffer received...",1); return; } if (typeof evt.data == 'string') {
// // text messages
debug_ws_data_received+=evt.data.byteLength/1000; debug_ws_data_received += evt.data.length / 1000;
first4Chars=getFirstChars(evt.data,4);
first3Chars=first4Chars.slice(0,3);
if(first3Chars=="CLI")
{
var stringData=arrayBufferToString(evt.data);
if(stringData.substring(0,16)=="CLIENT DE SERVER") divlog("Server acknowledged WebSocket connection.");
if (evt.data.substr(0, 16) == "CLIENT DE SERVER") {
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;
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;
waterfall_init();
audio_preinit();
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 "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/100,"Server CPU [" + server_cpu_usage + "%]",server_cpu_usage>85);
break;
case "clients":
var clients = json.value;
progressbar_set(e("openwebrx-bar-clients"), clients / max_clients_num, "Clients [" + clients + "]", clients > 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;
default:
console.warn('received message of unknown type: ' + json.type);
} }
if(first3Chars=="AUD") } 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;
type = new Uint8Array(evt.data, 0, 1)[0]
data = evt.data.slice(1)
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 2:
// audio data
var audio_data; var audio_data;
if(audio_compression=="adpcm") audio_data=new Uint8Array(evt.data,4) if (audio_compression=="adpcm") {
else audio_data=new Int16Array(evt.data,4); audio_data = new Uint8Array(data);
} else {
audio_data = new Int16Array(data);
}
audio_prepare(audio_data); audio_prepare(audio_data);
audio_buffer_current_size_debug += audio_data.length; audio_buffer_current_size_debug += audio_data.length;
audio_buffer_all_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() if (!(ios||is_chrome) && (audio_initialized==0 && audio_prepared_buffers.length>audio_buffering_fill_to)) audio_init()
} break;
else if(first3Chars=="FFT") case 3:
{ // secondary FFT
//alert("Yupee! Doing FFT"); if (fft_compression == "none") {
//if(first4Chars=="FFTS") console.log("FFTS"); secondary_demod_waterfall_add_queue(new Float32Array(data));
if(fft_compression=="none") waterfall_add_queue(new Float32Array(evt.data,4)); } else if (fft_compression == "adpcm") {
else if(fft_compression="adpcm")
{
fft_codec.reset(); fft_codec.reset();
var waterfall_i16=fft_codec.decode(new Uint8Array(evt.data,4)); var waterfall_i16=fft_codec.decode(new Uint8Array(data));
var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N); 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; 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 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":
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';
});
var meta = {};
stringData.substr(4).split(";").forEach(function(s) {
var item = s.split(":");
meta[item[0]] = item[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;
};
} }
break; break;
case 'YSF': case 4:
var strings = []; // secondary demod
if (meta.source) strings.push("Source: " + meta.source); secondary_demod_push_data(arrayBufferToString(data));
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; break;
default:
console.warn('unknown type of binary message: ' + type)
} }
metaPanels.forEach(update);
} }
} }
function add_problem(what) function add_problem(what)
@ -1630,7 +1608,9 @@ function audio_flush_notused()
function webrx_set_param(what, value) function webrx_set_param(what, value)
{ {
ws.send("SET "+what+"="+value.toString()); params = {};
params[what] = value;
ws.send(JSON.stringify({"type":"dspcontrol","params":params}));
} }
var starting_mute = false; var starting_mute = false;
@ -1680,6 +1660,7 @@ function audio_preinit()
else if(audio_context.sampleRate>44100*4) else if(audio_context.sampleRate>44100*4)
audio_buffer_size = 4096 * 4; audio_buffer_size = 4096 * 4;
if (!audio_rebuffer) {
audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED); audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED);
audio_last_output_buffer = new Float32Array(audio_buffer_size); audio_last_output_buffer = new Float32Array(audio_buffer_size);
@ -1688,8 +1669,9 @@ function audio_preinit()
audio_calculate_resampling(audio_context.sampleRate); audio_calculate_resampling(audio_context.sampleRate);
audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1); audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1);
ws.send("SET output_rate="+audio_server_output_rate.toString()+" action=start"); //now we'll get AUD packets as well }
ws.send(JSON.stringify({"type":"dspcontrol","action":"start","params":{"output_rate":audio_server_output_rate}}));
} }
function audio_init() function audio_init()
@ -1751,7 +1733,10 @@ function on_ws_closed()
audio_node.disconnect(); audio_node.disconnect();
} }
catch (dont_care) {} catch (dont_care) {}
divlog("WebSocket has closed unexpectedly. Please reload the page.", 1); audio_initialized = 0;
divlog("WebSocket has closed unexpectedly. Attempting to reconnect in 5 seconds...", 1);
setTimeout(open_websocket, 5000);
} }
function on_ws_error(event) function on_ws_error(event)
@ -1763,14 +1748,10 @@ String.prototype.startswith=function(str){ return this.indexOf(str) == 0; }; //h
function open_websocket() function open_websocket()
{ {
//if(ws_url.startswith("ws://localhost:")&&window.location.hostname!="127.0.0.1"&&window.location.hostname!="localhost")
//{
//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 ws_url="ws://"+(window.location.origin.split("://")[1])+"/ws/"; //guess automatically -> now default behaviour
//}
if (!("WebSocket" in window)) if (!("WebSocket" in window))
divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser."); divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser.");
ws = new WebSocket(ws_url+client_id); ws = new WebSocket(ws_url);
ws.onopen = on_ws_opened; ws.onopen = on_ws_opened;
ws.onmessage = on_ws_recv; ws.onmessage = on_ws_recv;
ws.onclose = on_ws_closed; ws.onclose = on_ws_closed;
@ -2023,18 +2004,14 @@ function waterfall_add(data)
waterfall_image.data[base+x*4+i] = ((color>>>0)>>((3-i)*8))&0xff; waterfall_image.data[base+x*4+i] = ((color>>>0)>>((3-i)*8))&0xff;
}*/ }*/
if(mathbox_mode==MATHBOX_MODES.WATERFALL) if (mathbox_mode==MATHBOX_MODES.WATERFALL) {
{
//Handle mathbox //Handle mathbox
for(var i=0;i<fft_size;i++) mathbox_data[i+mathbox_data_index*fft_size]=data[i]; for(var i=0;i<fft_size;i++) mathbox_data[i+mathbox_data_index*fft_size]=data[i];
mathbox_shift(); mathbox_shift();
} } else {
else
{
//Add line to waterfall image //Add line to waterfall image
oneline_image = canvas_context.createImageData(w,1); oneline_image = canvas_context.createImageData(w,1);
for(x=0;x<w;x++) for (x=0;x<w;x++) {
{
color=waterfall_mkcolor(data[x]); color=waterfall_mkcolor(data[x]);
for(i=0;i<4;i++) for(i=0;i<4;i++)
oneline_image.data[x*4+i] = ((color>>>0)>>((3-i)*8))&0xff; oneline_image.data[x*4+i] = ((color>>>0)>>((3-i)*8))&0xff;
@ -2272,7 +2249,7 @@ function openwebrx_init()
//Synchronise volume with slider //Synchronise volume with slider
updateVolume(); updateVolume();
waterfallColorsDefault();
} }
function iosPlayButtonClick() function iosPlayButtonClick()
@ -2281,6 +2258,7 @@ function iosPlayButtonClick()
audio_init(); audio_init();
e("openwebrx-big-grey").style.opacity=0; e("openwebrx-big-grey").style.opacity=0;
window.setTimeout(function(){ e("openwebrx-big-grey").style.display="none"; },1100); window.setTimeout(function(){ e("openwebrx-big-grey").style.display="none"; },1100);
audio_allowed = 1;
} }
/* /*
@ -2616,18 +2594,18 @@ function secondary_demod_init()
function secondary_demod_start(subtype) function secondary_demod_start(subtype)
{ {
secondary_demod_canvases_initialized = false; secondary_demod_canvases_initialized = false;
ws.send("SET secondary_mod="+subtype); ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":subtype}}));
secondary_demod = subtype; secondary_demod = subtype;
} }
function secondary_demod_set() function secondary_demod_set()
{ {
ws.send("SET secondary_offset_freq="+secondary_demod_offset_freq.toString()); ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_offset_freq":secondary_demod_offset_freq}}));
} }
function secondary_demod_stop() function secondary_demod_stop()
{ {
ws.send("SET secondary_mod=off"); ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":false}}));
secondary_demod = false; secondary_demod = false;
secondary_demod_waterfall_queue = []; secondary_demod_waterfall_queue = [];
} }
@ -2755,7 +2733,7 @@ function secondary_demod_update_channel_freq_from_event(evt)
{ {
secondary_demod_waiting_for_set = true; secondary_demod_waiting_for_set = true;
window.setTimeout(()=>{ window.setTimeout(()=>{
ws.send("SET secondary_offset_freq="+Math.floor(secondary_demod_channel_freq)); ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_offset_freq":Math.floor(secondary_demod_channel_freq)}}));
//console.log("doneset:", secondary_demod_channel_freq); //console.log("doneset:", secondary_demod_channel_freq);
secondary_demod_waiting_for_set = false; secondary_demod_waiting_for_set = false;
}, 50); }, 50);
@ -2815,3 +2793,8 @@ function secondary_demod_waterfall_set_zoom(low_cut, high_cut)
secondary_demod_canvases.map((x)=>{$(x).css("left",secondary_demod_canvas_left+"px").css("width",secondary_demod_canvas_width+"px");}); secondary_demod_canvases.map((x)=>{$(x).css("left",secondary_demod_canvas_left+"px").css("width",secondary_demod_canvas_width+"px");});
secondary_demod_update_channel_freq_from_event(); secondary_demod_update_channel_freq_from_event();
} }
function sdr_profile_changed() {
value = $('#openwebrx-sdr-profiles-listbox').val();
ws.send(JSON.stringify({ type:"selectprofile", params:{ profile:value }}));
}

769
openwebrx.py Executable file → Normal file
View File

@ -1,757 +1,52 @@
#!/usr/bin/python2 from http.server import HTTPServer
print "" # python2.7 is required to run OpenWebRX instead of python3. Please run me by: python2 openwebrx.py from owrx.http import RequestHandler
""" from owrx.config import PropertyManager
from owrx.feature import FeatureDetector
from owrx.source import SdrService, ClientRegistry
from socketserver import ThreadingMixIn
from owrx.sdrhu import SdrHuUpdater
This file is part of OpenWebRX, import logging
an open-source SDR receiver software with a web UI. logging.basicConfig(level = logging.DEBUG, format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
sw_version="v0.17"
#0.15 (added nmux)
import os
import code
import importlib
import csdr
import thread
import time
import datetime
import subprocess
import os
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from SocketServer import ThreadingMixIn
import fcntl
import time
import md5
import random
import threading
import sys
import traceback
from collections import namedtuple
import Queue
import ctypes
#import rtl_mus
import rxws
import uuid
import signal
import socket
try: import sdrhu
except: sdrhu=False
avatar_ctime=""
#pypy compatibility
try: import dl
except: pass
try: import __pypy__
except: pass
pypy="__pypy__" in globals()
"""
def import_all_plugins(directory):
for subdir in os.listdir(directory):
if os.path.isdir(directory+subdir) and not subdir[0]=="_":
exact_path=directory+subdir+"/plugin.py"
if os.path.isfile(exact_path):
importname=(directory+subdir+"/plugin").replace("/",".")
print "[openwebrx-import] Found plugin:",importname
importlib.import_module(importname)
"""
class MultiThreadHTTPServer(ThreadingMixIn, HTTPServer):
pass pass
def handle_signal(sig, frame):
global spectrum_dsp
if sig == signal.SIGUSR1:
print "[openwebrx] Verbose status information on USR1 signal"
print
print "time.time() =", time.time()
print "clients_mutex.locked() =", clients_mutex.locked()
print "clients_mutex_locker =", clients_mutex_locker
if server_fail: print "server_fail = ", server_fail
print "spectrum_thread_watchdog_last_tick =", spectrum_thread_watchdog_last_tick
print
print "clients:",len(clients)
for client in clients:
print
for key in client._fields:
print "\t%s = %s"%(key,str(getattr(client,key)))
elif sig == signal.SIGUSR2:
code.interact(local=globals())
else:
print "[openwebrx] Ctrl+C: aborting."
cleanup_clients(True)
spectrum_dsp.stop()
os._exit(1) #not too graceful exit
def access_log(data):
global logs
logs.access_log.write("["+datetime.datetime.now().isoformat()+"] "+data+"\n")
logs.access_log.flush()
receiver_failed=spectrum_thread_watchdog_last_tick=rtl_thread=spectrum_dsp=server_fail=None
def main(): def main():
global clients, clients_mutex, pypy, lock_try_time, avatar_ctime, cfg, logs print("""
global serverfail, rtl_thread
print
print "OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package"
print "_________________________________________________________________________________________________"
print
print "Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>"
print
no_arguments=len(sys.argv)==1 OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package
if no_arguments: print "[openwebrx-main] Configuration script not specified. I will use: \"config_webrx.py\"" _________________________________________________________________________________________________
cfg=__import__("config_webrx" if no_arguments else sys.argv[1])
for option in ("access_log","csdr_dynamic_bufsize","csdr_print_bufsizes","csdr_through"):
if not option in dir(cfg): setattr(cfg, option, False) #initialize optional config parameters
#Open log files Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>
logs = type("logs_class", (object,), {"access_log":open(cfg.access_log if cfg.access_log else "/dev/null","a"), "error_log":""})()
#Set signal handler """)
signal.signal(signal.SIGINT, handle_signal) #http://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python
signal.signal(signal.SIGUSR1, handle_signal)
signal.signal(signal.SIGUSR2, handle_signal)
#Pypy pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
if pypy: print "pypy detected (and now something completely different: c code is expected to run at a speed of 3*10^8 m/s?)"
#Change process name to "openwebrx" (to be seen in ps) featureDetector = FeatureDetector()
try: if not featureDetector.is_available("core"):
for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]: print("you are missing required dependencies to run openwebrx. "
if os.path.exists(libcpath): "please check that the following core requirements are installed:")
libc = dl.open(libcpath) print(", ".join(featureDetector.get_requirements("core")))
libc.call("prctl", 15, "openwebrx", 0, 0, 0)
break
except:
pass
#Start rtl thread
if os.system("csdr 2> /dev/null") == 32512: #check for csdr
print "[openwebrx-main] You need to install \"csdr\" to run OpenWebRX!\n"
return
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
#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()
#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 return
def check_server(): # Get error messages about unknown / unavailable features as soon as possible
global spectrum_dsp, server_fail, rtl_thread SdrService.loadProps()
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): if "sdrhu_key" in pm and pm["sdrhu_public_listing"]:
dsp.csdr_dynamic_bufsize = cfg.csdr_dynamic_bufsize updater = SdrHuUpdater()
dsp.csdr_print_bufsizes = cfg.csdr_print_bufsizes updater.start()
dsp.csdr_through = cfg.csdr_through
def spectrum_thread_function(): server = ThreadedHttpServer(('0.0.0.0', pm.getPropertyValue("web_port")), RequestHandler)
global clients, spectrum_dsp, spectrum_thread_watchdog_last_tick server.serve_forever()
spectrum_dsp=dsp=csdr.dsp()
dsp.nc_port=cfg.iq_server_port
dsp.set_demodulator("fft")
dsp.set_samp_rate(cfg.samp_rate)
dsp.set_fft_size(cfg.fft_size)
dsp.set_fft_fps(cfg.fft_fps)
dsp.set_fft_averages(int(round(1.0 * cfg.samp_rate / cfg.fft_size / cfg.fft_fps / (1.0 - cfg.fft_voverlap_factor))) if cfg.fft_voverlap_factor>0 else 0)
dsp.set_fft_compression(cfg.fft_compression)
dsp.set_format_conversion(cfg.format_conversion)
apply_csdr_cfg_to_dsp(dsp)
sleep_sec=0.87/cfg.fft_fps
print "[openwebrx-spectrum] Spectrum thread initialized successfully."
dsp.start()
if cfg.csdr_dynamic_bufsize:
dsp.read_async(8) #dummy read to skip bufsize & preamble
print "[openwebrx-spectrum] Note: CSDR_DYNAMIC_BUFSIZE_ON = 1"
print "[openwebrx-spectrum] Spectrum thread started."
bytes_to_read=int(dsp.get_fft_bytes_to_read())
spectrum_thread_counter=0
while True:
data=dsp.read_async(bytes_to_read)
if data is None:
time.sleep(.01)
continue
#print "gotcha",len(data),"bytes of spectrum data via spectrum_thread_function()"
if spectrum_thread_counter >= cfg.fft_fps:
spectrum_thread_counter=0
spectrum_thread_watchdog_last_tick = time.time() #once every second
else: spectrum_thread_counter+=1
cma("spectrum_thread")
correction=0
for i in range(0,len(clients)):
i-=correction
if (clients[i].ws_started):
if clients[i].spectrum_queue.full():
print "[openwebrx-spectrum] client spectrum queue full, closing it."
close_client(i, False)
correction+=1
else:
clients[i].spectrum_queue.put([data]) # add new string by "reference" to all clients
cmr()
def get_client_by_id(client_id, use_mutex=True):
global clients
output=-1
if use_mutex: cma("get_client_by_id")
for i in range(0,len(clients)):
if(clients[i].id==client_id):
output=i
break
if use_mutex: cmr()
if output==-1:
raise ClientNotFoundException
else:
return output
def log_client(client, what):
print "[openwebrx-httpd] client {0}#{1} :: {2}".format(client.ip,client.id,what)
def cleanup_clients(end_all=False):
# - if a client doesn't open websocket for too long time, we drop it
# - or if end_all is true, we drop all clients
global clients
cma("cleanup_clients")
correction=0
for i in range(0,len(clients)):
i-=correction
#print "cleanup_clients:: len(clients)=", len(clients), "i=", i
if end_all or ((not clients[i].ws_started) and (time.time()-clients[i].gen_time)>45):
if not end_all: print "[openwebrx] cleanup_clients :: client timeout to open WebSocket"
close_client(i, False)
correction+=1
cmr()
def generate_client_id(ip):
#add a client
global clients
new_client=namedtuple("ClientStruct", "id gen_time ws_started sprectum_queue ip closed bcastmsg dsp loopstat")
new_client.id=md5.md5(str(random.random())).hexdigest()
new_client.gen_time=time.time()
new_client.ws_started=False # to check whether client has ever tried to open the websocket
new_client.spectrum_queue=Queue.Queue(1000)
new_client.ip=ip
new_client.bcastmsg=""
new_client.closed=[False] #byref, not exactly sure if required
new_client.dsp=None
cma("generate_client_id")
clients.append(new_client)
log_client(new_client,"client added. Clients now: {0}".format(len(clients)))
cmr()
cleanup_clients()
return new_client.id
def close_client(i, use_mutex=True):
global clients
log_client(clients[i],"client being closed.")
if use_mutex: cma("close_client")
try:
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__": if __name__ == "__main__":
try:
main() main()
except KeyboardInterrupt:
for c in ClientRegistry.getSharedInstance().clients:
c.close()

100
owrx/config.py Normal file
View File

@ -0,0 +1,100 @@
import logging
logger = logging.getLogger(__name__)
class Property(object):
def __init__(self, value = None):
self.value = value
self.callbacks = []
def getValue(self):
return self.value
def setValue(self, value):
if (self.value == value):
return self
self.value = value
for c in self.callbacks:
try:
c(self.value)
except Exception as e:
logger.exception(e)
return self
def wire(self, callback):
self.callbacks.append(callback)
if not self.value is None: callback(self.value)
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(dict((name, self.getProperty(name) if self.hasProperty(name) else Property()) for name in props))
def __init__(self, properties = None):
self.properties = {}
self.callbacks = []
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.callbacks:
try:
c(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):
self.callbacks.append(callback)
return self
def unwire(self, callback):
self.callbacks.remove(callback)
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

178
owrx/connection.py Normal file
View File

@ -0,0 +1,178 @@
from owrx.config import PropertyManager
from owrx.source import DspManager, CpuUsageThread, SdrService, ClientRegistry
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
ClientRegistry.getSharedInstance().addClient(self)
self.dsp = None
self.sdr = None
self.configProps = None
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)
CpuUsageThread.getSharedInstance().add_client(self)
def sendConfig(self, key, value):
config = dict((key, self.configProps[key]) for key in OpenWebRxClient.config_keys)
# TODO mathematical properties? hmmmm
config["start_offset_freq"] = self.configProps["start_freq"] - self.configProps["center_freq"]
self.write_config(config)
def setSdr(self, id = None):
next = SdrService.getSource(id)
if (next == self.sdr):
return
self.stopDsp()
if self.configProps is not None:
self.configProps.unwire(self.sendConfig)
self.sdr = next
# send initial config
self.configProps = self.sdr.getProps().collect(*OpenWebRxClient.config_keys).defaults(PropertyManager.getSharedInstance())
self.configProps.wire(self.sendConfig)
self.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)
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") \
.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})
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
View 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()

65
owrx/feature.py Normal file
View File

@ -0,0 +1,65 @@
import os
import logging
logger = logging.getLogger(__name__)
class UnknownFeatureException(Exception):
pass
class FeatureDetector(object):
features = {
"core": [ "csdr", "nmux" ],
"rtl_sdr": [ "rtl_sdr" ],
"sdrplay": [ "rx_tools" ],
"hackrf": [ "hackrf_transfer" ]
}
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 has_csdr(self):
return os.system("csdr 2> /dev/null") != 32512
def has_nmux(self):
return os.system("nmux --help 2> /dev/null") != 32512
def has_rtl_sdr(self):
return os.system("rtl_sdr --help 2> /dev/null") != 32512
def has_rx_tools(self):
return os.system("rx_sdr --help 2> /dev/null") != 32512
"""
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 os.system("hackrf_transfer --help 2> /dev/null") != 32512

40
owrx/http.py Normal file
View File

@ -0,0 +1,40 @@
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}
]
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.")

36
owrx/sdrhu.py Normal file
View 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)

558
owrx/source.py Normal file
View File

@ -0,0 +1,558 @@
import subprocess
from owrx.config import PropertyManager
from owrx.feature import FeatureDetector, UnknownFeatureException
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(
"type", "samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp"
).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 these in subclasses as necessary
self.command = None
self.format_conversion = 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.command.format(
samp_rate = props["samp_rate"],
center_freq = props["center_freq"],
ppm = props["ppm"],
rf_gain = props["rf_gain"],
lna_gain = props["lna_gain"],
rf_amp = props["rf_amp"]
)
if self.format_conversion is not None:
start_sdr_command += " | " + self.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)
while True:
testsock = socket.socket()
try:
testsock.connect(("127.0.0.1", self.getPort()))
testsock.close()
break
except:
time.sleep(0.1)
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()
self.spectrumThread = SpectrumThread(self)
self.spectrumThread.start()
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.spectrumThread is not None:
self.spectrumThread.stop()
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)
def removeSpectrumClient(self, c):
try:
self.spectrumClients.remove(c)
except ValueError:
pass
def writeSpectrumData(self, data):
for c in self.spectrumClients:
c.write_spectrum_data(data)
class RtlSdrSource(SdrSource):
def __init__(self, props, port):
super().__init__(props, port)
self.command = "rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -"
self.format_conversion = "csdr convert_u8_f"
class HackrfSource(SdrSource):
def __init__(self, props, port):
super().__init__(props, port)
self.command = "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-"
self.format_conversion = "csdr convert_s8_f"
class SdrplaySource(SdrSource):
def __init__(self, props, port):
super().__init__(props, port)
self.command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -"
self.format_conversion = None
def sleepOnRestart(self):
time.sleep(1)
class SpectrumThread(threading.Thread):
def __init__(self, sdrSource):
self.doRun = True
self.sdrSource = sdrSource
super().__init__()
def run(self):
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()
dsp.nc_port = self.sdrSource.getPort()
dsp.set_demodulator("fft")
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)
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)
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.")
dsp.start()
if props["csdr_dynamic_bufsize"]:
dsp.read(8) #dummy read to skip bufsize & preamble
logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
logger.debug("Spectrum thread started.")
bytes_to_read=int(dsp.get_fft_bytes_to_read())
while self.doRun:
data=dsp.read(bytes_to_read)
if len(data) == 0:
time.sleep(1)
else:
self.sdrSource.writeSpectrumData(data)
dsp.stop()
logger.debug("spectrum thread shut down")
self.thread = None
self.sdrSource.removeClient(self)
def stop(self):
logger.debug("stopping spectrum thread")
self.doRun = False
class DspManager(object):
def __init__(self, handler, sdrSource):
self.doRun = False
self.handler = handler
self.sdrSource = sdrSource
self.dsp = None
self.sdrSource.addClient(self)
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"
).defaults(PropertyManager.getSharedInstance())
self.dsp = csdr.dsp()
#dsp_initialized=False
self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression)
self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression)
self.dsp.set_offset_freq(0)
self.dsp.set_bpf(-4000,4000)
self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size)
self.dsp.nc_port = self.sdrSource.getPort()
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"]
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)
def set_low_cut(cut):
bpf = self.dsp.get_bpf()
bpf[0] = cut
self.dsp.set_bpf(*bpf)
self.localProps.getProperty("low_cut").wire(set_low_cut)
def set_high_cut(cut):
bpf = self.dsp.get_bpf()
bpf[1] = cut
self.dsp.set_bpf(*bpf)
self.localProps.getProperty("high_cut").wire(set_high_cut)
self.localProps.getProperty("mod").wire(self.dsp.set_demodulator)
if (self.localProps["digimodes_enable"]):
def set_secondary_mod(mod):
if mod == False: mod = None
if self.dsp.get_secondary_demodulator() == mod: return
self.stopSecondaryThreads()
self.dsp.stop()
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.dsp.start()
if mod:
self.startSecondaryThreads()
self.localProps.getProperty("secondary_mod").wire(set_secondary_mod)
self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq)
super().__init__()
def start(self):
self.doRun = self.sdrSource.isAvailable()
if self.doRun:
self.dsp.start()
threading.Thread(target = self.readDspOutput).start()
threading.Thread(target = self.readSMeterOutput).start()
def startSecondaryThreads(self):
self.runSecondary = True
self.secondaryDemodThread = threading.Thread(target = self.readSecondaryDemod)
self.secondaryDemodThread.start()
self.secondaryFftThread = threading.Thread(target = self.readSecondaryFft)
self.secondaryFftThread.start()
def stopSecondaryThreads(self):
self.runSecondary = False
self.secondaryDemodThread = None
self.secondaryFftThread = None
def readDspOutput(self):
while (self.doRun):
data = self.dsp.read(256)
if len(data) != 256:
time.sleep(1)
else:
self.handler.write_dsp_data(data)
def readSMeterOutput(self):
while (self.doRun):
level = self.dsp.get_smeter_level()
self.handler.write_s_meter_level(level)
def readSecondaryDemod(self):
while (self.runSecondary):
data = self.dsp.read_secondary_demod(1)
self.handler.write_secondary_demod(data)
def readSecondaryFft(self):
while (self.runSecondary):
data = self.dsp.read_secondary_fft(int(self.dsp.get_secondary_fft_bytes_to_read()))
self.handler.write_secondary_fft(data)
def stop(self):
self.doRun = False
self.runSecondary = False
self.dsp.stop()
self.sdrSource.removeClient(self)
def setProperty(self, prop, value):
self.localProps.getProperty(prop).setValue(value)
def onSdrAvailable(self):
logger.debug("received onSdrAvailable, attempting DspSource restart")
if not self.doRun:
self.doRun = True
if self.dsp is not None:
self.dsp.start()
threading.Thread(target = self.readDspOutput).start()
threading.Thread(target = self.readSMeterOutput).start()
def onSdrUnavailable(self):
logger.debug("received onSdrUnavailable, shutting down DspSource")
if self.dsp is not None:
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")
if CpuUsageThread.sharedInstance == self:
CpuUsageThread.sharedInstance = None
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):
self.doRun = False
class ClientReportingThread(threading.Thread):
def __init__(self, registry):
self.doRun = True
self.registry = registry
super().__init__()
def run(self):
while self.doRun:
self.registry.broadcast()
time.sleep(3)
def stop(self):
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 = []
self.reporter = None
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)
if self.reporter is None:
self.reporter = ClientReportingThread(self)
self.reporter.start()
def clientCount(self):
return len(self.clients)
def removeClient(self, client):
try:
self.clients.remove(client)
except ValueError:
pass
if not self.clients and self.reporter is not None:
self.reporter.stop()
self.reporter = None

1
owrx/version.py Normal file
View File

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

91
owrx/websocket.py Normal file
View File

@ -0,0 +1,91 @@
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)
self.handler.wfile.write(header + data.encode('utf-8'))
self.handler.wfile.flush()
# anything else as binary
else:
header = self.get_header(len(data), 2)
self.handler.wfile.write(header + data)
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("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
View File

@ -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

View File

@ -20,31 +20,13 @@
""" """
import config_webrx as cfg, time, subprocess from owrx.sdrhu import SdrHuUpdater
from owrx.config import PropertyManager
def run(continuously=True):
if not cfg.sdrhu_key: return
firsttime="(Your receiver is soon getting listed on sdr.hu!)"
while True:
cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://"+cfg.server_hostname+":"+str(cfg.web_port)+"&apikey="+cfg.sdrhu_key+"\" 2>&1"
print "[openwebrx-sdrhu]", cmd
returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()
returned=returned[0]
#print returned
if "UPDATE:" in returned:
retrytime_mins = 20
value=returned.split("UPDATE:")[1].split("\n",1)[0]
if value.startswith("SUCCESS"):
print "[openwebrx-sdrhu] Update succeeded! "+firsttime
firsttime=""
else:
print "[openwebrx-sdrhu] Update failed, your receiver cannot be listed on sdr.hu! Reason:", value
else:
retrytime_mins = 2
print "[openwebrx-sdrhu] wget failed while updating, your receiver cannot be listed on sdr.hu!"
if not continuously: break
time.sleep(60*retrytime_mins)
if __name__=="__main__": if __name__=="__main__":
run(False) pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
if not "sdrhu_key" in pm:
exit(1)
SdrHuUpdater().update()