Skip to content
This repository has been archived by the owner on Jan 13, 2020. It is now read-only.

WSPR decoder, config improvements, sample IQ files #87

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added CQWW_CW_2005.fs96k.cf7040.iq.s16.dat.xz
Binary file not shown.
75 changes: 53 additions & 22 deletions config_webrx.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
# samp_rate = 250000
samp_rate = 2400000
center_freq = 144250000
start_freq = 0
start_mod = "nfm" #nfm, am, lsb, usb, cw
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

Expand Down Expand Up @@ -102,15 +104,17 @@
#################################################################################################

# 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):
sdr_source = "IQ_file" # use one of: "rtl_sdr" "rx_sdr" "gr-osmosdr" "sound_card" "noise_source" "IQ_file"

# >> RTL-SDR via rtl_sdr
start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
format_conversion="csdr convert_u8_f"

#lna_gain=8
#rf_amp=1
#start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm, rf_amp=rf_amp, lna_gain=lna_gain)
#format_conversion="csdr convert_s8_f"
if sdr_source == "rtl_sdr":
start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
format_conversion="csdr convert_u8_f"

#lna_gain=8
#rf_amp=1
#start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm, rf_amp=rf_amp, lna_gain=lna_gain)
#format_conversion="csdr convert_s8_f"
"""
To use a HackRF, compile the HackRF host tools from its "stdout" branch:
git clone https://github.com/mossmann/hackrf/
Expand All @@ -127,32 +131,59 @@

# >> Sound card SDR (needs ALSA)
# I did not have the chance to properly test it.
#samp_rate = 96000
#start_rtl_command="arecord -f S16_LE -r {samp_rate} -c2 -".format(samp_rate=samp_rate)
#format_conversion="csdr convert_s16_f | csdr gain_ff 30"
if sdr_source == "sound_card":
samp_rate = 96000
start_rtl_command="arecord -f S16_LE -r {samp_rate} -c2 -".format(samp_rate=samp_rate)
format_conversion="csdr convert_s16_f | csdr gain_ff 30"

# >> /dev/urandom test signal source
# samp_rate = 2400000
# start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate)
# format_conversion="csdr convert_u8_f"
if sdr_source == "noise_source":
samp_rate = 2400000
start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate)
format_conversion="csdr convert_u8_f"

# >> Pre-recorded raw I/Q file as signal source
# You will have to correctly specify: samp_rate, center_freq, format_conversion in order to correctly play an I/Q file.
#start_rtl_command="(while true; do cat my_iq_file.raw; done) | csdr flowcontrol {sr} 20 ".format(sr=samp_rate*2*1.05)
#format_conversion="csdr convert_u8_f"
# You will have to correctly specify: samp_rate, center_freq, bytes_per_sample, format_conversion in order to correctly play an I/Q file.
if sdr_source == "IQ_file":
# select one:
#sample_file = "CQWW"
sample_file = "WSPR_demo"

if sample_file == "CQWW":
file = "CQWW_CW_2005.fs96k.cf7040.iq.s16.dat"
samp_rate = 96000
center_freq = 7040000
bytes_per_sample = 4
start_rtl_command="(while true; do cat {fn}; done) | csdr flowcontrol {sr} 20 ".format(fn=file,sr=samp_rate*bytes_per_sample*1.05)
format_conversion="csdr convert_s16_f --bigendian | csdr iq_swap_ff"
start_mod = "lsb"

if sample_file == "WSPR_demo":
file = "wspr.fs12k.cf7040100.iq.f.dat"
samp_rate = 12000
center_freq = 7040100
bytes_per_sample = 8
start_rtl_command="csdr wspr --file {fn} {sr} 20 ".format(fn=file,sr=samp_rate*bytes_per_sample*1.05)
format_conversion=""
start_mod = "cw"
start_freq = center_freq - 750

#format_conversion="csdr convert_u8_f"

#>> The rx_sdr command works with a variety of SDR harware: RTL-SDR, HackRF, SDRplay, UHD, Airspy, Red Pitaya, audio devices, etc.
# It will auto-detect your SDR hardware if the following tools are installed:
# * the vendor provided driver and library,
# * the vendor-specific SoapySDR wrapper library,
# * and SoapySDR itself.
# Check out this article on the OpenWebRX Wiki: https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX/
#start_rtl_command="rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
#format_conversion=""
if sdr_source == "rx_sdr":
start_rtl_command="rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
format_conversion=""

# >> gr-osmosdr signal source using GNU Radio (follow this guide: https://github.com/simonyiszk/openwebrx/wiki/Using-GrOsmoSDR-as-signal-source)
#start_rtl_command="cat /tmp/osmocom_fifo"
#format_conversion=""
if sdr_source == "gr-osmosdr":
start_rtl_command="cat /tmp/osmocom_fifo"
format_conversion=""

# ==== Misc settings ====

Expand All @@ -163,8 +194,8 @@
# - also increase the latency
# - decrease the chance of audio underruns

start_freq = center_freq
start_mod = "nfm" #nfm, am, lsb, usb, cw
if start_freq == 0:
start_freq = center_freq

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.

Expand Down
67 changes: 52 additions & 15 deletions csdr.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ def __init__(self):
self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "iqtee_pipe", "iqtee2_pipe"]
self.secondary_pipe_names=["secondary_shift_pipe"]
self.secondary_offset_freq = 1000
self.secondary_dual_paths = True
self.secondary_demod_output_HTML = False

def chain(self,which):
any_chain_base="nc -v 127.0.0.1 {nc_port} | "
Expand All @@ -79,28 +81,58 @@ def chain(self,which):
chain_begin=any_chain_base+"csdr shift_addition_cc --fifo {shift_pipe} | csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | "
if self.secondary_demodulator:
chain_begin+="csdr tee {iqtee_pipe} | "
chain_begin+="csdr tee {iqtee2_pipe} | "
if self.secondary_dual_paths == True:
chain_begin+="csdr tee {iqtee2_pipe} | "
chain_end = ""
if self.audio_compression=="adpcm":
chain_end = " | csdr encode_ima_adpcm_i16_u8"
if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr fastagc_ff 1024 | csdr convert_f_s16"+chain_end
elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end
elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end

def secondary_chain(self, which):
def secondary_chain(self, demod, which):
secondary_chain_base="cat {input_pipe} | "
if which == "fft":
return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "")
elif which == "bpsk31":
return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \
"csdr bandpass_fir_fft_cc $(csdr '=-(31.25)/{if_samp_rate}') $(csdr '=(31.25)/{if_samp_rate}') $(csdr '=31.25/{if_samp_rate}') | " + \
"csdr simple_agc_cc 0.001 0.5 | " + \
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8"

if demod == "wspr":
if which == "fft":
fractional_decimate = self.if_samp_rate()/375.0
integer_decimate = round(fractional_decimate)
if abs(fractional_decimate - integer_decimate) > 1e-15:
# FIXME: fractional_decimator_cc doesn't exist yet (needs to be derived from fractional_decimator_ff)
# so for now WSPR only works with the corresponding demo IQ sample file
secondary_chain_base += "csdr fractional_decimator_cc {decimate} | csdr wspr --fft {{iqtee2_pipe}} 1 ".format(decimate=fractional_decimate)
integer_decimate = 1
secondary_chain_base += "csdr wspr --fft {{iqtee2_pipe}} {decimate} ".format(decimate=integer_decimate)
return secondary_chain_base + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "")
elif which == "demod":
# in this case the demod input_pipe will be connected to the iqtee2_pipe output from the FFT chain
#return "cat {input_pipe} | csdr wspr --demod"
return "cat {input_pipe}"
else:
if which == "fft":
return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "")
elif which == "demod":
if demod == "bpsk31":
return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \
"csdr bandpass_fir_fft_cc $(csdr '=-(31.25)/{if_samp_rate}') $(csdr '=(31.25)/{if_samp_rate}') $(csdr '=31.25/{if_samp_rate}') | " + \
"csdr simple_agc_cc 0.001 0.5 | " + \
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8"

# A "single path" secondary demodulator consists of samples sent to the FFT chain and the iqtee2_pipe setup
# as a bridge between the FFT and demod chains. No IQ samples are sent to the demod chain.
# Used when the demodulator needs to be a monolithic routine on a single chain but still produces
# FFT and demod text output.
def set_secondary_demodulator(self, what):
self.secondary_demodulator = what
if what == "wspr":
self.secondary_dual_paths = False
self.secondary_pipe_names = ["secondary_shift_pipe", "iqtee2_pipe"];
self.secondary_demod_output_HTML = True
else:
self.secondary_dual_paths = True
self.secondary_pipe_names = ["secondary_shift_pipe"];
self.secondary_demod_output_HTML = False

def secondary_fft_block_size(self):
return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here
Expand Down Expand Up @@ -130,15 +162,17 @@ def secondary_bw(self):
def start_secondary_demodulator(self):
if(not self.secondary_demodulator): return
print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate()
secondary_command_fft=self.secondary_chain("fft")
secondary_command_demod=self.secondary_chain(self.secondary_demodulator)
secondary_command_fft=self.secondary_chain(self.secondary_demodulator, "fft")
secondary_command_demod=self.secondary_chain(self.secondary_demodulator, "demod")
self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft)

secondary_command_fft=secondary_command_fft.format( \
input_pipe=self.iqtee_pipe, \
iqtee2_pipe=self.iqtee2_pipe, \
secondary_fft_input_size=self.secondary_fft_size, \
secondary_fft_size=self.secondary_fft_size, \
secondary_fft_block_size=self.secondary_fft_block_size(), \
if_samp_rate=self.if_samp_rate()
)
secondary_command_demod=secondary_command_demod.format( \
input_pipe=self.iqtee2_pipe, \
Expand Down Expand Up @@ -176,7 +210,7 @@ def start_secondary_demodulator(self):

def set_secondary_offset_freq(self, value):
self.secondary_offset_freq=value
if self.secondary_processes_running:
if self.secondary_processes_running and self.secondary_shift_pipe != None:
self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate()))
self.secondary_shift_pipe_file.flush()

Expand All @@ -200,6 +234,9 @@ def set_secondary_fft_size(self,secondary_fft_size):
#to change this, restart is required
self.secondary_fft_size=secondary_fft_size

def get_secondary_demod_output_HTML(self):
return self.secondary_demod_output_HTML

def set_audio_compression(self,what):
self.audio_compression = what

Expand Down
8 changes: 7 additions & 1 deletion htdocs/index.wrx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
<select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();">
<option value="none"></option>
<option value="bpsk31">BPSK31</option>
<option value="wspr">WSPR</option>
</select>
</div>
<div class="openwebrx-panel-line">
Expand Down Expand Up @@ -163,7 +164,12 @@
<div id="openwebrx-digimode-content-container">
<div class="gradient"></div>
<div id="openwebrx-digimode-content">
<span id="openwebrx-cursor-blink"></span>
<span id="openwebrx-digimode-HTML"></span>
<span id="openwebrx-digimode-text"></span>
<div id="openwebrx-digimode-status">
<div id="openwebrx-digimode-status-text"></div>
</div>
<span id="openwebrx-cursor-blink"></span>
</div>
</div>
</div>
Expand Down
31 changes: 31 additions & 0 deletions htdocs/openwebrx.css
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,37 @@ img.openwebrx-mirror-img
100%{ transform: rotateX(360deg) rotateY(720deg); }
}

#openwebrx-digimode-content div#openwebrx-digimode-status
{
width: 100px;
height: 20px;
}

#openwebrx-digimode-status
{
display: inline-block;
border-radius: 5px;
color: white;
text-align: center;
font-size: 8pt;
font-weight: bold;
text-shadow: 0px 0px 4px #000000;
cursor: default;
user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}

#openwebrx-digimode-status-text
{
position: relative;
top: 3px;
width: inherit;
}

#openwebrx-digimode-content
{
word-wrap: break-word;
Expand Down
Loading