diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..1aa044a --- /dev/null +++ b/build.sh @@ -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 . diff --git a/config_webrx.py b/config_webrx.py index 98bcb0a..0613278 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -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 # ################################################################################################# -# Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf" +# Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf", "airspy" sdrs = { "rtlsdr": { diff --git a/csdr.py b/csdr.py index 4825c64..87114d3 100755 --- a/csdr.py +++ b/csdr.py @@ -67,7 +67,8 @@ class dsp(object): self.secondary_fft_size = 1024 self.secondary_process_fft = None self.secondary_process_demod = None - self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", "iqtee2_pipe"] + self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", + "iqtee2_pipe", "dmr_control_pipe"] self.secondary_pipe_names=["secondary_shift_pipe"] self.secondary_offset_freq = 1000 self.unvoiced_quality = 1 @@ -107,16 +108,19 @@ class dsp(object): chain += "dsd -fd" elif which == "nxdn": chain += "dsd -fi" - chain += " -i - -o - -u {unvoiced_quality} -g 10 | " - 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 - " + chain += " -i - -o - -u {unvoiced_quality} -g -1 | CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f | " + max_gain = 5 # digiham modes else: - chain += "rrc_filter | csdr convert_f_s16 | gfsk_demodulator | " + chain += "rrc_filter | gfsk_demodulator | " 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": 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": chain += "csdr fmdemod_quadri_cf | " chain += last_decimation_block @@ -359,7 +363,7 @@ class dsp(object): actual_squelch = 0 if self.isDigitalVoice() else self.squelch_level if self.running: 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.modification_lock.release() @@ -370,6 +374,11 @@ class dsp(object): def get_unvoiced_quality(self): return self.unvoiced_quality + def set_dmr_filter(self, filter): + if self.dmr_control_pipe_file: + self.dmr_control_pipe_file.write("{0}\n".format(filter)) + self.dmr_control_pipe_file.flush() + def mkfifo(self,path): try: 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, squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000), - unvoiced_quality = self.get_unvoiced_quality(), 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) 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)) # open control pipes for csdr - if self.bpf_pipe != None: - self.bpf_pipe_file=open(self.bpf_pipe,"w") + if self.bpf_pipe: + self.bpf_pipe_file = open(self.bpf_pipe, "w") if self.shift_pipe: - self.shift_pipe_file=open(self.shift_pipe,"w") + self.shift_pipe_file = open(self.shift_pipe, "w") 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.modification_lock.release() @@ -475,6 +484,9 @@ class dsp(object): return raw.rstrip("\n") self.output.add_output("meta", read_meta) + if self.dmr_control_pipe: + self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w") + def stop(self): self.modification_lock.acquire() self.running = False diff --git a/docker/Dockerfiles/Dockerfile-airspy b/docker/Dockerfiles/Dockerfile-airspy new file mode 100644 index 0000000..09425b6 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-airspy @@ -0,0 +1,6 @@ +ARG ARCH +FROM openwebrx-base:$ARCH + +ADD docker/scripts/install-dependencies-airspy.sh / +RUN /install-dependencies-airspy.sh + diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base new file mode 100644 index 0000000..f58d41e --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-base @@ -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 diff --git a/docker/Dockerfiles/Dockerfile-full b/docker/Dockerfiles/Dockerfile-full new file mode 100644 index 0000000..e502515 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-full @@ -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 diff --git a/docker/Dockerfiles/Dockerfile-hackrf b/docker/Dockerfiles/Dockerfile-hackrf new file mode 100644 index 0000000..6afbc4a --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-hackrf @@ -0,0 +1,6 @@ +ARG ARCH +FROM openwebrx-base:$ARCH + +ADD docker/scripts/install-dependencies-hackrf.sh / +RUN /install-dependencies-hackrf.sh + diff --git a/docker/Dockerfiles/Dockerfile-rtlsdr b/docker/Dockerfiles/Dockerfile-rtlsdr new file mode 100644 index 0000000..3e14ab0 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-rtlsdr @@ -0,0 +1,6 @@ +ARG ARCH +FROM openwebrx-base:$ARCH + +ADD docker/scripts/install-dependencies-rtlsdr.sh / +RUN /install-dependencies-rtlsdr.sh + diff --git a/docker/Dockerfiles/Dockerfile-sdrplay b/docker/Dockerfiles/Dockerfile-sdrplay new file mode 100644 index 0000000..f9f2f1e --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-sdrplay @@ -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 + diff --git a/docker/Dockerfiles/Dockerfile-soapysdr b/docker/Dockerfiles/Dockerfile-soapysdr new file mode 100644 index 0000000..1150442 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-soapysdr @@ -0,0 +1,6 @@ +ARG ARCH +FROM openwebrx-base:$ARCH + +ADD docker/scripts/install-dependencies-soapysdr.sh / +RUN /install-dependencies-soapysdr.sh + diff --git a/docker/scripts/install-dependencies-airspy.sh b/docker/scripts/install-dependencies-airspy.sh new file mode 100755 index 0000000..e4fcac6 --- /dev/null +++ b/docker/scripts/install-dependencies-airspy.sh @@ -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 diff --git a/docker/scripts/install-dependencies-hackrf.sh b/docker/scripts/install-dependencies-hackrf.sh new file mode 100755 index 0000000..1a460cc --- /dev/null +++ b/docker/scripts/install-dependencies-hackrf.sh @@ -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 diff --git a/docker/scripts/install-dependencies-rtlsdr.sh b/docker/scripts/install-dependencies-rtlsdr.sh new file mode 100755 index 0000000..09f8676 --- /dev/null +++ b/docker/scripts/install-dependencies-rtlsdr.sh @@ -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 diff --git a/docker/scripts/install-dependencies-sdrplay.sh b/docker/scripts/install-dependencies-sdrplay.sh new file mode 100755 index 0000000..3ac29cc --- /dev/null +++ b/docker/scripts/install-dependencies-sdrplay.sh @@ -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 diff --git a/docker/scripts/install-dependencies-soapysdr.sh b/docker/scripts/install-dependencies-soapysdr.sh new file mode 100755 index 0000000..9e598c7 --- /dev/null +++ b/docker/scripts/install-dependencies-soapysdr.sh @@ -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 diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh new file mode 100755 index 0000000..48136b2 --- /dev/null +++ b/docker/scripts/install-dependencies.sh @@ -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 + #include + #include ++#include + #include + #include + #include +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 + #include + #include ++#include + + 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 + #include + #include ++#include + #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 diff --git a/docker/scripts/install-lib.armv7l.patch b/docker/scripts/install-lib.armv7l.patch new file mode 100644 index 0000000..0306c2b --- /dev/null +++ b/docker/scripts/install-lib.armv7l.patch @@ -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 diff --git a/docker/scripts/install-lib.x86_64.patch b/docker/scripts/install-lib.x86_64.patch new file mode 100644 index 0000000..588f14e --- /dev/null +++ b/docker/scripts/install-lib.x86_64.patch @@ -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..." + diff --git a/docker/scripts/run.sh b/docker/scripts/run.sh new file mode 100755 index 0000000..a9034c5 --- /dev/null +++ b/docker/scripts/run.sh @@ -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" + diff --git a/htdocs/gfx/google_maps_pin.svg b/htdocs/gfx/google_maps_pin.svg new file mode 100644 index 0000000..2c54fe1 --- /dev/null +++ b/htdocs/gfx/google_maps_pin.svg @@ -0,0 +1,77 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/htdocs/gfx/openwebrx-avatar.png b/htdocs/gfx/openwebrx-avatar.png index 7e9736f..2ae083f 100644 Binary files a/htdocs/gfx/openwebrx-avatar.png and b/htdocs/gfx/openwebrx-avatar.png differ diff --git a/htdocs/gfx/openwebrx-directcall.png b/htdocs/gfx/openwebrx-directcall.png new file mode 100644 index 0000000..2d74713 Binary files /dev/null and b/htdocs/gfx/openwebrx-directcall.png differ diff --git a/htdocs/gfx/openwebrx-groupcall.png b/htdocs/gfx/openwebrx-groupcall.png new file mode 100644 index 0000000..5d61a4c Binary files /dev/null and b/htdocs/gfx/openwebrx-groupcall.png differ diff --git a/htdocs/gfx/openwebrx-mute.png b/htdocs/gfx/openwebrx-mute.png new file mode 100644 index 0000000..23da7bb Binary files /dev/null and b/htdocs/gfx/openwebrx-mute.png differ diff --git a/htdocs/index.html b/htdocs/index.html index 7e7b1e5..022e2ac 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -171,7 +171,34 @@ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Timeslot 1
+
+
+
+
+
+
+
Timeslot 2
+
+
+
+
+
+
diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css index 4736bf3..5624d79 100644 --- a/htdocs/openwebrx.css +++ b/htdocs/openwebrx.css @@ -928,3 +928,88 @@ img.openwebrx-mirror-img border-color: Red; } +.openwebrx-meta-slot { + width: 145px; + height: 196px; + float: left; + margin-right: 10px; + + background-color: #676767; + padding: 2px 0; + color: #333; + + text-align: center; + position: relative; +} + +.openwebrx-meta-slot, .openwebrx-meta-slot.muted:before { + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.openwebrx-meta-slot.muted:before { + display: block; + content: ""; + background-image: url("gfx/openwebrx-mute.png"); + width:100%; + height:133px; + background-position: center; + background-repeat: no-repeat; + cursor: pointer; + + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: rgba(0,0,0,.3); +} + +.openwebrx-meta-slot.active { + background-color: #95bbdf; +} + +.openwebrx-meta-slot.sync .openwebrx-dmr-slot:before { + content:""; + display: inline-block; + margin: 0 5px; + width: 12px; + height: 12px; + background-color: #ABFF00; + border-radius: 50%; + box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px; +} + +.openwebrx-meta-slot:last-child { + margin-right: 0; +} + +.openwebrx-meta-slot .openwebrx-meta-user-image { + width:100%; + height:133px; + background-position: center; + background-repeat: no-repeat; +} + +.openwebrx-meta-slot.active .openwebrx-meta-user-image { + background-image: url("gfx/openwebrx-directcall.png"); +} + +.openwebrx-meta-slot.active .openwebrx-meta-user-image.group { + background-image: url("gfx/openwebrx-groupcall.png"); +} + +.openwebrx-dmr-timeslot-panel * { + cursor: pointer; +} + +.openwebrx-maps-pin { + background-image: url("gfx/google_maps_pin.svg"); + background-position: center; + background-repeat: no-repeat; + width: 15px; + height: 15px; + background-size: contain; + display: inline-block; +} diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 08bc951..505aef2 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -624,7 +624,8 @@ function demodulator_analog_replace(subtype, for_digital) } demodulator_add(new demodulator_default_analog(temp_offset,subtype)); demodulator_buttons_update(); - clear_metadata(); + hide_digitalvoice_panels(); + toggle_panel("openwebrx-panel-metadata-" + subtype, true); } function demodulator_set_offset_frequency(which,to_what) @@ -1315,56 +1316,78 @@ function on_ws_recv(evt) } function update_metadata(meta) { - 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.additional && meta.additional.callsign) { - html += ' Source: ' + meta.additional.callsign; - if (meta.additional.fname) { - html += ' (' + meta.additional.fname + ')'; + var el = $("#openwebrx-panel-metadata-dmr .openwebrx-dmr-timeslot-panel").get(meta.slot); + var id = ""; + var name = ""; + var target = ""; + var group = false; + $(el)[meta.sync ? "addClass" : "removeClass"]("sync"); + if (meta.sync && meta.sync == "voice") { + id = (meta.additional && meta.additional.callsign) || meta.source || ""; + name = (meta.additional && meta.additional.fname) || ""; + if (meta.type == "group") { + target = "Talkgroup: "; + group = true; } - } else if (meta.source) { - html += ' Source: ' + meta.source; + if (meta.type == "direct") target = "Direct: "; + target += meta.target || ""; + $(el).addClass("active"); + } else { + $(el).removeClass("active"); } - if (meta.target) html += ' 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; - }; + $(el).find(".openwebrx-dmr-id").text(id); + $(el).find(".openwebrx-dmr-name").text(name); + $(el).find(".openwebrx-dmr-target").text(target); + $(el).find(".openwebrx-meta-user-image")[group ? "addClass" : "removeClass"]("group"); + } else { + clear_metadata(); } break; case 'YSF': - var strings = []; - if (meta.mode) strings.push("Mode: " + meta.mode); - if (meta.source) strings.push("Source: " + meta.source); - if (meta.target) strings.push("Destination: " + meta.target); - if (meta.up) strings.push("Up: " + meta.up); - if (meta.down) strings.push("Down: " + meta.down); - var html = strings.join(' '); - update = function(_, el) { - el.innerHTML = html; + var el = $("#openwebrx-panel-metadata-ysf"); + + var mode = " " + var source = ""; + var up = ""; + var down = ""; + if (meta.mode && meta.mode != "") { + mode = "Mode: " + meta.mode; + source = meta.source || ""; + if (meta.lat && meta.lon) { + source = "" + source; + } + up = meta.up ? "Up: " + meta.up : ""; + down = meta.down ? "Down: " + meta.down : ""; + $(el).find(".openwebrx-meta-slot").addClass("active"); + } else { + $(el).find(".openwebrx-meta-slot").removeClass("active"); } + $(el).find(".openwebrx-ysf-mode").text(mode); + $(el).find(".openwebrx-ysf-source").html(source); + $(el).find(".openwebrx-ysf-up").text(up); + $(el).find(".openwebrx-ysf-down").text(down); + break; + } else { + clear_metadata(); } - $('.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() { - 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) @@ -1817,7 +1840,12 @@ String.prototype.startswith=function(str){ return this.indexOf(str) == 0; }; //h 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)) 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); @@ -2311,7 +2339,7 @@ function openwebrx_init() init_rx_photo(); open_websocket(); secondary_demod_init(); - clear_metadata(); + digimodes_init(); place_panels(first_show_panel); window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); 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() { //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) { var item=e(what); + if (!item) return; if(typeof on !== "undefined") { if(item.openwebrxHidden && !on) return; @@ -2472,7 +2520,7 @@ function place_panels(function_apply) for(i=0;i= 0) { if(c.openwebrxHidden) { diff --git a/owrx/feature.py b/owrx/feature.py index 8233cfb..ff72fe0 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -2,6 +2,8 @@ import os import subprocess from functools import reduce from operator import and_ +import re +from distutils.version import LooseVersion import logging logger = logging.getLogger(__name__) @@ -16,8 +18,9 @@ class FeatureDetector(object): "rtl_sdr": [ "rtl_sdr" ], "sdrplay": [ "rx_tools" ], "hackrf": [ "hackrf_transfer" ], + "airspy": [ "airspy_rx" ], "digital_voice_digiham": [ "digiham", "sox" ], - "digital_voice_dsd": [ "dsd", "sox" ], + "digital_voice_dsd": [ "dsd", "sox", "digiham" ], "packet": [ "direwolf" ] } @@ -82,19 +85,31 @@ class FeatureDetector(object): def command_exists(self, command): return os.system("which {0}".format(command)) == 0 + """ + To use DMR and YSF, the digiham package is required. You can find the package and installation instructions here: + https://github.com/jketterl/digiham + + Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work. + If you have an older verison of digiham installed, please update it along with openwebrx. + As of now, we require version 0.2 of digiham. + """ def has_digiham(self): - # the digiham tools expect to be fed via stdin, they will block until their stdin is closed. - def check_with_stdin(command): + required_version = LooseVersion("0.2") + + digiham_version_regex = re.compile('^digiham version (.*)$') + def check_digiham_version(command): try: - process = subprocess.Popen(command, stdin=subprocess.PIPE) - process.communicate("") - return process.wait() == 0 + process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE) + version = LooseVersion(digiham_version_regex.match(process.stdout.readline().decode()).group(1)) + process.wait(1) + return version >= required_version except FileNotFoundError: return False return reduce(and_, map( - check_with_stdin, - ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator"] + check_digiham_version, + ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator", + "digitalvoice_filter"] ), True) @@ -105,4 +120,7 @@ class FeatureDetector(object): return self.command_is_runnable("sox") def has_direwolf(self): - return self.command_is_runnable("direwolf --help") \ No newline at end of file + return self.command_is_runnable("direwolf --help") + + def has_airspy_rx(self): + return self.command_is_runnable("airspy_rx --help 2> /dev/null") diff --git a/owrx/http.py b/owrx/http.py index 7012f0e..ca7d357 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -18,7 +18,9 @@ class Router(object): {"route": "/status", "controller": StatusController}, {"regex": "/static/(.+)", "controller": AssetsController}, {"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): for m in Router.mappings: diff --git a/owrx/meta.py b/owrx/meta.py index e215d89..ec4966a 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -64,11 +64,13 @@ class MetaParser(object): enrichers = { "DMR": DmrMetaEnricher() } + def __init__(self, handler): self.handler = handler + def parse(self, meta): fields = meta.split(";") - meta = {v[0] : "".join(v[1:]) for v in map(lambda x: x.split(":"), fields)} + meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""} if "protocol" in meta: protocol = meta["protocol"] diff --git a/owrx/source.py b/owrx/source.py index c390dbf..7d62388 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -257,6 +257,16 @@ class SdrplaySource(SdrSource): def sleepOnRestart(self): time.sleep(1) +class AirspySource(SdrSource): + def getCommand(self): + frequency = self.props['center_freq'] / 1e6 + command = "airspy_rx" + command += " -f{0}".format(frequency) + command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}" + return command + def getFormatConversion(self): + return "csdr convert_s16_f" + class SpectrumThread(csdr.output): def __init__(self, sdrSource): self.sdrSource = sdrSource @@ -339,7 +349,8 @@ class DspManager(csdr.output): self.localProps = self.sdrSource.getProps().collect( "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", - "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality" + "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality", + "dmr_filter" ).defaults(PropertyManager.getSharedInstance()) 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("high_cut").wire(set_high_cut), self.localProps.getProperty("mod").wire(self.dsp.set_demodulator), - self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality) + self.localProps.getProperty("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) diff --git a/owrx/websocket.py b/owrx/websocket.py index d0385b8..a247b2a 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -38,12 +38,16 @@ class WebSocketConnection(object): # 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() + data_to_send = header + data.encode('utf-8') # anything else as binary else: 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() def read_loop(self): @@ -78,7 +82,9 @@ class WebSocketConnection(object): self.handler.wfile.write(header) self.handler.wfile.flush() 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: self.handler.finish() diff --git a/push.sh b/push.sh new file mode 100755 index 0000000..0288c81 --- /dev/null +++ b/push.sh @@ -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