57 Commits

Author SHA1 Message Date
0145cf5668 update release versions for docker 2022-06-16 22:48:57 +02:00
921fb23c8d prepare release of version 1.2.0 2022-06-15 18:28:50 +02:00
eb3ec5dc36 update m17-cxx-demod to version 2.3 2022-06-15 18:25:26 +02:00
35ad4712bb disable PPM input for devices that don't support it 2022-06-09 20:25:29 +02:00
fe7f2317de add a quick note about HTML being supported 2022-06-09 19:22:45 +02:00
b5bbdae317 fix failed logins for path-routed environemnts 2022-06-09 18:43:54 +02:00
cec4e326c8 prevent "None" showing up in text inputs 2022-06-09 17:24:53 +02:00
eccbdc1655 update libraries in docker 2022-06-01 18:04:54 +02:00
08485f255a add return codes 2022-06-01 17:58:06 +02:00
be8e35cbcf output more descriptive output when dependencies fail 2022-06-01 17:11:45 +02:00
843dde1a68 check for csdr & digiham python bindings 2022-06-01 16:43:18 +02:00
f018ef1d81 turn off debug logging for now 2022-06-01 16:19:24 +02:00
6b43ddf920 add udev dependencies for codecserver 2022-01-24 11:38:06 +01:00
242ec5dfd0 update docker dependencies 2022-01-24 11:03:15 +01:00
b354f38bfb add js8call as a recommended package (available in bullseye now) 2022-01-18 16:55:19 +01:00
983aa8cebc add bladerf docker image build 2022-01-12 18:01:25 +01:00
619f1254fd update wsjt-x to version 2.5.4 2022-01-12 17:59:45 +01:00
b5b52770ee update changelogs 2022-01-12 16:00:16 +01:00
7fd98c8c5c add support for blade rf devices 2022-01-12 15:48:06 +01:00
39bfba673b catch error resulting from monitor race condition 2022-01-11 21:56:16 +01:00
5adb53d990 distinguish between error condition and normal socket close 2022-01-11 19:57:52 +01:00
f3dcf5c320 check closed condition after aquiring the lock to avoid deadlocks 2022-01-05 17:55:46 +01:00
2ce7d943fa fix a client counting bug by deferring client instantiation 2022-01-03 15:19:12 +01:00
60f57bf206 add codecserver to recommended packages 2021-12-29 14:26:30 +01:00
221e0f232b try to avoid "can only be started once" error 2021-12-27 16:37:10 +01:00
46c78f6463 avoid demodulator concurrency
* this frees up resources used by the current demodulator before
  starting a new one
* this addresses an issue where users of single-channel AMBE sticks
  could not seamlessly switch between digital modes
2021-12-23 16:32:51 +01:00
40c68933e1 add preliminary parsing and display of M17 metadata 2021-12-21 21:18:17 +01:00
81b8f183c2 update connector with bias_tee fixes 2021-12-20 16:11:51 +01:00
03f0faf378 update digiham / pydigiham dependencies 2021-12-18 17:54:27 +01:00
f316b2c8ca allow latitude and longitude to be 0 in location-picker 2021-12-14 12:46:25 +01:00
6c3ef7a6ed Merge pull request #281 from chrismrutledge/patch-1
Update bands.json
2021-12-13 14:21:47 +01:00
4ce3816f48 show codecserver errors in the client 2021-12-13 13:26:47 +01:00
397155983d improve handling of failed devices 2021-12-06 15:50:03 +01:00
9c28143dfb add debugging to the feature detection system 2021-12-01 19:22:48 +01:00
ed354cfa6f Update bands.json
80M WSPR frequency change and additional 60M frequency according to wsprnet.org.
2021-11-28 07:02:16 -06:00
dcdfe7969a fix sample rate updates for secondary demods 2021-11-08 17:52:37 +01:00
6d414698e8 update to wsjt-x 2.5.2 2021-11-05 02:20:49 +01:00
70cf4557f7 update to wsjt-x 2.5.1 2021-11-02 17:31:58 +01:00
b0e18286df update connector 2021-11-02 16:11:19 +01:00
85c7a05978 use ImportError for python 3.5 compatibility 2021-10-27 18:33:23 +02:00
33c8e34456 use the resume call before pumping data from a reader 2021-10-26 16:40:38 +02:00
4bc6608e87 update csdr in docker 2021-10-25 14:15:32 +02:00
f967a8d87a catch exceptions while parsing ax25 frames 2021-10-22 15:07:42 +02:00
d757b817b1 make digimodes work in start_mod again 2021-10-15 16:41:07 +02:00
9f89a21cfb remove psk31 character animation since it's killing the client 2021-10-15 15:57:27 +02:00
aaf696e8d7 Merge pull request #273 from doccodyblue/feature/266-normalize-prometheus-metric-names
Feature/266 normalize prometheus metric names
2021-10-04 16:00:34 +02:00
efa305eeec normalize metric label to match prometheus data-model guide 2021-10-03 08:48:40 +02:00
eb43e39a81 normalize metric label to match prometheus data-model guide 2021-10-03 08:39:57 +02:00
c4687816c1 update docker to debian bullseye 2021-10-01 16:23:47 +02:00
8cce5bd889 add metrics for pocsag 2021-10-01 00:52:32 +02:00
66dd4b4581 update list of supported modes for pskreporter 2021-10-01 00:09:20 +02:00
9689ce5202 catch invalid config values for enum dropdowns and reset to default 2021-09-30 23:32:46 +02:00
818b9d87b8 add a validator that prevents invalid locations 2021-09-30 23:26:26 +02:00
0f2aca62f3 code style 2021-09-30 23:09:22 +02:00
1e57fb4609 expect a broken pipe 2021-09-30 23:04:59 +02:00
0b64b4ac97 handle errors when gps coordinates are out of range 2021-09-30 23:03:21 +02:00
460bada88f update owrx libraries 2021-09-30 01:37:10 +02:00
59 changed files with 672 additions and 294 deletions

View File

@ -1,6 +1,9 @@
**unreleased**
**1.2.0**
- Major rewrite of all demodulation components to make use of the new csdr/pycsdr and digiham/pydigiham demodulator
modules
- Preliminary display of M17 callsign information
- New devices supported:
- Blade RF
**1.1.0**
- Reworked most graphical elements as SVGs for faster loadtimes and crispier display on hi-dpi displays

View File

@ -42,7 +42,7 @@
"frequencies": {
"bpsk31": 3580000,
"ft8": 3573000,
"wspr": 3592600,
"wspr": 3568600,
"jt65": 3570000,
"jt9": 3572000,
"ft4": [3568000, 3575000],
@ -56,7 +56,7 @@
"upper_bound": 5366500,
"frequencies": {
"ft8": 5357000,
"wspr": 5364700
"wspr": [5287200, 5364700]
},
"tags": ["hamradio"]
},
@ -364,4 +364,4 @@
"upper_bound": 446200000,
"tags": ["public"]
}
]
]

View File

@ -67,3 +67,7 @@ class SecondaryDemodulator(Chain):
class ServiceDemodulator(SecondaryDemodulator, FixedAudioRateChain, metaclass=ABCMeta):
pass
class DemodulatorError(Exception):
pass

View File

@ -1,8 +1,8 @@
from csdr.chain.demodulator import BaseDemodulatorChain, FixedAudioRateChain, FixedIfSampleRateChain, DialFrequencyReceiver, MetaProvider, SlotFilterChain
from csdr.chain.demodulator import BaseDemodulatorChain, FixedAudioRateChain, FixedIfSampleRateChain, DialFrequencyReceiver, MetaProvider, SlotFilterChain, DemodulatorError
from pycsdr.modules import FmDemod, Agc, Writer, Buffer
from pycsdr.types import Format
from digiham.modules import DstarDecoder, DcBlock, FskDemodulator, GfskDemodulator, DigitalVoiceFilter, MbeSynthesizer, NarrowRrcFilter, NxdnDecoder, DmrDecoder, WideRrcFilter, YsfDecoder
from digiham.ambe import Modes
from digiham.ambe import Modes, ServerError
from owrx.meta import MetaParser
@ -17,10 +17,16 @@ class DigihamChain(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateC
workers = [FmDemod(), DcBlock()]
if filter is not None:
workers += [filter]
try:
mbeSynthesizer = MbeSynthesizer(mbeMode, codecserver)
except ConnectionError as ce:
raise DemodulatorError("Connection to codecserver failed: {}".format(ce))
except ServerError as se:
raise DemodulatorError("Codecserver error: {}".format(se))
workers += [
fskDemodulator,
decoder,
MbeSynthesizer(mbeMode, codecserver),
mbeSynthesizer,
DigitalVoiceFilter(),
agc
]

View File

@ -45,13 +45,14 @@ class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver):
self.parser.setDialFrequency(frequency)
class PocsagDemodulator(ServiceDemodulator):
class PocsagDemodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self):
self.parser = PocsagParser()
workers = [
FmDemod(),
FskDemodulator(samplesPerSymbol=40, invert=True),
PocsagDecoder(),
PocsagParser(),
self.parser,
]
super().__init__(workers)
@ -61,6 +62,9 @@ class PocsagDemodulator(ServiceDemodulator):
def getFixedAudioRate(self) -> int:
return 48000
def setDialFrequency(self, frequency: int) -> None:
self.parser.setDialFrequency(frequency)
class PskDemodulator(SecondaryDemodulator, SecondarySelectorChain):
def __init__(self, baudRate: float):

14
csdr/chain/dummy.py Normal file
View File

@ -0,0 +1,14 @@
from pycsdr.types import Format
from csdr.chain import Module
class DummyDemodulator(Module):
def __init__(self, outputFormat: Format):
self.outputFormat = outputFormat
super().__init__()
def getInputFormat(self) -> Format:
return Format.COMPLEX_FLOAT
def getOutputFormat(self) -> Format:
return self.outputFormat

View File

@ -1,18 +1,19 @@
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider
from csdr.module.m17 import M17Module
from pycsdr.modules import FmDemod, Limit, Convert
from pycsdr.modules import FmDemod, Limit, Convert, Writer
from pycsdr.types import Format
from digiham.modules import DcBlock
class M17(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain):
class M17(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider):
def __init__(self):
self.module = M17Module()
workers = [
FmDemod(),
DcBlock(),
Limit(),
Convert(Format.FLOAT, Format.SHORT),
M17Module(),
self.module,
]
super().__init__(workers)
@ -24,3 +25,6 @@ class M17(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain):
def supportsSquelch(self) -> bool:
return False
def setMetaWriter(self, writer: Writer) -> None:
self.module.setMetaWriter(writer)

View File

@ -37,6 +37,8 @@ class Module(BaseModule, metaclass=ABCMeta):
data = read()
except ValueError:
pass
except BrokenPipeError:
break
if data is None or isinstance(data, bytes) and len(data) == 0:
break
write(data)
@ -116,8 +118,13 @@ class PopenModule(AutoStartModule, metaclass=ABCMeta):
def getCommand(self):
pass
def _getProcess(self):
return Popen(self.getCommand(), stdin=PIPE, stdout=PIPE)
def start(self):
self.process = Popen(self.getCommand(), stdin=PIPE, stdout=PIPE)
self.process = self._getProcess()
# resume in case the reader has been stop()ed before
self.reader.resume()
Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start()
Thread(target=self.pump(partial(self.process.stdout.read, 1024), self.writer.write)).start()

View File

@ -1,8 +1,20 @@
from csdr.module import PopenModule
from pycsdr.types import Format
from pycsdr.modules import Writer
from subprocess import Popen, PIPE
from threading import Thread
import re
import pickle
class M17Module(PopenModule):
lsfRegex = re.compile("SRC: ([a-zA-Z0-9]+), DEST: ([a-zA-Z0-9]+)")
def __init__(self):
super().__init__()
self.metawriter = None
def getInputFormat(self) -> Format:
return Format.SHORT
@ -10,4 +22,37 @@ class M17Module(PopenModule):
return Format.SHORT
def getCommand(self):
return ["m17-demod"]
return ["m17-demod", "-l"]
def _getProcess(self):
return Popen(self.getCommand(), stdin=PIPE, stdout=PIPE, stderr=PIPE)
def start(self):
super().start()
Thread(target=self._readOutput).start()
def _readOutput(self):
while True:
line = self.process.stderr.readline()
if not line:
break
self.parseOutput(line.decode())
def parseOutput(self, line):
if self.metawriter is None:
return
matches = self.lsfRegex.match(line)
msg = {"protocol": "M17"}
if matches:
# fake sync
msg["sync"] = "voice"
msg["source"] = matches.group(1)
msg["destination"] = matches.group(2)
elif line.startswith("EOS"):
pass
else:
return
self.metawriter.write(pickle.dumps(msg))
def setMetaWriter(self, writer: Writer) -> None:
self.metawriter = writer

7
debian/changelog vendored
View File

@ -1,9 +1,12 @@
openwebrx (1.2.0) UNRELEASED; urgency=low
openwebrx (1.2.0) bullseye jammy; urgency=low
* Major rewrite of all demodulation components to make use of the new
csdr/pycsdr and digiham/pydigiham demodulator modules
* Preliminary display of M17 callsign information
* New devices supported:
- Blade RF
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 03 Aug 2021 13:54:00 +0000
-- Jakob Ketterl <jakob.ketterl@gmx.de> Wed, 15 Jun 2022 16:20:00 +0000
openwebrx (1.1.0) buster hirsute; urgency=low

2
debian/control vendored
View File

@ -11,6 +11,6 @@ Vcs-Git: https://github.com/jketterl/openwebrx.git
Package: openwebrx
Architecture: all
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, owrx-connector (>= 0.5), soapysdr-tools, python3-csdr (>= 0.18), ${python3:Depends}, ${misc:Depends}
Recommends: python3-digiham (>= 0.6), direwolf (>= 1.4), wsjtx, runds-connector (>= 0.2), hpsdrconnector, aprs-symbols, m17-demod, js8call, python3-js8py (>= 0.1), nmux (>= 0.18)
Recommends: python3-digiham (>= 0.6), direwolf (>= 1.4), wsjtx, js8call, runds-connector (>= 0.2), hpsdrconnector, aprs-symbols, m17-demod, js8call, python3-js8py (>= 0.1), nmux (>= 0.18), codecserver (>= 0.1)
Description: multi-user web sdr
Open source, multi-user SDR receiver with a web interface

View File

@ -2,7 +2,7 @@
set -euo pipefail
ARCH=$(uname -m)
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-rtltcp openwebrx-runds openwebrx-hpsdr openwebrx-full openwebrx"
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-rtltcp openwebrx-runds openwebrx-hpsdr openwebrx-bladerf openwebrx-full openwebrx"
ALL_ARCHS="x86_64 armv7l aarch64"
TAG=${TAG:-"latest"}
ARCHTAG="${TAG}-${ARCH}"

View File

@ -1,4 +1,4 @@
FROM debian:buster-slim
FROM debian:bullseye-slim
COPY docker/files/js8call/js8call-hamlib.patch \
docker/files/wsjtx/wsjtx.patch \

View File

@ -0,0 +1,8 @@
ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG
COPY docker/scripts/install-dependencies-bladerf.sh /
RUN /install-dependencies-bladerf.sh &&\
rm /install-dependencies-bladerf.sh
COPY . /opt/openwebrx

View File

@ -19,6 +19,7 @@ RUN /install-dependencies-rtlsdr.sh &&\
/install-dependencies-radioberry.sh &&\
/install-dependencies-uhd.sh &&\
/install-dependencies-hpsdr.sh &&\
/install-dependencies-bladerf.sh &&\
/install-connectors.sh &&\
/install-dependencies-runds.sh &&\
rm /install-dependencies-*.sh &&\

View File

@ -1,6 +1,6 @@
diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
--- wsjtx-orig/CMakeLists.txt 2021-09-28 14:36:01.731488130 +0200
+++ wsjtx/CMakeLists.txt 2021-09-28 15:51:30.136197625 +0200
--- wsjtx-orig/CMakeLists.txt 2021-11-02 16:34:09.361811689 +0100
+++ wsjtx/CMakeLists.txt 2021-11-02 16:38:36.696088115 +0100
@@ -122,7 +122,7 @@
option (WSJT_QDEBUG_TO_FILE "Redirect Qt debuging messages to a trace file.")
option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON)
@ -87,16 +87,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
)
set (wsjt_qtmm_CXXSRCS
@@ -884,8 +815,6 @@
check_type_size (CACHE_ALL HAMLIB_OLD_CACHING)
check_symbol_exists (rig_set_cache_timeout_ms "hamlib/rig.h" HAVE_HAMLIB_CACHING)
-find_package (Portaudio REQUIRED)
-
find_package (Usb REQUIRED)
#
@@ -1081,9 +1010,6 @@
@@ -1079,9 +1010,6 @@
if (WSJT_GENERATE_DOCS)
add_subdirectory (doc)
endif (WSJT_GENERATE_DOCS)
@ -106,7 +97,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# build a library of package functionality (without and optionally with OpenMP support)
add_library (wsjt_cxx STATIC ${wsjt_CSRCS} ${wsjt_CXXSRCS})
@@ -1341,10 +1267,7 @@
@@ -1340,10 +1268,7 @@
add_library (wsjt_qt STATIC ${wsjt_qt_CXXSRCS} ${wsjt_qt_GENUISRCS} ${GENAXSRCS})
# set wsjtx_udp exports to static variants
target_compile_definitions (wsjt_qt PUBLIC UDP_STATIC_DEFINE)
@ -118,7 +109,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# build a library of package Qt functionality used in Fortran utilities
add_library (fort_qt STATIC ${fort_qt_CXXSRCS})
@@ -1408,60 +1331,6 @@
@@ -1408,60 +1333,6 @@
add_subdirectory (map65)
endif ()
@ -179,7 +170,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# make a library for WSJT-X UDP servers
# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS})
add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS})
@@ -1501,47 +1370,9 @@
@@ -1501,47 +1372,9 @@
add_executable (wsjtx_app_version AppVersion/AppVersion.cpp ${wsjtx_app_version_VERSION_RESOURCES})
target_link_libraries (wsjtx_app_version wsjt_qt)
@ -227,7 +218,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# install (TARGETS wsjtx_udp EXPORT udp
# RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
@@ -1560,12 +1391,7 @@
@@ -1560,12 +1393,7 @@
# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx
# )
@ -241,7 +232,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
)
@@ -1578,38 +1404,6 @@
@@ -1578,38 +1406,6 @@
)
endif(WSJT_BUILD_UTILS)
@ -280,7 +271,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
install (FILES
cty.dat
cty.dat_copyright.txt
@@ -1618,13 +1412,6 @@
@@ -1618,13 +1414,6 @@
#COMPONENT runtime
)
@ -294,7 +285,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
#
# Mac installer files
#
@@ -1676,22 +1463,6 @@
@@ -1676,22 +1465,6 @@
"${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
)
@ -317,4 +308,3 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
if (APPLE)
set (CMAKE_POSTFLIGHT_SCRIPT
"${wsjtx_BINARY_DIR}/postflight.sh")
Only in wsjtx: .idea

View File

@ -18,13 +18,13 @@ function cmakebuild() {
cd /tmp
BUILD_PACKAGES="git cmake make gcc g++"
BUILD_PACKAGES="git cmake make gcc g++ libsamplerate-dev libfftw3-dev"
apt-get update
apt-get -y install --no-install-recommends $BUILD_PACKAGES
git clone https://github.com/jketterl/owrx_connector.git
cmakebuild owrx_connector 0.5.0
cmakebuild owrx_connector 0.6.0
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean

View File

@ -0,0 +1,36 @@
#!/bin/bash
set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb-1.0-0"
BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev"
apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/Nuand/bladeRF.git
cmakebuild bladeRF 2021.10
git clone https://github.com/pothosware/SoapyBladeRF.git
# latest from master as of 2022-01-12
cmakebuild SoapyBladeRF 70505a5cdf8c9deabc4af3eb3384aa82a7b6f021
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -25,7 +25,7 @@ apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/jketterl/runds_connector.git
cmakebuild runds_connector 0.2.0
cmakebuild runds_connector 0.2.1
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean

View File

@ -18,17 +18,16 @@ function cmakebuild() {
cd /tmp
STATIC_PACKAGES="libusb-1.0.0 libboost-chrono1.67.0 libboost-date-time1.67.0 libboost-filesystem1.67.0 libboost-program-options1.67.0 libboost-regex1.67.0 libboost-test1.67.0 libboost-serialization1.67.0 libboost-thread1.67.0 libboost-system1.67.0 python3-numpy python3-mako"
STATIC_PACKAGES="libusb-1.0.0 libboost-chrono1.74.0 libboost-date-time1.74.0 libboost-filesystem1.74.0 libboost-program-options1.74.0 libboost-regex1.74.0 libboost-test1.74.0 libboost-serialization1.74.0 libboost-thread1.74.0 libboost-system1.74.0 python3-numpy python3-mako"
BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev libboost-dev libboost-chrono-dev libboost-date-time-dev libboost-filesystem-dev libboost-program-options-dev libboost-regex-dev libboost-test-dev libboost-serialization-dev libboost-thread-dev libboost-system-dev"
apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/EttusResearch/uhd.git
# 3.15.0.0 Release
mkdir -p uhd/host/build
cd uhd/host/build
git checkout v3.15.0.0
git checkout v4.1.0.4
# see https://github.com/EttusResearch/uhd/issues/350
case `uname -m` in
arm*)

View File

@ -18,8 +18,8 @@ function cmakebuild() {
cd /tmp
STATIC_PACKAGES="libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline7 libgfortran4 libgomp1 libasound2 libudev1 ca-certificates libpulse0 libfaad2 libopus0 libboost-program-options1.67.0 libboost-log1.67.0"
BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-default libfaad-dev libopus-dev libboost-dev libboost-program-options-dev libboost-log-dev libboost-regex-dev"
STATIC_PACKAGES="libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline8 libgfortran5 libgomp1 libasound2 libudev1 ca-certificates libpulse0 libfaad2 libopus0 libboost-program-options1.74.0 libboost-log1.74.0"
BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-qmake libfaad-dev libopus-dev libboost-dev libboost-program-options-dev libboost-log-dev libboost-regex-dev libpulse-dev"
apt-get update
apt-get -y install auto-apt-proxy
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
@ -48,10 +48,10 @@ tar xfz ${JS8CALL_TGZ}
# patch allows us to build against the packaged hamlib
patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch
rm /js8call-hamlib.patch
CMAKE_ARGS="-D CMAKE_CXX_FLAGS=-DJS8_USE_HAMLIB_THREE" cmakebuild ${JS8CALL_DIR}
cmakebuild ${JS8CALL_DIR}
rm ${JS8CALL_TGZ}
WSJT_DIR=wsjtx-2.5.0
WSJT_DIR=wsjtx-2.5.4
WSJT_TGZ=${WSJT_DIR}.tgz
wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ}
tar xfz ${WSJT_TGZ}
@ -102,7 +102,7 @@ rm -rf dream
rm dream-2.1.1-svn808.tar.gz
git clone https://github.com/mobilinkd/m17-cxx-demod.git
cmakebuild m17-cxx-demod v2.2
cmakebuild m17-cxx-demod v2.3
git clone https://github.com/hessu/aprs-symbols /usr/share/aprs-symbols
pushd /usr/share/aprs-symbols

View File

@ -18,26 +18,24 @@ function cmakebuild() {
cd /tmp
STATIC_PACKAGES="libfftw3-bin libprotobuf17 libsamplerate0 libicu63"
BUILD_PACKAGES="git autoconf automake libtool libfftw3-dev pkg-config cmake make gcc g++ libprotobuf-dev protobuf-compiler libsamplerate-dev libicu-dev libpython3-dev"
STATIC_PACKAGES="libfftw3-bin libprotobuf23 libsamplerate0 libicu67 libudev1"
BUILD_PACKAGES="git autoconf automake libtool libfftw3-dev pkg-config cmake make gcc g++ libprotobuf-dev protobuf-compiler libsamplerate-dev libicu-dev libpython3-dev libudev-dev"
apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/jketterl/js8py.git
pushd js8py
git checkout 0.1.0
git checkout 0.1.2
python3 setup.py install
popd
rm -rf js8py
git clone https://github.com/jketterl/csdr.git
# latest develop as of 2021-09-22 (template fixes)
cmakebuild csdr 536f3b9eb7cfe5434e9a9f1e807c96115dc9ac10
cmakebuild csdr 0.18.0
git clone https://github.com/jketterl/pycsdr.git
cd pycsdr
# latest develop as of 2021-09-22 (first version)
git checkout 52da48a87ef97eb7d337f1b146db66ca453801e4
git checkout 0.18.0
./setup.py install install_headers
cd ..
rm -rf pycsdr
@ -45,17 +43,14 @@ rm -rf pycsdr
git clone https://github.com/jketterl/codecserver.git
mkdir -p /usr/local/etc/codecserver
cp codecserver/conf/codecserver.conf /usr/local/etc/codecserver
# latest develop as of 2021-09-24 (new parsing)
cmakebuild codecserver c51254323b32db5b169cdfc39e043eed6d613a77
cmakebuild codecserver 0.2.0
git clone https://github.com/jketterl/digiham.git
# latest develop as of 2021-09-22 (post-merge)
cmakebuild digiham 62d2b4581025568263ae8c90d2450b65561b7ce8
cmakebuild digiham 0.6.0
git clone https://github.com/jketterl/pydigiham.git
cd pydigiham
# latest develop as of 2021-09-22 (split from digiham)
git checkout b0cc0c35d5ef2ae84c9bb1a02d56161d5bd5bf2f
git checkout 0.6.0
./setup.py install
cd ..
rm -rf pydigiham

View File

@ -917,33 +917,6 @@ img.openwebrx-mirror-img
z-index: 10;
}
#openwebrx-digimode-content .part
{
perspective: 700px;
}
#openwebrx-digimode-content .part
{
animation: new-digimode-data-3d 100ms;
animation-timing-function: linear;
display: inline-block;
perspective-origin: 50% 50%;
transform-origin: 0% 50%;
}
@keyframes new-digimode-data
{
0%{ opacity: 0; }
100%{ opacity: 1; }
}
@keyframes new-digimode-data-3d
{
0%{ transform: rotateX(0deg) rotateY(-90deg) translateX(-5px) scale(1.3); }
100%{ transform: rotateX(0deg) rotateY(0deg) translateX(0) scale(1); }
}
#openwebrx-digimode-select-channel
{
transition: all 500ms;
@ -1052,7 +1025,8 @@ img.openwebrx-mirror-img
.openwebrx-meta-slot.active.direct .openwebrx-meta-user-image .directcall,
.openwebrx-meta-slot.active.individual .openwebrx-meta-user-image .directcall,
#openwebrx-panel-metadata-ysf .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall,
#openwebrx-panel-metadata-dstar .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall {
#openwebrx-panel-metadata-dstar .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall,
#openwebrx-panel-metadata-m17 .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall {
display: initial;
}
@ -1086,6 +1060,14 @@ img.openwebrx-mirror-img
content: "Down: ";
}
.openwebrx-m17-source:not(:empty):before {
content: "SRC: ";
}
.openwebrx-m17-destination:not(:empty):before {
content: "DEST: ";
}
.openwebrx-dstar-yourcall:not(:empty):before {
content: "UR: ";
}

View File

@ -55,10 +55,6 @@
</div>
<div id="openwebrx-panels-container">
<div id="openwebrx-panels-container-left">
<div class="openwebrx-panel" data-panel-name="client-under-devel" style="width: 245px; background-color: Red;">
<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
<br />We're working on the code right now, so the application might fail.
</div>
<div class="openwebrx-panel" id="openwebrx-panel-digimodes" style="display: none; width: 619px;" data-panel-name="digimodes">
<div id="openwebrx-digimode-canvas-container">
<div id="openwebrx-digimode-select-channel"></div>
@ -74,6 +70,15 @@
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message"></div>
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"></div>
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"></div>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-m17" style="display: none;" data-panel-name="metadata-m17">
<div class="openwebrx-meta-slot">
<div class="openwebrx-meta-user-image">
<img class="directcall" src="static/gfx/openwebrx-directcall.svg">
</div>
<div class="openwebrx-m17-source"></div>
<div class="openwebrx-m17-destination"></div>
</div>
</div>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" style="display: none;" data-panel-name="metadata-ysf">
<div class="openwebrx-meta-slot">
<div class="openwebrx-ysf-mode"></div>

View File

@ -321,18 +321,57 @@ NxdnMetaPanel.prototype.clear = function() {
this.setDestination();
};
function M17MetaPanel(el) {
MetaPanel.call(this, el);
this.modes = ['M17'];
this.clear();
}
M17MetaPanel.prototype = new MetaPanel();
M17MetaPanel.prototype.update = function(data) {
if (!this.isSupported(data)) return;
if (data['sync'] && data['sync'] === 'voice') {
this.el.find(".openwebrx-meta-slot").addClass("active");
this.setSource(data['source']);
this.setDestination(data['destination']);
} else {
this.clear();
}
};
M17MetaPanel.prototype.setSource = function(source) {
if (this.source === source) return;
this.source = source;
this.el.find('.openwebrx-m17-source').text(source || '');
};
M17MetaPanel.prototype.setDestination = function(destination) {
if (this.destination === destination) return;
this.destination = destination;
this.el.find('.openwebrx-m17-destination').text(destination || '');
};
M17MetaPanel.prototype.clear = function() {
MetaPanel.prototype.clear.call(this);
this.setSource();
this.setDestination();
};
MetaPanel.types = {
dmr: DmrMetaPanel,
ysf: YsfMetaPanel,
dstar: DStarMetaPanel,
nxdn: NxdnMetaPanel,
m17: M17MetaPanel,
};
$.fn.metaPanel = function() {
return this.map(function() {
var $self = $(this);
if (!$self.data('metapanel')) {
var matches = /^openwebrx-panel-metadata-([a-z]+)$/.exec($self.prop('id'));
var matches = /^openwebrx-panel-metadata-([a-z0-9]+)$/.exec($self.prop('id'));
var constructor = matches && MetaPanel.types[matches[1]] || MetaPanel;
$self.data('metapanel', new constructor($self));
}

View File

@ -1,2 +1,4 @@
/* Taken from https://github.com/cyphercodes/location-picker under GPLv3 license */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.locationPicker=t()}(this,function(){"use strict";return function(e,t){void 0===t&&(t={});var n=t.insertAt;if(e&&"undefined"!=typeof document){var o=document.head||document.getElementsByTagName("head")[0],i=document.createElement("style");i.type="text/css","top"===n&&o.firstChild?o.insertBefore(i,o.firstChild):o.appendChild(i),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(document.createTextNode(e))}}('.location-picker .centerMarker{position:absolute;background:url("data:image/png;base64,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") no-repeat;background-size:100%;top:50%;left:50%;z-index:1;margin-left:-14px;margin-top:-43px;height:44px;width:28px;cursor:pointer}'),function(){function e(e,t,n){void 0===t&&(t={}),void 0===n&&(n={});var o={setCurrentPosition:!0};Object.assign(o,t);var i={center:new google.maps.LatLng(o.lat?o.lat:34.4346,o.lng?o.lng:35.8362),zoom:15};Object.assign(i,n),e instanceof HTMLElement?this.element=e:this.element=document.getElementById(e),this.map=new google.maps.Map(this.element,i);var r=document.createElement("div");r.classList.add("centerMarker"),this.element&&(this.element.classList.add("location-picker"),this.element.children[0].appendChild(r)),!o.setCurrentPosition||o.lat||o.lng||this.setCurrentPosition()}return e.prototype.getMarkerPosition=function(){var e=this.map.getCenter();return{lat:e.lat(),lng:e.lng()}},e.prototype.setLocation=function(e,t){this.map.setCenter(new google.maps.LatLng(e,t))},e.prototype.setCurrentPosition=function(){var e=this;navigator.geolocation?navigator.geolocation.getCurrentPosition(function(t){var n={lat:t.coords.latitude,lng:t.coords.longitude};e.map.setCenter(n)},function(){console.log("Could not determine your location...")}):console.log("Your browser does not support Geolocation.")},e}()});
/* Contains https://github.com/cyphercodes/location-picker/pull/11 to allow latitude and longitude to be 0 */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.locationPicker=t()}(this,function(){"use strict";return function(e,t){void 0===t&&(t={});var n=t.insertAt;if(e&&"undefined"!=typeof document){var o=document.head||document.getElementsByTagName("head")[0],i=document.createElement("style");i.type="text/css","top"===n&&o.firstChild?o.insertBefore(i,o.firstChild):o.appendChild(i),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(document.createTextNode(e))}}('.location-picker .centerMarker{position:absolute;background:url("data:image/png;base64,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") no-repeat;background-size:100%;top:50%;left:50%;z-index:1;margin-left:-14px;margin-top:-43px;height:44px;width:28px;cursor:pointer}'),function(){function e(e,t,n){void 0===t&&(t={}),void 0===n&&(n={});var o={setCurrentPosition:!0};Object.assign(o,t);var i={center:new google.maps.LatLng("number"==typeof o.lat?o.lat:34.4346,"number"==typeof o.lng?o.lng:35.8362),zoom:15};Object.assign(i,n),e instanceof HTMLElement?this.element=e:this.element=document.getElementById(e),this.map=new google.maps.Map(this.element,i);var r=document.createElement("div");r.classList.add("centerMarker"),this.element&&(this.element.classList.add("location-picker"),this.element.children[0].appendChild(r)),!o.setCurrentPosition||o.lat||o.lng||this.setCurrentPosition()}return e.prototype.getMarkerPosition=function(){var e=this.map.getCenter();return{lat:e.lat(),lng:e.lng()}},e.prototype.setLocation=function(e,t){this.map.setCenter(new google.maps.LatLng(e,t))},e.prototype.setCurrentPosition=function(){var e=this;navigator.geolocation?navigator.geolocation.getCurrentPosition(function(t){var n={lat:t.coords.latitude,lng:t.coords.longitude};e.map.setCenter(n)},function(){console.log("Could not determine your location...")}):console.log("Your browser does not support Geolocation.")},e}()});

View File

@ -838,6 +838,9 @@ function on_ws_recv(evt) {
$overlay.show();
$("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator();
break;
case "demodulator_error":
divlog(json['value'], true);
break;
case 'secondary_demod':
var value = json['value'];
var panels = [
@ -1458,11 +1461,8 @@ function secondary_demod_push_data(x) {
if (y === "<") return "&lt;";
if (y === ">") return "&gt;";
if (y === " ") return "&nbsp;";
if (y === "\n") return "<br />";
return y;
}).map(function (y) {
if (y === "\n")
return "<br />";
return "<span class=\"part\">" + y + "</span>";
}).join("");
$("#openwebrx-cursor-blink").before(x);
}

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3
import sys
from owrx.__main__ import main
if __name__ == "__main__":
main()
sys.exit(main())

View File

@ -57,12 +57,15 @@ def main():
if args.version:
print("OpenWebRX version {version}".format(version=openwebrx_version))
elif args.module == "admin":
run_admin_action(adminparser, args)
elif args.module == "config":
run_admin_action(configparser, args)
else:
start_receiver()
return 0
if args.module == "admin":
return run_admin_action(adminparser, args)
if args.module == "config":
return run_admin_action(configparser, args)
return start_receiver()
def start_receiver():
@ -89,13 +92,18 @@ Support and info: https://groups.io/g/openwebrx
coreConfig = CoreConfig()
featureDetector = FeatureDetector()
if not featureDetector.is_available("core"):
failed = featureDetector.get_failed_requirements("core")
if failed:
logger.error(
"you are missing required dependencies to run openwebrx. "
"please check that the following core requirements are installed and up to date:"
"please check that the following core requirements are installed and up to date: %s",
", ".join(failed)
)
logger.error(", ".join(featureDetector.get_requirements("core")))
return
for f in failed:
description = featureDetector.get_requirement_description(f)
if description:
logger.error("description for %s:\n%s", f, description)
return 1
# Get error messages about unknown / unavailable features as soon as possible
# start up "always-on" sources right away
@ -114,3 +122,5 @@ Support and info: https://groups.io/g/openwebrx
SdrService.stopAllSources()
ReportingEngine.stopAll()
DecoderQueue.stopAll()
return 0

View File

@ -46,15 +46,14 @@ def run_admin_action(parser, args):
else:
if not hasattr(args, "silent") or not args.silent:
parser.print_help()
sys.exit(1)
sys.exit(0)
return 1
return 0
try:
command.run(args)
return command.run(args)
except Exception:
if not hasattr(args, "silent") or not args.silent:
print("Error running command:")
traceback.print_exc()
sys.exit(1)
sys.exit(0)
return 1
return 0

View File

@ -30,8 +30,7 @@ class UserCommand(Command, metaclass=ABCMeta):
password = getpass("Please enter the new password for {username}: ".format(username=username))
confirm = getpass("Please confirm the new password: ")
if password != confirm:
print("ERROR: Password mismatch.")
sys.exit(1)
raise ValueError("Password mismatch")
generated = False
return password, generated
@ -108,8 +107,9 @@ class HasUser(Command):
if args.user in userList:
if not args.silent:
print('User "{name}" exists.'.format(name=args.user))
return 0
else:
if not args.silent:
print('User "{name}" does not exist.'.format(name=args.user))
# in bash, a return code > 0 is interpreted as "false"
sys.exit(1)
return 1

View File

@ -56,12 +56,15 @@ class Ax25Parser(PickleModule):
for i in range(0, len(l), n):
yield l[i:i + n]
return {
"destination": self.extractCallsign(ax25frame[0:7]),
"source": self.extractCallsign(ax25frame[7:14]),
"path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)],
"data": ax25frame[control_pid + 2 :],
}
try:
return {
"destination": self.extractCallsign(ax25frame[0:7]),
"source": self.extractCallsign(ax25frame[7:14]),
"path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)],
"data": ax25frame[control_pid + 2 :],
}
except (ValueError, IndexError):
logger.exception("error parsing ax25 frame")
def extractCallsign(self, input):
cs = bytes([b >> 1 for b in input[0:6]]).decode(encoding, "replace").strip()

View File

@ -49,6 +49,8 @@ class DirewolfModule(AutoStartModule, DirewolfConfigSubscriber):
stdin=PIPE,
)
# resume in case the reader has been stop()ed before
self.reader.resume()
threading.Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start()
delay = 0.5

View File

@ -56,9 +56,10 @@ class Client(Handler, metaclass=ABCMeta):
try:
self.conn.send(data)
except IOError:
self.close()
logger.exception("error in Client::send()")
self.close(error=True)
def close(self):
def close(self, error: bool = False):
if self.multithreadingQueue is not None:
while True:
try:
@ -70,7 +71,7 @@ class Client(Handler, metaclass=ABCMeta):
except Full:
# this shouldn't happen, we just emptied the queue, but it's not worth risking the exception
logger.exception("impossible queue state: Full after Empty")
self.conn.close()
self.conn.close(socketError=error)
def mp_send(self, data):
if self.multithreadingQueue is None:
@ -78,7 +79,7 @@ class Client(Handler, metaclass=ABCMeta):
try:
self.multithreadingQueue.put(data, block=False)
except Full:
self.close()
self.close(error=True)
@abstractmethod
def handleTextMessage(self, conn, message):
@ -107,9 +108,9 @@ class OpenWebRxClient(Client, metaclass=ABCMeta):
def write_receiver_details(self, details):
self.send({"type": "receiver_details", "value": details})
def close(self):
def close(self, error: bool = False):
self._detailsSubscription.cancel()
super().close()
super().close(error)
class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
@ -339,7 +340,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def handleNoSdrsAvailable(self):
self.write_sdr_error("No SDR Devices available")
def close(self):
def close(self, error: bool = False):
if self.sdr is not None:
self.sdr.removeClient(self)
self.stopDsp()
@ -350,7 +351,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
if self.bookmarkSub is not None:
self.bookmarkSub.cancel()
self.bookmarkSub = None
super().close()
super().close(error)
def stopDsp(self):
with self.dspLock:
@ -422,6 +423,9 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def write_sdr_error(self, message):
self.send({"type": "sdr_error", "value": message})
def write_demodulator_error(self, message):
self.send({"type": "demodulator_error", "value": message})
def write_backoff_message(self, reason):
self.send({"type": "backoff", "reason": reason})
@ -463,9 +467,9 @@ class MapConnection(OpenWebRxClient):
def handleTextMessage(self, conn, message):
pass
def close(self):
def close(self, error: bool = False):
Map.getSharedInstance().removeClient(self)
super().close()
super().close(error)
def write_config(self, cfg):
self.send({"type": "config", "value": cfg})
@ -489,17 +493,17 @@ class HandshakeMessageHandler(Handler):
client = None
if "type" in handshake:
if handshake["type"] == "receiver":
client = OpenWebRxReceiverClient(conn)
client = OpenWebRxReceiverClient
elif handshake["type"] == "map":
client = MapConnection(conn)
client = MapConnection
else:
logger.warning("invalid connection type: %s", handshake["type"])
if client is not None:
logger.debug("handshake complete, handing off to %s", type(client).__name__)
logger.debug("handshake complete, handing off to %s", client.__name__)
# hand off all further communication to the correspondig connection
conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version=openwebrx_version))
conn.setMessageHandler(client)
conn.setMessageHandler(client(conn))
else:
logger.warning('invalid handshake received')
else:

View File

@ -1,6 +1,8 @@
from . import Controller
from owrx.metrics import CounterMetric, DirectMetric, Metrics
import json
import re
class MetricsController(Controller):
@ -21,7 +23,7 @@ class MetricsController(Controller):
else:
raise ValueError("Unexpected metric type for metric {}".format(repr(metric)))
return "{key} {value}".format(key=key.replace(".", "_"), value=value)
return "{key} {value}".format(key=re.sub('[^a-zA-Z0-9:_]', '_', key), value=value)
data = ["# https://prometheus.io/docs/instrumenting/exposition_formats/"] + [
prometheusFormat(k, v) for k, v in metrics.items()

View File

@ -72,8 +72,8 @@ class SessionController(WebpageController):
self.set_response_cookies(cookie)
self.send_redirect(target)
return
target = "?{}".format(urlencode({"ref": self.request.query["ref"][0]})) if "ref" in self.request.query else ""
self.send_redirect(self.request.path + target)
target = "{}login?{}".format(self.get_document_root(), urlencode({"ref": self.request.query["ref"][0]}))
self.send_redirect(target)
def logoutAction(self):
self.send_redirect("logout happening here")

View File

@ -5,7 +5,6 @@ from owrx.form.input import (
TextInput,
NumberInput,
FloatInput,
LocationInput,
TextAreaInput,
DropdownInput,
Option,
@ -14,6 +13,7 @@ from owrx.form.input.converter import WaterfallColorsConverter, IntConverter
from owrx.form.input.receiverid import ReceiverKeysConverter
from owrx.form.input.gfx import AvatarInput, TopPhotoInput
from owrx.form.input.device import WaterfallLevelsInput, WaterfallAutoLevelsInput
from owrx.form.input.location import LocationInput
from owrx.waterfall import WaterfallOptions
from owrx.breadcrumb import Breadcrumb, BreadcrumbItem
from owrx.controllers.settings import SettingsBreadcrumb
@ -48,7 +48,7 @@ class GeneralSettingsController(SettingsFormController):
TextInput("receiver_admin", "Receiver admin"),
LocationInput("receiver_gps", "Receiver coordinates"),
TextInput("photo_title", "Photo title"),
TextAreaInput("photo_desc", "Photo description"),
TextAreaInput("photo_desc", "Photo description", infotext="HTML supported "),
),
Section(
"Receiver images",

View File

@ -22,6 +22,7 @@ class CpuUsageThread(threading.Thread):
self.last_worktime = 0
self.last_idletime = 0
self.endEvent = threading.Event()
self.startLock = threading.Lock()
super().__init__()
def run(self):
@ -59,8 +60,9 @@ class CpuUsageThread(threading.Thread):
def add_client(self, c):
self.clients.append(c)
if not self.is_alive():
self.start()
with self.startLock:
if not self.is_alive():
self.start()
def remove_client(self, c):
try:

View File

@ -2,6 +2,9 @@ from owrx.config import Config
from owrx.locator import Locator
from owrx.property import PropertyFilter
from owrx.property.filter import ByPropertyName
import logging
logger = logging.getLogger(__name__)
class ReceiverDetails(PropertyFilter):
@ -20,5 +23,8 @@ class ReceiverDetails(PropertyFilter):
def __dict__(self):
receiver_info = super().__dict__()
receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"])
try:
receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"])
except ValueError as e:
logger.error("invalid receiver location, check in settings: %s", str(e))
return receiver_info

View File

@ -1,12 +1,13 @@
from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass
from owrx.property import PropertyStack, PropertyLayer, PropertyValidator
from owrx.property.validators import OrValidator, RegexValidator, BoolValidator
from owrx.modes import Modes
from owrx.modes import Modes, DigitalMode
from csdr.chain import Chain
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain, DeemphasisTauChain
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain, DeemphasisTauChain, DemodulatorError
from csdr.chain.selector import Selector, SecondarySelector
from csdr.chain.clientaudio import ClientAudioChain
from csdr.chain.fft import FftChain
from csdr.chain.dummy import DummyDemodulator
from pycsdr.modules import Buffer, Writer
from pycsdr.types import Format
from typing import Union, Optional
@ -103,22 +104,11 @@ class ClientDemodulatorChain(Chain):
self.demodulator = demodulator
outputRate = self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
self.selector.setOutputRate(self._getSelectorOutputRate())
if isinstance(self.demodulator, FixedIfSampleRateChain):
self.selector.setOutputRate(self.demodulator.getFixedIfSampleRate())
elif isinstance(self.secondaryDemodulator, FixedAudioRateChain):
self.selector.setOutputRate(self.secondaryDemodulator.getFixedAudioRate())
else:
self.selector.setOutputRate(outputRate)
self.demodulator.setSampleRate(outputRate)
if isinstance(self.demodulator, FixedAudioRateChain):
self.clientAudioChain.setInputRate(self.demodulator.getFixedAudioRate())
elif isinstance(self.secondaryDemodulator, FixedAudioRateChain):
self.clientAudioChain.setInputRate(self.secondaryDemodulator.getFixedAudioRate())
else:
self.clientAudioChain.setInputRate(outputRate)
clientRate = self._getClientAudioInputRate()
self.clientAudioChain.setInputRate(clientRate)
self.demodulator.setSampleRate(clientRate)
if isinstance(self.demodulator, DeemphasisTauChain):
self.demodulator.setDeemphasisTau(self.wfmDeemphasisTau)
@ -126,17 +116,42 @@ class ClientDemodulatorChain(Chain):
self._updateDialFrequency()
self._syncSquelch()
outputRate = self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
self.clientAudioChain.setClientRate(outputRate)
if self.metaWriter is not None and isinstance(demodulator, MetaProvider):
demodulator.setMetaWriter(self.metaWriter)
def stopDemodulator(self):
if self.demodulator is None:
return
# we need to get the currrent demodulator out of the chain so that it can be deallocated properly
# so we just replace it with a dummy here
# in order to avoid any client audio chain hassle, the dummy simply imitates the output format of the current
# demodulator
self.replace(1, DummyDemodulator(self.demodulator.getOutputFormat()))
self.demodulator.stop()
self.demodulator = None
def _getSelectorOutputRate(self):
if isinstance(self.secondaryDemodulator, FixedAudioRateChain):
if isinstance(self.demodulator, FixedIfSampleRateChain):
return self.demodulator.getFixedIfSampleRate()
elif isinstance(self.secondaryDemodulator, FixedAudioRateChain):
if isinstance(self.demodulator, FixedAudioRateChain) and self.demodulator.getFixedAudioRate() != self.secondaryDemodulator.getFixedAudioRate():
raise ValueError("secondary and primary demodulator chain audio rates do not match!")
return self.secondaryDemodulator.getFixedAudioRate()
return self.outputRate
else:
return self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
def _getClientAudioInputRate(self):
if isinstance(self.demodulator, FixedAudioRateChain):
return self.demodulator.getFixedAudioRate()
elif isinstance(self.secondaryDemodulator, FixedAudioRateChain):
return self.secondaryDemodulator.getFixedAudioRate()
else:
return self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
def setSecondaryDemodulator(self, demod: Optional[SecondaryDemodulator]):
if demod is self.secondaryDemodulator:
@ -149,8 +164,11 @@ class ClientDemodulatorChain(Chain):
rate = self._getSelectorOutputRate()
self.selector.setOutputRate(rate)
self.clientAudioChain.setInputRate(rate)
self.demodulator.setSampleRate(rate)
clientRate = self._getClientAudioInputRate()
self.clientAudioChain.setInputRate(clientRate)
self.demodulator.setSampleRate(clientRate)
self._updateDialFrequency()
self._syncSquelch()
@ -433,12 +451,17 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
self.readers = {}
if "start_mod" in self.props:
self.setDemodulator(self.props["start_mod"])
mode = Modes.findByModulation(self.props["start_mod"])
if mode and mode.bandpass:
bpf = [mode.bandpass.low_cut, mode.bandpass.high_cut]
self.chain.setBandpass(*bpf)
if mode:
self.setDemodulator(mode.get_modulation())
if isinstance(mode, DigitalMode):
self.setSecondaryDemodulator(mode.modulation)
if mode.bandpass:
bpf = [mode.bandpass.low_cut, mode.bandpass.high_cut]
self.chain.setBandpass(*bpf)
else:
# TODO modes should be mandatory
self.setDemodulator(self.props["start_mod"])
if "start_freq" in self.props and "center_freq" in self.props:
self.chain.setFrequencyOffset(self.props["start_freq"] - self.props["center_freq"])
@ -490,8 +513,6 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
self.sdrSource.addClient(self)
super().__init__()
def setSecondaryFftSize(self, size):
self.chain.setSecondaryFftSize(size)
self.handler.write_secondary_dsp_config({"secondary_fft_size": size})
@ -535,19 +556,23 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
return FreeDV()
def setDemodulator(self, mod):
demodulator = self._getDemodulator(mod)
if demodulator is None:
raise ValueError("unsupported demodulator: {}".format(mod))
self.chain.setDemodulator(demodulator)
self.chain.stopDemodulator()
try:
demodulator = self._getDemodulator(mod)
if demodulator is None:
raise ValueError("unsupported demodulator: {}".format(mod))
self.chain.setDemodulator(demodulator)
output = "hd_audio" if isinstance(demodulator, HdAudio) else "audio"
output = "hd_audio" if isinstance(demodulator, HdAudio) else "audio"
if output != self.audioOutput:
self.audioOutput = output
# re-wire the audio to the correct client API
buffer = Buffer(self.chain.getOutputFormat())
self.chain.setWriter(buffer)
self.wireOutput(self.audioOutput, buffer)
if output != self.audioOutput:
self.audioOutput = output
# re-wire the audio to the correct client API
buffer = Buffer(self.chain.getOutputFormat())
self.chain.setWriter(buffer)
self.wireOutput(self.audioOutput, buffer)
except DemodulatorError as de:
self.handler.write_demodulator_error(str(de))
def _getSecondaryDemodulator(self, mod) -> Optional[SecondaryDemodulator]:
if isinstance(mod, SecondaryDemodulator):

View File

@ -13,6 +13,7 @@ from datetime import datetime, timedelta
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class UnknownFeatureException(Exception):
@ -51,7 +52,7 @@ class FeatureCache(object):
class FeatureDetector(object):
features = {
# core features; we won't start without these
"core": ["csdr", "pycsdr"],
"core": ["csdr"],
# different types of sdrs and their requirements
"rtl_sdr": ["rtl_connector"],
"rtl_sdr_soapy": ["soapy_connector", "soapy_rtl_sdr"],
@ -68,6 +69,7 @@ class FeatureDetector(object):
"uhd": ["soapy_connector", "soapy_uhd"],
"radioberry": ["soapy_connector", "soapy_radioberry"],
"fcdpp": ["soapy_connector", "soapy_fcdpp"],
"bladerf": ["soapy_connector", "soapy_bladerf"],
"sddc": ["sddc_connector"],
"hpsdr": ["hpsdr_connector"],
"runds": ["runds_connector"],
@ -108,6 +110,9 @@ class FeatureDetector(object):
def is_available(self, feature):
return self.has_requirements(self.get_requirements(feature))
def get_failed_requirements(self, feature):
return [req for req in self.get_requirements(feature) if not self.has_requirement(req)]
def get_requirements(self, feature):
try:
return FeatureDetector.features[feature]
@ -131,12 +136,14 @@ class FeatureDetector(object):
if cache.has(requirement):
return cache.get(requirement)
logger.debug("performing feature check for %s", requirement)
method = self._get_requirement_method(requirement)
result = False
if method is not None:
result = method()
else:
logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement))
logger.debug("feature check for %s complete. result: %s", requirement, result)
cache.set(requirement, result)
return result
@ -167,27 +174,24 @@ class FeatureDetector(object):
except FileNotFoundError:
return False
_required_csdr_version = LooseVersion("0.18.0")
def has_csdr(self):
"""
OpenWebRX uses the demodulator and pipeline tools provided by the csdr project. Please check out [the project
page on github](https://github.com/jketterl/csdr) for further details and installation instructions.
In addition, the [pycsdr](https://github.com/jketterl/pycsdr) package must be installed to provide
python bindings for the csdr library.
"""
required_version = LooseVersion("0.18.0")
try:
from pycsdr.modules import csdr_version
return LooseVersion(csdr_version) >= FeatureDetector._required_csdr_version
except ImportError:
return False
def has_pycsdr(self):
"""
OpenWebRX uses the csdr python bindings from the pycsdr package to build its demodulator pipelines.
Please visit [the project page](https://github.com/jketterl/pycsdr) for further details.
"""
try:
from pycsdr.modules import version as pycsdr_version
return LooseVersion(pycsdr_version) >= FeatureDetector._required_csdr_version
return (
LooseVersion(csdr_version) >= required_version and
LooseVersion(pycsdr_version) >= required_version
)
except ImportError:
return False
@ -222,15 +226,23 @@ class FeatureDetector(object):
To use digital voice modes, the digiham package is required. You can find the package and installation
instructions [here](https://github.com/jketterl/digiham).
In addition, the [pydigiham](https://github.com/jketterl/pydigiham) package must be installed to provide
python bindings for the digiham library.
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.3 of digiham.
As of now, we require version 0.6 of digiham.
"""
required_version = LooseVersion("0.5")
required_version = LooseVersion("0.6")
try:
from digiham.modules import version as digiham_version
return LooseVersion(digiham_version) >= required_version
from digiham.modules import digiham_version as digiham_version
from digiham.modules import version as pydigiham_version
return (
LooseVersion(digiham_version) >= required_version
and LooseVersion(pydigiham_version) >= required_version
)
except ImportError:
return False
@ -383,6 +395,14 @@ class FeatureDetector(object):
"""
return self._has_soapy_driver("fcdpp")
def has_soapy_bladerf(self):
"""
The SoapyBladeRF module allows the use of Blade RF devices.
You can get it [here](https://github.com/pothosware/SoapyBladeRF).
"""
return self._has_soapy_driver("bladerf")
def has_m17_demod(self):
"""
The `m17-demod` tool is used to demodulate M17 digital voice signals.
@ -457,6 +477,7 @@ class FeatureDetector(object):
required_version = StrictVersion("0.1")
try:
from js8py.version import strictversion
return strictversion >= required_version
except ImportError:
return False
@ -533,6 +554,7 @@ class FeatureDetector(object):
server = config["digital_voice_codecserver"]
try:
from digiham.modules import MbeSynthesizer
return MbeSynthesizer.hasAmbe(server)
except ImportError:
return False

View File

@ -77,8 +77,9 @@ class SpectrumThread(SdrSourceEventClient):
return
self.dsp.stop()
self.dsp = None
self.reader.stop()
self.reader = None
if self.reader:
self.reader.stop()
self.reader = None
self.sdrSource.removeClient(self)
while self.subscriptions:
self.subscriptions.pop().cancel()
@ -92,7 +93,8 @@ class SpectrumThread(SdrSourceEventClient):
def onStateChange(self, state: SdrSourceState):
if state is SdrSourceState.STOPPING:
self.dsp.stop()
if self.dsp:
self.dsp.stop()
elif state == SdrSourceState.RUNNING:
if self.dsp is None:
self.start()

View File

@ -1,8 +1,7 @@
from abc import ABC
from owrx.modes import Modes
from owrx.config import Config
from owrx.form.input.validator import Validator
from owrx.form.input.converter import Converter, NullConverter, IntConverter, FloatConverter, EnumConverter
from owrx.form.input.converter import Converter, NullConverter, IntConverter, FloatConverter, EnumConverter, TextConverter
from enum import Enum
@ -107,6 +106,9 @@ class TextInput(Input):
props["type"] = "text"
return props
def defaultConverter(self):
return TextConverter()
class NumberInput(Input):
def __init__(self, id, label, infotext=None, append="", converter: Converter = None, validator: Validator = None):
@ -158,45 +160,6 @@ class FloatInput(NumberInput):
return FloatConverter()
class LocationInput(Input):
def render_input_group(self, value, errors):
return """
<div class="row {rowclass}">
{inputs}
</div>
{errors}
<div class="row">
<div class="col map-input" data-key="{key}" for="{id}"></div>
</div>
""".format(
id=self.id,
rowclass="is-invalid" if errors else "",
inputs=self.render_input(value, errors),
errors=self.render_errors(errors),
key=Config.get()["google_maps_api_key"],
)
def render_input(self, value, errors):
return "".join(self.render_sub_input(value, id, errors) for id in ["lat", "lon"])
def render_sub_input(self, value, id, errors):
return """
<div class="col">
<input type="number" class="{classes}" id="{id}" name="{id}" placeholder="{label}" value="{value}"
step="any" {disabled}>
</div>
""".format(
id="{0}-{1}".format(self.id, id),
label=self.label,
classes=self.input_classes(errors),
value=value[id],
disabled="disabled" if self.disabled else "",
)
def parse(self, data):
return {self.id: {k: float(data["{0}-{1}".format(self.id, k)][0]) for k in ["lat", "lon"]}}
class TextAreaInput(Input):
def render_input(self, value, errors):
return """

View File

@ -14,6 +14,10 @@ class Converter(ABC):
class NullConverter(Converter):
"""
The default converter class
Does not change the value in any way, just passes them through
"""
def convert_to_form(self, value):
return value
@ -21,10 +25,25 @@ class NullConverter(Converter):
return value
class TextConverter(Converter):
"""
Converter class for text inputs
Does nothing more than to prevent the special python value "None" from appearing in the form
The string "None" should pass
"""
def convert_to_form(self, value):
if value is None:
return ""
return value
def convert_from_form(self, value):
return value
class OptionalConverter(Converter):
"""
Transforms a special form value to None
The default is look for an empty string, but this can be used to adopt to other types.
The default is to look for an empty string, but this can be used to adopt to other types.
If the default is not found, the actual value is passed to the sub_converter for further transformation.
useful for optional fields since None is not stored in the configuration
"""
@ -61,7 +80,14 @@ class EnumConverter(Converter):
self.enumCls = enumCls
def convert_to_form(self, value):
return None if value is None else self.enumCls(value).name
if value is None:
return None
try:
return self.enumCls(value).name
# if the current value is not part of the enum, this will happen:
except ValueError:
# and this will restore the default
return None
def convert_from_form(self, value):
return self.enumCls[value].value

View File

@ -0,0 +1,64 @@
from owrx.form.input import Input
from owrx.form.input.validator import Validator
from owrx.form.error import ValidationError
from owrx.config import Config
import logging
logger = logging.getLogger(__name__)
class LocationValidator(Validator):
def validate(self, key, value):
if "lat" in value and not -90 < value["lat"] < 90:
raise ValidationError(key, "Latitude out of range (-90 to 90)")
if "lon" in value and not -180 < value["lon"] < 180:
raise ValidationError(key, "Longitude out of range (-180 to 180)")
pass
class LocationInput(Input):
def __init__(self, id, label, validator: Validator = None):
if validator is None:
validator = LocationValidator()
super().__init__(id, label, validator=validator)
def render_input_group(self, value, errors):
return """
<div class="row {rowclass}">
{inputs}
</div>
{errors}
<div class="row">
<div class="col map-input" data-key="{key}" for="{id}"></div>
</div>
""".format(
id=self.id,
rowclass="is-invalid" if errors else "",
inputs=self.render_input(value, errors),
errors=self.render_errors(errors),
key=Config.get()["google_maps_api_key"],
)
def render_input(self, value, errors):
return "".join(self.render_sub_input(value, id, errors) for id in ["lat", "lon"])
def render_sub_input(self, value, id, errors):
return """
<div class="col">
<input type="number" class="{classes}" id="{id}" name="{id}" placeholder="{label}" value="{value}"
step="any" {disabled}>
</div>
""".format(
id="{0}-{1}".format(self.id, id),
label=self.label,
classes=self.input_classes(errors),
value=value[id],
disabled="disabled" if self.disabled else "",
)
def parse(self, data):
value = {k: float(data["{0}-{1}".format(self.id, k)][0]) for k in ["lat", "lon"]}
if self.validator is not None:
self.validator.validate(self.id, value)
return {self.id: value}

View File

@ -4,23 +4,26 @@ from owrx.form.error import ValidationError
class Validator(ABC):
@abstractmethod
def validate(self, key, value):
def validate(self, key, value) -> None:
pass
class RequiredValidator(Validator):
def validate(self, key, value):
def validate(self, key, value) -> None:
if value is None or value == "":
raise ValidationError(key, "Field is required")
class RangeValidator(Validator):
def __init__(self, minValue, maxValue):
self.minValue = minValue
self.maxValue = maxValue
def validate(self, key, value):
def validate(self, key, value) -> None:
if value is None or value == "":
return # Ignore empty values
return # Ignore empty values
n = float(value)
if n < self.minValue or n > self.maxValue:
raise ValidationError(key, 'Value must be between %s and %s'%(self.minValue, self.maxValue))
raise ValidationError(
key, "Value must be between {min} and {max}".format(min=self.minValue, max=self.maxValue)
)

View File

@ -5,6 +5,11 @@ class Locator(object):
lat = coordinates["lat"]
lon = coordinates["lon"]
if not -90 < lat < 90:
raise ValueError("invalid latitude: {}".format(lat))
if not -180 < lon < 180:
raise ValueError("invalid longitude: {}".format(lon))
lon = lon + 180
lat = lat + 90

View File

@ -1,15 +1,37 @@
from csdr.module import PickleModule
from owrx.bands import Bandplan
from owrx.metrics import Metrics, CounterMetric
import logging
logger = logging.getLogger(__name__)
class PocsagParser(PickleModule):
def __init__(self):
self.band = None
super().__init__()
def process(self, meta):
try:
if "address" in meta:
meta["address"] = int(meta["address"])
meta["mode"] = "Pocsag"
self.pushDecode()
return meta
except Exception:
logger.exception("Exception while parsing Pocsag message")
def setDialFrequency(self, freq: int) -> None:
self.band = Bandplan.getSharedInstance().findBand(freq)
def pushDecode(self):
band = "unknown"
if self.band is not None:
band = self.band.getName()
name = "digiham.decodes.{band}.pocsag".format(band=band)
metrics = Metrics.getSharedInstance()
metric = metrics.getMetric(name)
if metric is None:
metric = CounterMetric()
metrics.addMetric(name, metric)
metric.inc()

View File

@ -15,10 +15,21 @@ logger = logging.getLogger(__name__)
class PskReporter(Reporter):
"""
This class implements the reporting interface to send received signals to pskreporter.info.
It interfaces with pskreporter as documented here: https://pskreporter.info/pskdev.html
"""
interval = 300
def getSupportedModes(self):
return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65"]
"""
Supports all valid MODE and SUBMODE values from the ADIF standard.
Current version at the time of the last change:
https://www.adif.org/312/ADIF_312.htm#Mode_Enumeration
"""
return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65", "WSPR", "FST4W"]
def stop(self):
self.cancelTimer()

View File

@ -376,6 +376,9 @@ class SdrSource(ABC):
except ProcessLookupError:
# been killed by something else, ignore
pass
except AttributeError:
# self.process has been overwritten by the monitor since we checked it, which is fine
pass
if self.monitor:
self.monitor.join()
if self.tcpSource is not None:
@ -484,7 +487,7 @@ class SdrDeviceDescription(object):
module = __import__("owrx.source.{0}".format(sdr_type), fromlist=[className])
cls = getattr(module, className)
return cls()
except (ModuleNotFoundError, AttributeError):
except (ImportError, AttributeError):
raise SdrDeviceDescriptionMissing("Device description for type {} not available".format(sdr_type))
@staticmethod
@ -511,6 +514,14 @@ class SdrDeviceDescription(object):
"""
return None
def supportsPpm(self):
"""
can be overridden if the device does not support configuring PPM correction
:return: bool
"""
return True
def getDeviceInputs(self) -> List[Input]:
keys = self.getDeviceMandatoryKeys() + self.getDeviceOptionalKeys()
return [TextInput("name", "Device name", validator=RequiredValidator())] + [
@ -565,8 +576,7 @@ class SdrDeviceDescription(object):
return ["name", "enabled"]
def getDeviceOptionalKeys(self):
return [
"ppm",
keys = [
"always-on",
"services",
"rf_gain",
@ -574,6 +584,9 @@ class SdrDeviceDescription(object):
"waterfall_levels",
"scheduler",
]
if self.supportsPpm():
keys += ["ppm"]
return keys
def getProfileMandatoryKeys(self):
return ["name", "center_freq", "samp_rate", "start_freq", "start_mod"]

View File

@ -23,6 +23,12 @@ class AirspyDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "Airspy R2 or Mini"
def supportsPpm(self):
# not supported by the device API
# frequency calibration can be done with separate tools and will be persisted on the device.
# see discussion here: https://groups.io/g/openwebrx/topic/79360293
return False
def getInputs(self) -> List[Input]:
return super().getInputs() + [
BiasTeeInput(),

View File

@ -9,3 +9,7 @@ class AirspyhfSource(SoapyConnectorSource):
class AirspyhfDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "Airspy HF+ or Discovery"
def supportsPpm(self):
# not currently supported by the SoapySDR module.
return False

11
owrx/source/bladerf.py Normal file
View File

@ -0,0 +1,11 @@
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
class BladerfSource(SoapyConnectorSource):
def getDriver(self):
return "bladerf"
class BladerfDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "Blade RF"

View File

@ -45,3 +45,7 @@ class FifiSdrSource(DirectSource):
class FifiSdrDeviceDescription(DirectSourceDeviceDescription):
def getName(self):
return "FiFi SDR"
def supportsPpm(self):
# not currently mapped, and it's unclear how this should be sent to the device
return False

View File

@ -18,6 +18,11 @@ class HackrfDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "HackRF"
def supportsPpm(self):
# not implemented by the SoapySDR module.
# see discussion here: https://groups.io/g/openwebrx/topic/78339109
return False
def getInputs(self) -> List[Input]:
return super().getInputs() + [BiasTeeInput()]

View File

@ -53,6 +53,10 @@ class PerseussdrDeviceDescription(DirectSourceDeviceDescription):
def getName(self):
return "Perseus SDR"
def supportsPpm(self):
# not currently mapped, and not available as an option to "perseustest"
return False
def getInputs(self) -> List[Input]:
return super().getInputs() + [
DropdownInput("attenuator", "Attenuator", options=AttenuatorOptions),

View File

@ -40,6 +40,10 @@ class RundsDeviceDescription(ConnectorDeviceDescription):
def getName(self):
return "R&S device using EB200 or Ammos protocol"
def supportsPpm(self):
# currently not implemented in the connector
return False
def getInputs(self) -> List[Input]:
return super().getInputs() + [
RemoteInput(),

View File

@ -1,5 +1,5 @@
from distutils.version import LooseVersion
_versionstring = "1.2.0-dev"
_versionstring = "1.2.0"
looseversion = LooseVersion(_versionstring)
openwebrx_version = "v{0}".format(looseversion)

View File

@ -30,10 +30,6 @@ class Drained(WebSocketException):
pass
class WebSocketClosed(WebSocketException):
pass
class Handler(ABC):
@abstractmethod
def handleTextMessage(self, connection, message: str):
@ -66,6 +62,7 @@ class WebSocketConnection(object):
self.setMessageHandler(messageHandler)
(self.interruptPipeRecv, self.interruptPipeSend) = Pipe(duplex=False)
self.open = True
self.socketError = False
self.sendLock = threading.Lock()
headers = {key.lower(): value for key, value in self.handler.headers.items()}
@ -118,8 +115,6 @@ class WebSocketConnection(object):
return bytes([ws_first_byte, size])
def send(self, data):
if not self.open:
raise WebSocketClosed()
# convenience
if type(data) == dict:
# allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway.
@ -142,27 +137,30 @@ class WebSocketConnection(object):
for i in range(0, len(input), n):
yield input[i: i + n]
try:
with self.sendLock:
for chunk in chunks(data_to_send, 1024):
(_, write, _) = select.select([], [self.handler.wfile], [], 10)
if self.handler.wfile in write:
written = self.handler.wfile.write(chunk)
if written != len(chunk):
logger.error("incomplete write! closing socket!")
self.close()
with self.sendLock:
if self.socketError:
logger.warning("_sendBytes() after socket error, ignoring")
else:
try:
for chunk in chunks(data_to_send, 1024):
(_, write, _) = select.select([], [self.handler.wfile], [], 10)
if self.handler.wfile in write:
written = self.handler.wfile.write(chunk)
if written != len(chunk):
logger.error("incomplete write! closing socket!")
self.close(socketError=True)
break
else:
logger.debug("socket not returned from select; closing")
self.close(socketError=True)
break
else:
logger.debug("socket not returned from select; closing")
self.close()
break
# these exception happen when the socket is closed
except OSError:
logger.exception("OSError while writing data")
self.close()
except ValueError:
logger.exception("ValueError while writing data")
self.close()
# these exception happen when the socket is closed
except OSError:
logger.exception("OSError while writing data")
self.close(socketError=True)
except ValueError:
logger.exception("ValueError while writing data")
self.close(socketError=True)
def interrupt(self):
if self.interruptPipeSend is None:
@ -180,10 +178,13 @@ class WebSocketConnection(object):
self.messageHandler.handleClose()
self.cancelPing()
logger.debug("websocket loop ended; sending close frame")
if self.socketError:
logger.debug("websocket closed in error, skipping close frame")
else:
logger.debug("websocket loop ended; sending close frame")
header = self.get_header(0, OPCODE_CLOSE)
self._sendBytes(header)
header = self.get_header(0, OPCODE_CLOSE)
self._sendBytes(header)
try:
WebSocketConnection.connections.remove(self)
@ -245,9 +246,11 @@ class WebSocketConnection(object):
available = False
except IncompleteRead:
logger.warning("incomplete read on websocket; closing connection")
self.socketError = True
self.open = False
except OSError:
logger.exception("OSError while reading data; closing connection")
self.socketError = True
self.open = False
self.interruptPipeSend.close()
@ -262,7 +265,10 @@ class WebSocketConnection(object):
self.interruptPipeRecv.close()
self.interruptPipeRecv = None
def close(self):
def close(self, socketError: bool = False):
# only set flag if it is True
if socketError:
self.socketError = True
if not self.open:
return
self.open = False
@ -270,7 +276,9 @@ class WebSocketConnection(object):
def cancelPing(self):
if self.pingTimer:
self.pingTimer.cancel()
old = self.pingTimer
self.pingTimer = None
old.cancel()
def resetPing(self):
self.cancelPing()