Merge branch 'develop' into packet

This commit is contained in:
Jakob Ketterl 2019-06-22 18:20:01 +02:00
commit 1f6f755d7f
33 changed files with 784 additions and 71 deletions

22
build.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
set -euxo pipefail
ARCH=$(uname -m)
case $ARCH in
x86_64)
BASE_IMAGE=alpine
;;
armv*)
BASE_IMAGE=arm32v6/alpine
esac
TAGS=$ARCH
docker build --build-arg BASE_IMAGE=$BASE_IMAGE -t openwebrx-base:$ARCH -f docker/Dockerfiles/Dockerfile-base .
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-rtlsdr:$ARCH -f docker/Dockerfiles/Dockerfile-rtlsdr .
docker build --build-arg ARCH=$ARCH -t openwebrx-soapysdr-base:$ARCH -f docker/Dockerfiles/Dockerfile-soapysdr .
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-sdrplay:$ARCH -f docker/Dockerfiles/Dockerfile-sdrplay .
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-hackrf:$ARCH -f docker/Dockerfiles/Dockerfile-hackrf .
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-airspy:$ARCH -f docker/Dockerfiles/Dockerfile-airspy .
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-full:$ARCH -t jketterl/openwebrx:$ARCH -f docker/Dockerfiles/Dockerfile-full .

View File

@ -99,7 +99,7 @@ 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 #
################################################################################################# #################################################################################################
# Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf" # Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf", "airspy"
sdrs = { sdrs = {
"rtlsdr": { "rtlsdr": {

38
csdr.py
View File

@ -67,7 +67,8 @@ class dsp(object):
self.secondary_fft_size = 1024 self.secondary_fft_size = 1024
self.secondary_process_fft = None self.secondary_process_fft = None
self.secondary_process_demod = None self.secondary_process_demod = None
self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", "iqtee2_pipe"] self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe",
"iqtee2_pipe", "dmr_control_pipe"]
self.secondary_pipe_names=["secondary_shift_pipe"] self.secondary_pipe_names=["secondary_shift_pipe"]
self.secondary_offset_freq = 1000 self.secondary_offset_freq = 1000
self.unvoiced_quality = 1 self.unvoiced_quality = 1
@ -107,16 +108,19 @@ class dsp(object):
chain += "dsd -fd" chain += "dsd -fd"
elif which == "nxdn": elif which == "nxdn":
chain += "dsd -fi" chain += "dsd -fi"
chain += " -i - -o - -u {unvoiced_quality} -g 10 | " chain += " -i - -o - -u {unvoiced_quality} -g -1 | CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f | "
chain += "digitalvoice_filter | sox -V -v 0.95 -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " max_gain = 5
# digiham modes # digiham modes
else: else:
chain += "rrc_filter | csdr convert_f_s16 | gfsk_demodulator | " chain += "rrc_filter | gfsk_demodulator | "
if which == "dmr": if which == "dmr":
chain += "dmr_decoder --fifo {meta_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | " chain += "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | "
elif which == "ysf": elif which == "ysf":
chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | " chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | "
chain += "digitalvoice_filter -f | csdr agc_ff 160000 0.8 1 0.0000001 0.0005 | csdr convert_f_s16 | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " max_gain = 0.0005
chain += "digitalvoice_filter -f | "
chain += "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain} | ".format(max_gain=max_gain)
chain += "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - "
elif which == "packet": elif which == "packet":
chain += "csdr fmdemod_quadri_cf | " chain += "csdr fmdemod_quadri_cf | "
chain += last_decimation_block chain += last_decimation_block
@ -359,7 +363,7 @@ class dsp(object):
actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level
if self.running: if self.running:
self.modification_lock.acquire() self.modification_lock.acquire()
self.squelch_pipe_file.write( "%g\n"%(float(actual_squelch)) ) self.squelch_pipe_file.write("%g\n"%(float(actual_squelch)))
self.squelch_pipe_file.flush() self.squelch_pipe_file.flush()
self.modification_lock.release() self.modification_lock.release()
@ -370,6 +374,11 @@ class dsp(object):
def get_unvoiced_quality(self): def get_unvoiced_quality(self):
return self.unvoiced_quality return self.unvoiced_quality
def set_dmr_filter(self, filter):
if self.dmr_control_pipe_file:
self.dmr_control_pipe_file.write("{0}\n".format(filter))
self.dmr_control_pipe_file.flush()
def mkfifo(self,path): def mkfifo(self,path):
try: try:
os.unlink(path) os.unlink(path)
@ -417,7 +426,8 @@ class dsp(object):
flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port,
squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe,
output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000), output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000),
unvoiced_quality = self.get_unvoiced_quality(), audio_rate = self.get_audio_rate()) unvoiced_quality = self.get_unvoiced_quality(), audio_rate = self.get_audio_rate(),
dmr_control_pipe = self.dmr_control_pipe)
logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command)
my_env=os.environ.copy() my_env=os.environ.copy()
@ -437,13 +447,12 @@ class dsp(object):
self.output.add_output("audio", partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256)) self.output.add_output("audio", partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256))
# open control pipes for csdr # open control pipes for csdr
if self.bpf_pipe != None: if self.bpf_pipe:
self.bpf_pipe_file=open(self.bpf_pipe,"w") self.bpf_pipe_file = open(self.bpf_pipe, "w")
if self.shift_pipe: if self.shift_pipe:
self.shift_pipe_file=open(self.shift_pipe,"w") self.shift_pipe_file = open(self.shift_pipe, "w")
if self.squelch_pipe: if self.squelch_pipe:
self.squelch_pipe_file=open(self.squelch_pipe,"w") self.squelch_pipe_file = open(self.squelch_pipe, "w")
self.start_secondary_demodulator() self.start_secondary_demodulator()
self.modification_lock.release() self.modification_lock.release()
@ -475,6 +484,9 @@ class dsp(object):
return raw.rstrip("\n") return raw.rstrip("\n")
self.output.add_output("meta", read_meta) self.output.add_output("meta", read_meta)
if self.dmr_control_pipe:
self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w")
def stop(self): def stop(self):
self.modification_lock.acquire() self.modification_lock.acquire()
self.running = False self.running = False

View File

@ -0,0 +1,6 @@
ARG ARCH
FROM openwebrx-base:$ARCH
ADD docker/scripts/install-dependencies-airspy.sh /
RUN /install-dependencies-airspy.sh

View File

@ -0,0 +1,16 @@
ARG BASE_IMAGE
FROM $BASE_IMAGE
RUN apk add --no-cache bash
ADD docker/scripts/install-dependencies.sh /
RUN /install-dependencies.sh
ADD . /openwebrx
WORKDIR /openwebrx
VOLUME /config
ENTRYPOINT [ "/openwebrx/docker/scripts/run.sh" ]
EXPOSE 8073

View File

@ -0,0 +1,11 @@
ARG ARCH
FROM openwebrx-base:$ARCH
ADD docker/scripts/install-dependencies-*.sh /
ADD docker/scripts/install-lib.*.patch /
RUN /install-dependencies-rtlsdr.sh
RUN /install-dependencies-hackrf.sh
RUN /install-dependencies-soapysdr.sh
RUN /install-dependencies-sdrplay.sh
RUN /install-dependencies-airspy.sh

View File

@ -0,0 +1,6 @@
ARG ARCH
FROM openwebrx-base:$ARCH
ADD docker/scripts/install-dependencies-hackrf.sh /
RUN /install-dependencies-hackrf.sh

View File

@ -0,0 +1,6 @@
ARG ARCH
FROM openwebrx-base:$ARCH
ADD docker/scripts/install-dependencies-rtlsdr.sh /
RUN /install-dependencies-rtlsdr.sh

View File

@ -0,0 +1,7 @@
ARG ARCH
FROM openwebrx-soapysdr-base:$ARCH
ADD docker/scripts/install-dependencies-sdrplay.sh /
ADD docker/scripts/install-lib.*.patch /
RUN /install-dependencies-sdrplay.sh

View File

@ -0,0 +1,6 @@
ARG ARCH
FROM openwebrx-base:$ARCH
ADD docker/scripts/install-dependencies-soapysdr.sh /
RUN /install-dependencies-soapysdr.sh

View File

@ -0,0 +1,26 @@
#!/bin/bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://github.com/airspy/airspyone_host.git
cmakebuild airspyone_host
apk del .build-deps

View File

@ -0,0 +1,29 @@
#!/bin/bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb fftw"
BUILD_PACKAGES="git cmake make patch wget sudo udev gcc g++ libusb-dev fftw-dev"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://github.com/mossmann/hackrf.git
cd hackrf
cmakebuild host
cd ..
rm -rf hackrf
apk del .build-deps

View File

@ -0,0 +1,26 @@
#!/bin/bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://github.com/osmocom/rtl-sdr.git
cmakebuild rtl-sdr
apk del .build-deps

View File

@ -0,0 +1,47 @@
#!/bin/bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb"
BUILD_PACKAGES="git cmake make patch wget sudo udev gcc g++ libusb-dev"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
ARCH=$(uname -m)
case $ARCH in
x86_64)
BINARY=SDRplay_RSP_API-Linux-2.13.1.run
;;
armv*)
BINARY=SDRplay_RSP_API-RPi-2.13.1.run
;;
esac
wget http://www.sdrplay.com/software/$BINARY
sh $BINARY --noexec --target sdrplay
patch --verbose -Np0 < /install-lib.$ARCH.patch
cd sdrplay
./install_lib.sh
cd ..
rm -rf sdrplay
rm $BINARY
git clone https://github.com/pothosware/SoapySDRPlay.git
cmakebuild SoapySDRPlay
apk del .build-deps

View File

@ -0,0 +1,27 @@
#!/bin/bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
BUILD_PACKAGES="git cmake make patch wget sudo udev gcc g++"
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://github.com/pothosware/SoapySDR
cmakebuild SoapySDR
git clone https://github.com/rxseger/rx_tools
cmakebuild rx_tools
apk del .build-deps

View File

@ -0,0 +1,78 @@
#!/bin/bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack"
BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://git.code.sf.net/p/itpp/git itpp
cmakebuild itpp
git clone https://github.com/simonyiszk/csdr.git
cd csdr
patch -Np1 <<'EOF'
--- a/csdr.c
+++ b/csdr.c
@@ -38,6 +38,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
+#include <sys/select.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
diff --git a/ddcd_old.h b/ddcd_old.h
index af4cfb5..b70092b 100644
--- a/ddcd_old.h
+++ b/ddcd_old.h
@@ -19,6 +19,7 @@
#include <stdarg.h>
#include <sys/stat.h>
#include <semaphore.h>
+#include <sys/select.h>
typedef struct client_s
{
diff --git a/nmux.h b/nmux.h
index 038bc51..079e416 100644
--- a/nmux.h
+++ b/nmux.h
@@ -11,6 +11,7 @@
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
+#include <sys/select.h>
#include "tsmpool.h"
#define MSG_START "nmux: "
EOF
make
make install
cd ..
rm -rf csdr
git clone https://github.com/szechyjs/mbelib.git
cmakebuild mbelib
git clone https://github.com/jketterl/digiham.git
cmakebuild digiham
git clone https://github.com/f4exb/dsd.git
cmakebuild dsd
apk del .build-deps

View File

@ -0,0 +1,40 @@
--- sdrplay/install_lib.sh
+++ sdrplay/install_lib_patched.sh
@@ -3,19 +3,7 @@
echo "Installing SDRplay RSP API library 2.13..."
-more sdrplay_license.txt
-
-while true; do
- echo "Press y and RETURN to accept the license agreement and continue with"
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
-
-export ARCH=`arch`
+export ARCH=`uname -m`
export VERS="2.13"
echo "Architecture: ${ARCH}"
@@ -60,16 +48,6 @@
echo "ERROR: udev rules directory not found, add udev support and run the"
echo "installer again. udev support can be added by running..."
echo "sudo apt-get install libudev-dev"
- echo " "
- exit 1
-fi
-
-if /sbin/ldconfig -p | /bin/fgrep -q libusb-1.0; then
- echo "Libusb found, continuing..."
-else
- echo " "
- echo "ERROR: Libusb cannot be found. Please install libusb and then run"
- echo "the installer again. Libusb can be installed from http://libusb.info"
echo " "
exit 1
fi

View File

@ -0,0 +1,40 @@
--- sdrplay/install_lib.sh 2018-06-21 01:57:02.000000000 +0200
+++ sdrplay/install_lib_patched.sh 2019-01-22 17:21:06.445804136 +0100
@@ -2,19 +2,7 @@
echo "Installing SDRplay RSP API library 2.13..."
-more sdrplay_license.txt
-
-while true; do
- echo "Press y and RETURN to accept the license agreement and continue with"
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
-
-export ARCH=`arch`
+export ARCH=`uname -m`
export VERS="2.13"
echo "Architecture: ${ARCH}"
@@ -60,16 +48,6 @@
echo " "
exit 1
fi
-
-if /sbin/ldconfig -p | /bin/fgrep -q libusb-1.0; then
- echo "Libusb found, continuing..."
-else
- echo " "
- echo "ERROR: Libusb cannot be found. Please install libusb and then run"
- echo "the installer again. Libusb can be installed from http://libusb.info"
- echo " "
- exit 1
-fi
#echo "Installing SoapySDRPlay..."

23
docker/scripts/run.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash
set -euo pipefail
if [[ ! -f /config/config_webrx.py ]] ; then
cp config_webrx.py /config
fi
rm config_webrx.py
ln -s /config/config_webrx.py .
_term() {
echo "Caught signal!"
kill -TERM "$child" 2>/dev/null
}
trap _term SIGTERM SIGINT
python3 openwebrx.py $@ &
child=$!
wait "$child"

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="5.6444445mm"
height="9.847393mm"
viewBox="0 0 20 34.892337"
id="svg3455"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="Map Pin.svg">
<defs
id="defs3457" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="12.181359"
inkscape:cx="8.4346812"
inkscape:cy="14.715224"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1024"
inkscape:window-height="705"
inkscape:window-x="-4"
inkscape:window-y="-4"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata3460">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-814.59595,-274.38623)">
<g
id="g3477"
transform="matrix(1.1855854,0,0,1.1855854,-151.17715,-57.3976)">
<path
sodipodi:nodetypes="sscccccsscs"
inkscape:connector-curvature="0"
id="path4337-3"
d="m 817.11249,282.97118 c -1.25816,1.34277 -2.04623,3.29881 -2.01563,5.13867 0.0639,3.84476 1.79693,5.3002 4.56836,10.59179 0.99832,2.32851 2.04027,4.79237 3.03125,8.87305 0.13772,0.60193 0.27203,1.16104 0.33416,1.20948 0.0621,0.0485 0.19644,-0.51262 0.33416,-1.11455 0.99098,-4.08068 2.03293,-6.54258 3.03125,-8.87109 2.77143,-5.29159 4.50444,-6.74704 4.56836,-10.5918 0.0306,-1.83986 -0.75942,-3.79785 -2.01758,-5.14062 -1.43724,-1.53389 -3.60504,-2.66908 -5.91619,-2.71655 -2.31115,-0.0475 -4.4809,1.08773 -5.91814,2.62162 z"
style="display:inline;opacity:1;fill:#ff4646;fill-opacity:1;stroke:#d73534;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
r="3.0355"
cy="288.25278"
cx="823.03064"
id="path3049"
style="display:inline;opacity:1;fill:#590000;fill-opacity:1;stroke-width:0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -171,7 +171,34 @@
</div> </div>
</div> </div>
</div> </div>
<div class="openwebrx-panel" id="openwebrx-panel-metadata" data-panel-name="metadata" data-panel-pos="left" data-panel-order="1" data-panel-size="615,36"> <div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" data-panel-name="metadata-ysf" data-panel-pos="left" data-panel-order="2" data-panel-size="145,220">
<div class="openwebrx-meta-frame">
<div class="openwebrx-meta-slot">
<div class="openwebrx-ysf-mode openwebrx-meta-autoclear"></div>
<div class="openwebrx-meta-user-image"></div>
<div class="openwebrx-ysf-source openwebrx-meta-autoclear"></div>
<div class="openwebrx-ysf-up openwebrx-meta-autoclear"></div>
<div class="openwebrx-ysf-down openwebrx-meta-autoclear"></div>
</div>
</div>
</div>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-dmr" data-panel-name="metadata-dmr" data-panel-pos="left" data-panel-order="2" data-panel-size="300,220">
<div class="openwebrx-meta-frame">
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
<div class="openwebrx-dmr-slot">Timeslot 1</div>
<div class="openwebrx-meta-user-image"></div>
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div>
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div>
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
</div>
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
<div class="openwebrx-dmr-slot">Timeslot 2</div>
<div class="openwebrx-meta-user-image"></div>
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div>
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div>
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -928,3 +928,88 @@ img.openwebrx-mirror-img
border-color: Red; border-color: Red;
} }
.openwebrx-meta-slot {
width: 145px;
height: 196px;
float: left;
margin-right: 10px;
background-color: #676767;
padding: 2px 0;
color: #333;
text-align: center;
position: relative;
}
.openwebrx-meta-slot, .openwebrx-meta-slot.muted:before {
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.openwebrx-meta-slot.muted:before {
display: block;
content: "";
background-image: url("gfx/openwebrx-mute.png");
width:100%;
height:133px;
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: rgba(0,0,0,.3);
}
.openwebrx-meta-slot.active {
background-color: #95bbdf;
}
.openwebrx-meta-slot.sync .openwebrx-dmr-slot:before {
content:"";
display: inline-block;
margin: 0 5px;
width: 12px;
height: 12px;
background-color: #ABFF00;
border-radius: 50%;
box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px;
}
.openwebrx-meta-slot:last-child {
margin-right: 0;
}
.openwebrx-meta-slot .openwebrx-meta-user-image {
width:100%;
height:133px;
background-position: center;
background-repeat: no-repeat;
}
.openwebrx-meta-slot.active .openwebrx-meta-user-image {
background-image: url("gfx/openwebrx-directcall.png");
}
.openwebrx-meta-slot.active .openwebrx-meta-user-image.group {
background-image: url("gfx/openwebrx-groupcall.png");
}
.openwebrx-dmr-timeslot-panel * {
cursor: pointer;
}
.openwebrx-maps-pin {
background-image: url("gfx/google_maps_pin.svg");
background-position: center;
background-repeat: no-repeat;
width: 15px;
height: 15px;
background-size: contain;
display: inline-block;
}

View File

@ -624,7 +624,8 @@ function demodulator_analog_replace(subtype, for_digital)
} }
demodulator_add(new demodulator_default_analog(temp_offset,subtype)); demodulator_add(new demodulator_default_analog(temp_offset,subtype));
demodulator_buttons_update(); demodulator_buttons_update();
clear_metadata(); hide_digitalvoice_panels();
toggle_panel("openwebrx-panel-metadata-" + subtype, true);
} }
function demodulator_set_offset_frequency(which,to_what) function demodulator_set_offset_frequency(which,to_what)
@ -1315,56 +1316,78 @@ function on_ws_recv(evt)
} }
function update_metadata(meta) { function update_metadata(meta) {
var update = function(_, el) {
el.innerHTML = "";
};
if (meta.protocol) switch (meta.protocol) { if (meta.protocol) switch (meta.protocol) {
case 'DMR': case 'DMR':
if (meta.slot) { if (meta.slot) {
var html = 'Timeslot: ' + meta.slot; var el = $("#openwebrx-panel-metadata-dmr .openwebrx-dmr-timeslot-panel").get(meta.slot);
if (meta.type) html += ' Typ: ' + meta.type; var id = "";
if (meta.additional && meta.additional.callsign) { var name = "";
html += ' Source: ' + meta.additional.callsign; var target = "";
if (meta.additional.fname) { var group = false;
html += ' (' + meta.additional.fname + ')'; $(el)[meta.sync ? "addClass" : "removeClass"]("sync");
if (meta.sync && meta.sync == "voice") {
id = (meta.additional && meta.additional.callsign) || meta.source || "";
name = (meta.additional && meta.additional.fname) || "";
if (meta.type == "group") {
target = "Talkgroup: ";
group = true;
} }
} else if (meta.source) { if (meta.type == "direct") target = "Direct: ";
html += ' Source: ' + meta.source; target += meta.target || "";
$(el).addClass("active");
} else {
$(el).removeClass("active");
} }
if (meta.target) html += ' Target: ' + meta.target; $(el).find(".openwebrx-dmr-id").text(id);
update = function(_, el) { $(el).find(".openwebrx-dmr-name").text(name);
var slotEl = el.getElementsByClassName('slot-' + meta.slot); $(el).find(".openwebrx-dmr-target").text(target);
if (!slotEl.length) { $(el).find(".openwebrx-meta-user-image")[group ? "addClass" : "removeClass"]("group");
slotEl = document.createElement('div'); } else {
slotEl.className = 'slot-' + meta.slot; clear_metadata();
el.appendChild(slotEl);
} else {
slotEl = slotEl[0];
}
slotEl.innerHTML = html;
};
} }
break; break;
case 'YSF': case 'YSF':
var strings = []; var el = $("#openwebrx-panel-metadata-ysf");
if (meta.mode) strings.push("Mode: " + meta.mode);
if (meta.source) strings.push("Source: " + meta.source); var mode = " "
if (meta.target) strings.push("Destination: " + meta.target); var source = "";
if (meta.up) strings.push("Up: " + meta.up); var up = "";
if (meta.down) strings.push("Down: " + meta.down); var down = "";
var html = strings.join(' '); if (meta.mode && meta.mode != "") {
update = function(_, el) { mode = "Mode: " + meta.mode;
el.innerHTML = html; source = meta.source || "";
if (meta.lat && meta.lon) {
source = "<a class=\"openwebrx-maps-pin\" href=\"https://www.google.com/maps/search/?api=1&query=" + meta.lat + "," + meta.lon + "\" target=\"_blank\"></a>" + source;
}
up = meta.up ? "Up: " + meta.up : "";
down = meta.down ? "Down: " + meta.down : "";
$(el).find(".openwebrx-meta-slot").addClass("active");
} else {
$(el).find(".openwebrx-meta-slot").removeClass("active");
} }
$(el).find(".openwebrx-ysf-mode").text(mode);
$(el).find(".openwebrx-ysf-source").html(source);
$(el).find(".openwebrx-ysf-up").text(up);
$(el).find(".openwebrx-ysf-down").text(down);
break; break;
} else {
clear_metadata();
} }
$('.openwebrx-panel[data-panel-name="metadata"]').each(update); }
toggle_panel("openwebrx-panel-metadata", true);
function hide_digitalvoice_panels() {
$(".openwebrx-meta-panel").each(function(_, p){
toggle_panel(p.id, false);
});
clear_metadata();
} }
function clear_metadata() { function clear_metadata() {
toggle_panel("openwebrx-panel-metadata", false); $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text("");
$(".openwebrx-meta-slot").removeClass("active").removeClass("sync");
$(".openwebrx-dmr-timeslot-panel").removeClass("muted");
} }
function add_problem(what) function add_problem(what)
@ -1817,7 +1840,12 @@ String.prototype.startswith=function(str){ return this.indexOf(str) == 0; }; //h
function open_websocket() function open_websocket()
{ {
ws_url="ws://"+(window.location.origin.split("://")[1])+"/ws/"; //guess automatically -> now default behaviour var protocol = 'ws';
if (window.location.toString().startsWith('https://')) {
protocol = 'wss';
}
ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; //guess automatically -> now default behaviour
if (!("WebSocket" in window)) 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); ws = new WebSocket(ws_url);
@ -2311,7 +2339,7 @@ function openwebrx_init()
init_rx_photo(); init_rx_photo();
open_websocket(); open_websocket();
secondary_demod_init(); secondary_demod_init();
clear_metadata(); digimodes_init();
place_panels(first_show_panel); place_panels(first_show_panel);
window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000);
window.addEventListener("resize",openwebrx_resize); window.addEventListener("resize",openwebrx_resize);
@ -2322,6 +2350,25 @@ function openwebrx_init()
} }
function digimodes_init() {
hide_digitalvoice_panels();
// initialze DMR timeslot muting
$('.openwebrx-dmr-timeslot-panel').click(function(e) {
$(e.currentTarget).toggleClass("muted");
update_dmr_timeslot_filtering();
});
}
function update_dmr_timeslot_filtering() {
var filter = $('.openwebrx-dmr-timeslot-panel').map(function(index, el){
return (!$(el).hasClass("muted")) << index;
}).toArray().reduce(function(acc, v){
return acc | v;
}, 0);
webrx_set_param("dmr_filter", filter);
}
function iosPlayButtonClick() function iosPlayButtonClick()
{ {
//On iOS, we can only start audio from a click or touch event. //On iOS, we can only start audio from a click or touch event.
@ -2409,6 +2456,7 @@ function pop_bottommost_panel(from)
function toggle_panel(what, on) function toggle_panel(what, on)
{ {
var item=e(what); var item=e(what);
if (!item) return;
if(typeof on !== "undefined") if(typeof on !== "undefined")
{ {
if(item.openwebrxHidden && !on) return; if(item.openwebrxHidden && !on) return;
@ -2472,7 +2520,7 @@ function place_panels(function_apply)
for(i=0;i<plist.length;i++) for(i=0;i<plist.length;i++)
{ {
c=plist[i]; c=plist[i];
if(c.className=="openwebrx-panel") if(c.className.indexOf("openwebrx-panel") >= 0)
{ {
if(c.openwebrxHidden) if(c.openwebrxHidden)
{ {

View File

@ -2,6 +2,8 @@ import os
import subprocess import subprocess
from functools import reduce from functools import reduce
from operator import and_ from operator import and_
import re
from distutils.version import LooseVersion
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -16,8 +18,9 @@ class FeatureDetector(object):
"rtl_sdr": [ "rtl_sdr" ], "rtl_sdr": [ "rtl_sdr" ],
"sdrplay": [ "rx_tools" ], "sdrplay": [ "rx_tools" ],
"hackrf": [ "hackrf_transfer" ], "hackrf": [ "hackrf_transfer" ],
"airspy": [ "airspy_rx" ],
"digital_voice_digiham": [ "digiham", "sox" ], "digital_voice_digiham": [ "digiham", "sox" ],
"digital_voice_dsd": [ "dsd", "sox" ], "digital_voice_dsd": [ "dsd", "sox", "digiham" ],
"packet": [ "direwolf" ] "packet": [ "direwolf" ]
} }
@ -82,19 +85,31 @@ class FeatureDetector(object):
def command_exists(self, command): def command_exists(self, command):
return os.system("which {0}".format(command)) == 0 return os.system("which {0}".format(command)) == 0
"""
To use DMR and YSF, the digiham package is required. You can find the package and installation instructions here:
https://github.com/jketterl/digiham
Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work.
If you have an older verison of digiham installed, please update it along with openwebrx.
As of now, we require version 0.2 of digiham.
"""
def has_digiham(self): def has_digiham(self):
# the digiham tools expect to be fed via stdin, they will block until their stdin is closed. required_version = LooseVersion("0.2")
def check_with_stdin(command):
digiham_version_regex = re.compile('^digiham version (.*)$')
def check_digiham_version(command):
try: try:
process = subprocess.Popen(command, stdin=subprocess.PIPE) process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE)
process.communicate("") version = LooseVersion(digiham_version_regex.match(process.stdout.readline().decode()).group(1))
return process.wait() == 0 process.wait(1)
return version >= required_version
except FileNotFoundError: except FileNotFoundError:
return False return False
return reduce(and_, return reduce(and_,
map( map(
check_with_stdin, check_digiham_version,
["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator"] ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator",
"digitalvoice_filter"]
), ),
True) True)
@ -105,4 +120,7 @@ class FeatureDetector(object):
return self.command_is_runnable("sox") return self.command_is_runnable("sox")
def has_direwolf(self): def has_direwolf(self):
return self.command_is_runnable("direwolf --help") return self.command_is_runnable("direwolf --help")
def has_airspy_rx(self):
return self.command_is_runnable("airspy_rx --help 2> /dev/null")

View File

@ -18,7 +18,9 @@ class Router(object):
{"route": "/status", "controller": StatusController}, {"route": "/status", "controller": StatusController},
{"regex": "/static/(.+)", "controller": AssetsController}, {"regex": "/static/(.+)", "controller": AssetsController},
{"route": "/ws/", "controller": WebSocketController}, {"route": "/ws/", "controller": WebSocketController},
{"regex": "(/favicon.ico)", "controller": AssetsController} {"regex": "(/favicon.ico)", "controller": AssetsController},
# backwards compatibility for the sdr.hu portal
{"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController}
] ]
def find_controller(self, path): def find_controller(self, path):
for m in Router.mappings: for m in Router.mappings:

View File

@ -64,11 +64,13 @@ class MetaParser(object):
enrichers = { enrichers = {
"DMR": DmrMetaEnricher() "DMR": DmrMetaEnricher()
} }
def __init__(self, handler): def __init__(self, handler):
self.handler = handler self.handler = handler
def parse(self, meta): def parse(self, meta):
fields = meta.split(";") fields = meta.split(";")
meta = {v[0] : "".join(v[1:]) for v in map(lambda x: x.split(":"), fields)} meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""}
if "protocol" in meta: if "protocol" in meta:
protocol = meta["protocol"] protocol = meta["protocol"]

View File

@ -257,6 +257,16 @@ class SdrplaySource(SdrSource):
def sleepOnRestart(self): def sleepOnRestart(self):
time.sleep(1) time.sleep(1)
class AirspySource(SdrSource):
def getCommand(self):
frequency = self.props['center_freq'] / 1e6
command = "airspy_rx"
command += " -f{0}".format(frequency)
command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}"
return command
def getFormatConversion(self):
return "csdr convert_s16_f"
class SpectrumThread(csdr.output): class SpectrumThread(csdr.output):
def __init__(self, sdrSource): def __init__(self, sdrSource):
self.sdrSource = sdrSource self.sdrSource = sdrSource
@ -339,7 +349,8 @@ class DspManager(csdr.output):
self.localProps = self.sdrSource.getProps().collect( self.localProps = self.sdrSource.getProps().collect(
"audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize",
"csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality" "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality",
"dmr_filter"
).defaults(PropertyManager.getSharedInstance()) ).defaults(PropertyManager.getSharedInstance())
self.dsp = csdr.dsp(self) self.dsp = csdr.dsp(self)
@ -366,7 +377,8 @@ class DspManager(csdr.output):
self.localProps.getProperty("low_cut").wire(set_low_cut), self.localProps.getProperty("low_cut").wire(set_low_cut),
self.localProps.getProperty("high_cut").wire(set_high_cut), self.localProps.getProperty("high_cut").wire(set_high_cut),
self.localProps.getProperty("mod").wire(self.dsp.set_demodulator), self.localProps.getProperty("mod").wire(self.dsp.set_demodulator),
self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality) self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality),
self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter)
] ]
self.dsp.set_offset_freq(0) self.dsp.set_offset_freq(0)

View File

@ -38,12 +38,16 @@ class WebSocketConnection(object):
# string-type messages are sent as text frames # string-type messages are sent as text frames
if (type(data) == str): if (type(data) == str):
header = self.get_header(len(data), 1) header = self.get_header(len(data), 1)
self.handler.wfile.write(header + data.encode('utf-8')) data_to_send = header + data.encode('utf-8')
self.handler.wfile.flush()
# anything else as binary # anything else as binary
else: else:
header = self.get_header(len(data), 2) header = self.get_header(len(data), 2)
self.handler.wfile.write(header + data) data_to_send = header + data
written = self.handler.wfile.write(data_to_send)
if (written != len(data_to_send)):
logger.error("incomplete write! closing socket!")
self.close()
else:
self.handler.wfile.flush() self.handler.wfile.flush()
def read_loop(self): def read_loop(self):
@ -78,7 +82,9 @@ class WebSocketConnection(object):
self.handler.wfile.write(header) self.handler.wfile.write(header)
self.handler.wfile.flush() self.handler.wfile.flush()
except ValueError: except ValueError:
logger.exception("while writing close frame:") logger.exception("ValueError while writing close frame:")
except OSError:
logger.exception("OSError while writing close frame:")
try: try:
self.handler.finish() self.handler.finish()

8
push.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
set -euxo pipefail
ARCH=$(uname -m)
for image in openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-full openwebrx; do
docker push jketterl/$image:$ARCH
done