diff --git a/CHANGELOG.md b/CHANGELOG.md index af01871..8456653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ **unreleased** - Introduced `squelch_auto_margin` config option that allows configuring the auto squelch level +- Removed `port` configuration option; `rtltcp_compat` takes the port number with the new connectors +- Added support for new WSJT-X modes FST4 and FST4W (only available with WSJT-X 2.3) +- New devices supported: + - HPSDR devices (Hermes Lite 2) + - BBRF103 / RX666 / RX888 devices supported by libsddc + - Devices using the EB200 protocol + +**0.20.1** +- Remove broken OSM map fallback **0.20.0** - Added the ability to sign multiple keys in a single request, thus enabling multiple users to claim a single receiver diff --git a/bands.json b/bands.json index f216d5e..ad7cc8c 100644 --- a/bands.json +++ b/bands.json @@ -1,4 +1,22 @@ [ + { + "name": "2190m", + "lower_bound": 135700, + "upper_bound": 137800, + "frequencies": { + "fst4": 136000, + "fst4w": 136000 + } + }, + { + "name": "630m", + "lower_bound": 472000, + "upper_bound": 479000, + "frequencies": { + "fst4": 474200, + "fst4w": 474200 + } + }, { "name": "160m", "lower_bound": 1810000, @@ -9,7 +27,9 @@ "wspr": 1836600, "jt65": 1838000, "jt9": 1839000, - "js8": 1842000 + "js8": 1842000, + "fst4": 1839000, + "fst4w": 1836800 } }, { diff --git a/config_webrx.py b/config_webrx.py index 0bb707f..e6d6970 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -117,7 +117,7 @@ Note: if you experience audio underruns while CPU usage is 100%, you can: # Currently supported types of sdr receivers: # "rtl_sdr", "rtl_sdr_soapy", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr", # "perseussdr", "lime_sdr", "pluto_sdr", "soapy_remote", "hpsdr", "red_pitaya", "uhd", -# "radioberry", "fcdpp", "rtl_tcp" +# "radioberry", "fcdpp", "rtl_tcp", "sddc", "eb200" # For more details on specific types, please checkout the wiki: # https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices @@ -306,6 +306,14 @@ wsjt_decoding_depth = 3 # jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent wsjt_decoding_depths = {"jt65": 1} +# FST4 can be transmitted in different intervals. This setting determines which intervals will be decoded. +# available values (in seconds): 15, 30, 60, 120, 300, 900, 1800 +fst4_enabled_intervals = [15, 30] + +# FST4W can be transmitted in different intervals. This setting determines which intervals will be decoded. +# available values (in seconds): 120, 300, 900, 1800 +fst4w_enabled_intervals = [120, 300] + # JS8 comes in different speeds: normal, slow, fast, turbo. This setting controls which ones are enabled. js8_enabled_profiles = ["normal", "slow"] # JS8 decoding depth; higher value will get more results, but will also consume more cpu @@ -326,7 +334,7 @@ aprs_igate_password = "" aprs_igate_beacon = False # path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols) -aprs_symbols_path = "/opt/aprs-symbols/png" +aprs_symbols_path = "/usr/share/aprs-symbols/png" # === PSK Reporter setting === # enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info diff --git a/csdr/csdr.py b/csdr/csdr.py index c90c1b8..0c1d74a 100644 --- a/csdr/csdr.py +++ b/csdr/csdr.py @@ -29,7 +29,7 @@ import math from functools import partial from owrx.kiss import KissClient, DirewolfConfig -from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile +from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile, Fst4Profile, Fst4wProfile from owrx.js8 import Js8Profiles from owrx.audio import AudioChopper @@ -421,19 +421,23 @@ class dsp(object): if self.isWsjtMode(): smd = self.get_secondary_demodulator() - chopper_profile = None + chopper_profiles = None if smd == "ft8": - chopper_profile = Ft8Profile() + chopper_profiles = [Ft8Profile()] elif smd == "wspr": - chopper_profile = WsprProfile() + chopper_profiles = [WsprProfile()] elif smd == "jt65": - chopper_profile = Jt65Profile() + chopper_profiles = [Jt65Profile()] elif smd == "jt9": - chopper_profile = Jt9Profile() + chopper_profiles = [Jt9Profile()] elif smd == "ft4": - chopper_profile = Ft4Profile() - if chopper_profile is not None: - chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile) + chopper_profiles = [Ft4Profile()] + elif smd == "fst4": + chopper_profiles = Fst4Profile.getEnabledProfiles() + elif smd == "fst4w": + chopper_profiles = Fst4wProfile.getEnabledProfiles() + if chopper_profiles is not None and len(chopper_profiles): + chopper = AudioChopper(self, self.secondary_process_demod.stdout, *chopper_profiles) chopper.start() self.output.send_output("wsjt_demod", chopper.read) elif self.isJs8(): @@ -575,7 +579,7 @@ class dsp(object): def isWsjtMode(self, demodulator=None): if demodulator is None: demodulator = self.get_secondary_demodulator() - return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"] + return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w"] def isJs8(self, demodulator = None): if demodulator is None: @@ -665,6 +669,9 @@ class dsp(object): def get_operating_freq(self): return self.center_freq + self.offset_freq + def set_bandpass(self, bandpass): + self.set_bpf(bandpass.low_cut, bandpass.high_cut) + def set_bpf(self, low_cut, high_cut): self.low_cut = low_cut self.high_cut = high_cut diff --git a/debian/changelog b/debian/changelog index 7357723..c7dc727 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,9 +1,23 @@ openwebrx (0.21.0) UNRELEASED; urgency=low * Introduced `squelch_auto_margin` config option that allows configuring the auto squelch level + * Removed `port` configuration option; `rtltcp_compat` takes the port number + with the new connectors + * Added support for new WSJT-X modes FST4 and FST4W (only available with + WSJT-X 2.3) + * New devices supported: + - HPSDR devices (Hermes Lite 2) (`"type": "hpsdr"`) + - BBRF103 / RX666 / RX888 devices supported by libsddc (`"type": "sddc"`) + - Devices using the EB200 protocol (`"type": "eb200"`) -- Jakob Ketterl Sun, 11 Oct 2020 21:12:00 +0000 +openwebrx (0.20.1) buster focal; urgency=low + + * Remove broken OSM map fallback + + -- Jakob Ketterl Mon, 30 Nov 2020 17:29:00 +0000 + openwebrx (0.20.0) buster focal; urgency=low * Added the ability to sign multiple keys in a single request, thus enabling diff --git a/debian/control b/debian/control index 0c2fcd5..3538a8d 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Vcs-Git: https://github.com/jketterl/openwebrx.git Package: openwebrx Architecture: all -Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.17), netcat, owrx-connector (>= 0.3), python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends} -Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, soapysdr-tools +Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.17), netcat, owrx-connector (>= 0.4), soapysdr-tools, python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends} +Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, eb200-connector, hpsdrconnector, aprs-symbols Description: multi-user web sdr Open source, multi-user SDR receiver with a web interface \ No newline at end of file diff --git a/docker.sh b/docker.sh index eda1ad0..1583af6 100755 --- a/docker.sh +++ b/docker.sh @@ -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-redpitaya openwebrx-rtltcp 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-redpitaya openwebrx-rtltcp openwebrx-eb200 openwebrx-hpsdr openwebrx-full openwebrx" ALL_ARCHS="x86_64 armv7l aarch64" TAG=${TAG:-"latest"} ARCHTAG="$TAG-$ARCH" diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base index 59b380d..c338dc0 100644 --- a/docker/Dockerfiles/Dockerfile-base +++ b/docker/Dockerfiles/Dockerfile-base @@ -4,6 +4,7 @@ COPY docker/files/js8call/js8call-hamlib.patch \ docker/files/wsjtx/wsjtx.patch \ docker/files/wsjtx/wsjtx-hamlib.patch \ docker/files/dream/dream.patch \ + docker/files/direwolf/direwolf-hamlib.patch \ docker/scripts/install-dependencies.sh / RUN /install-dependencies.sh && \ rm /install-dependencies.sh && \ diff --git a/docker/Dockerfiles/Dockerfile-eb200 b/docker/Dockerfiles/Dockerfile-eb200 new file mode 100644 index 0000000..f2f5181 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-eb200 @@ -0,0 +1,12 @@ +ARG ARCHTAG +FROM openwebrx-base:$ARCHTAG + +COPY docker/scripts/install-connectors.sh \ + docker/scripts/install-dependencies-eb200.sh / + +RUN /install-connectors.sh &&\ + rm /install-connectors.sh && \ + /install-dependencies-eb200.sh && \ + rm /install-dependencies-eb200.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-full b/docker/Dockerfiles/Dockerfile-full index 3a38c9f..b68d18a 100644 --- a/docker/Dockerfiles/Dockerfile-full +++ b/docker/Dockerfiles/Dockerfile-full @@ -19,7 +19,9 @@ RUN /install-dependencies-rtlsdr.sh &&\ /install-dependencies-radioberry.sh &&\ /install-dependencies-uhd.sh &&\ /install-dependencies-redpitaya.sh &&\ + /install-dependencies-hpsdr.sh &&\ /install-connectors.sh &&\ + /install-dependencies-eb200.sh &&\ rm /install-dependencies-*.sh &&\ rm /install-lib.*.patch && \ rm /install-connectors.sh diff --git a/docker/Dockerfiles/Dockerfile-hpsdr b/docker/Dockerfiles/Dockerfile-hpsdr new file mode 100644 index 0000000..96d58b9 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-hpsdr @@ -0,0 +1,9 @@ +ARG ARCHTAG +FROM openwebrx-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-hpsdr.sh / + +RUN /install-dependencies-hpsdr.sh &&\ + rm /install-dependencies-hpsdr.sh + +COPY . /opt/openwebrx diff --git a/docker/files/direwolf/direwolf-hamlib.patch b/docker/files/direwolf/direwolf-hamlib.patch new file mode 100644 index 0000000..2347c24 --- /dev/null +++ b/docker/files/direwolf/direwolf-hamlib.patch @@ -0,0 +1,20 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 9e710f5..da90b43 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -257,13 +257,8 @@ else() + set(GPSD_LIBRARIES "") + endif() + +-find_package(hamlib) +-if(HAMLIB_FOUND) +- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_HAMLIB") +-else() +- set(HAMLIB_INCLUDE_DIRS "") +- set(HAMLIB_LIBRARIES "") +-endif() ++set(HAMLIB_INCLUDE_DIRS "") ++set(HAMLIB_LIBRARIES "") + + if(LINUX) + find_package(ALSA REQUIRED) diff --git a/docker/scripts/install-connectors.sh b/docker/scripts/install-connectors.sh index 33390e1..be76988 100755 --- a/docker/scripts/install-connectors.sh +++ b/docker/scripts/install-connectors.sh @@ -24,7 +24,8 @@ 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.3.0 +# latest develop as of 2020-11-28 (int32 samples; debhelper) +cmakebuild owrx_connector 87a2fcc54e221aad71ec0700737ca7f385c388de apt-get -y purge --autoremove $BUILD_PACKAGES apt-get clean diff --git a/docker/scripts/install-dependencies-eb200.sh b/docker/scripts/install-dependencies-eb200.sh new file mode 100755 index 0000000..ce0aba1 --- /dev/null +++ b/docker/scripts/install-dependencies-eb200.sh @@ -0,0 +1,33 @@ +#!/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="" +BUILD_PACKAGES="git cmake make gcc g++ pkg-config" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/jketterl/eb200_connector.git +# latest from develop as of 2020-12-01 +cmakebuild eb200_connector 9c8313770c1072df72d2fdb85307ca206c29c60a + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-hpsdr.sh b/docker/scripts/install-dependencies-hpsdr.sh new file mode 100755 index 0000000..03ff176 --- /dev/null +++ b/docker/scripts/install-dependencies-hpsdr.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +BUILD_PACKAGES="git wget gcc libc6-dev" + +apt-get update +apt-get -y install --no-install-recommends $BUILD_PACKAGES + +pushd /tmp + +ARCH=$(uname -m) +GOVERSION=1.15.5 + +case ${ARCH} in + x86_64) + PACKAGE=go${GOVERSION}.linux-amd64.tar.gz + ;; + armv*) + PACKAGE=go${GOVERSION}.linux-armv6l.tar.gz + ;; + aarch64) + PACKAGE=go${GOVERSION}.linux-arm64.tar.gz + ;; +esac + +wget https://golang.org/dl/${PACKAGE} +tar xfz $PACKAGE + +git clone https://github.com/jancona/hpsdrconnector.git +pushd hpsdrconnector +git checkout v0.4.2 +/tmp/go/bin/go build +install -m 0755 hpsdrconnector /usr/local/bin + +popd + +rm -rf hpsdrconnector +rm -rf go +rm $PACKAGE + +popd + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index a86aa56..338201c 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -69,13 +69,17 @@ mv /wsjtx.patch ${WSJT_DIR} cmakebuild ${WSJT_DIR} rm ${WSJT_TGZ} -git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git +git clone --depth 1 -b 1.6 https://github.com/wb2osz/direwolf.git cd direwolf # hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need. -# by setting enable_hamlib we prevent direwolf from linking to it, and it can be stripped at the end of the script. -make enable_hamlib= +# this patch prevents direwolf from linking to it, and it can be stripped at the end of the script. +patch -Np1 < /direwolf-hamlib.patch +mkdir build +cd build +cmake .. +make make install -cd .. +cd ../.. rm -rf direwolf # strip lots of generic documentation that will never be read inside a docker container rm /usr/local/share/doc/direwolf/*.pdf @@ -106,8 +110,8 @@ popd rm -rf dream rm dream-2.1.1-svn808.tar.gz -git clone https://github.com/hessu/aprs-symbols /opt/aprs-symbols -pushd /opt/aprs-symbols +git clone https://github.com/hessu/aprs-symbols /usr/share/aprs-symbols +pushd /usr/share/aprs-symbols git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802 popd diff --git a/htdocs/lib/Demodulator.js b/htdocs/lib/Demodulator.js index a1f3798..de6f3c5 100644 --- a/htdocs/lib/Demodulator.js +++ b/htdocs/lib/Demodulator.js @@ -11,6 +11,8 @@ Filter.prototype.getLimits = function() { max_bw = 80000; } else if (this.demodulator.get_modulation() === 'drm') { max_bw = 100000; + } else if (this.demodulator.get_secondary_demod() === 'packet') { + max_bw = 12500; } else { max_bw = (audioEngine.getOutputRate() / 2) - 1; } diff --git a/htdocs/map.js b/htdocs/map.js index 0d447e2..69be2ca 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -223,26 +223,14 @@ case "config": var config = json.value; if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ - var mapTypeId = config.google_maps_api_key ? 'roadmap' : 'OSM'; - map = new google.maps.Map($('.openwebrx-map')[0], { center: { lat: config.receiver_gps.lat, lng: config.receiver_gps.lon }, zoom: 5, - mapTypeId: mapTypeId }); - map.mapTypes.set("OSM", new google.maps.ImageMapType({ - getTileUrl: function(coord, zoom) { - return "https://maps.wikimedia.org/osm-intl/" + zoom + "/" + coord.x + "/" + coord.y + ".png"; - }, - tileSize: new google.maps.Size(256, 256), - name: "OpenStreetMap", - maxZoom: 18 - })); - $.getScript("static/lib/nite-overlay.js").done(function(){ nite.init(map); setInterval(function() { nite.refresh() }, 10000); // every 10s diff --git a/owrx/audio.py b/owrx/audio.py index 330b88b..89956fd 100644 --- a/owrx/audio.py +++ b/owrx/audio.py @@ -194,10 +194,11 @@ class AudioWriter(object): try: rc = decoder.wait(timeout=10) if rc != 0: - logger.warning("decoder return code: %i", rc) + raise RuntimeError("decoder return code: {0}".format(rc)) except subprocess.TimeoutExpired: logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid) decoder.kill() + raise def start(self): (self.wavefilename, self.wavefile) = self.getWaveFile() diff --git a/owrx/command.py b/owrx/command.py index 87ae711..0559b72 100644 --- a/owrx/command.py +++ b/owrx/command.py @@ -76,4 +76,4 @@ class Option(CommandMapping): class Argument(CommandMapping): def map(self, value): - return value + return str(value) diff --git a/owrx/feature.py b/owrx/feature.py index 16e5b3d..a9cfe5a 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -68,7 +68,9 @@ class FeatureDetector(object): "red_pitaya": ["soapy_connector", "soapy_red_pitaya"], "radioberry": ["soapy_connector", "soapy_radioberry"], "fcdpp": ["soapy_connector", "soapy_fcdpp"], + "sddc": ["sddc_connector"], "hpsdr": ["hpsdr_connector"], + "eb200": ["eb200_connector"], # optional features and their requirements "digital_voice_digiham": ["digiham", "sox"], "digital_voice_dsd": ["dsd", "sox", "digiham"], @@ -256,7 +258,7 @@ class FeatureDetector(object): ) def _check_connector(self, command): - required_version = LooseVersion("0.3") + required_version = LooseVersion("0.4") owrx_connector_version_regex = re.compile("^owrx-connector version (.*)$") @@ -499,9 +501,25 @@ class FeatureDetector(object): """ return self.command_is_runnable("dream --help", 0) + def has_sddc_connector(self): + """ + The sddc_connector allows connectivity with SDR devices powered by libsddc, e.g. RX666, RX888, HF103. + + You can find more information [here](https://github.com/jketterl/sddc_connector). + """ + return self._check_connector("sddc_connector") + def has_hpsdr_connector(self): """ In order to use the HPSDR connector, you will need to install [hpsdrconnector] (https://github.com/jancona/hpsdrconnector). """ return self.command_is_runnable("hpsdrconnector -h") + + def has_eb200_connector(self): + """ + To use radios supporting the EB200 radios, you need to install the eb200_connector. + + You can find more information [here](https://github.com/jketterl/eb200_connector). + """ + return self._check_connector("eb200_connector") \ No newline at end of file diff --git a/owrx/modes.py b/owrx/modes.py index 09e79a3..8b642e1 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -24,6 +24,12 @@ class Mode(object): def is_service(self): return self.service + def get_bandpass(self): + return self.bandpass + + def get_modulation(self): + return self.modulation + class AnalogMode(Mode): pass @@ -36,6 +42,14 @@ class DigitalMode(Mode): super().__init__(modulation, name, bandpass, requirements, service, squelch) self.underlying = underlying + def get_bandpass(self): + if self.bandpass is not None: + return self.bandpass + return Modes.findByModulation(self.underlying[0]).get_bandpass() + + def get_modulation(self): + return Modes.findByModulation(self.underlying[0]).get_modulation() + class Modes(object): mappings = [ @@ -54,16 +68,24 @@ class Modes(object): AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False), DigitalMode("bpsk31", "BPSK31", underlying=["usb"]), DigitalMode("bpsk63", "BPSK63", underlying=["usb"]), - DigitalMode("ft8", "FT8", underlying=["usb"], requirements=["wsjt-x"], service=True), - DigitalMode("ft4", "FT4", underlying=["usb"], requirements=["wsjt-x"], service=True), - DigitalMode("jt65", "JT65", underlying=["usb"], requirements=["wsjt-x"], service=True), - DigitalMode("jt9", "JT9", underlying=["usb"], requirements=["wsjt-x"], service=True), + DigitalMode("ft8", "FT8", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), + DigitalMode("ft4", "FT4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), + DigitalMode("jt65", "JT65", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), + DigitalMode("jt9", "JT9", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), DigitalMode( "wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True ), - DigitalMode("js8", "JS8Call", underlying=["usb"], requirements=["js8call"], service=True), + DigitalMode("fst4", "FST4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), + DigitalMode("fst4w", "FST4W", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True), + DigitalMode("js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True), DigitalMode( - "packet", "Packet", underlying=["nfm", "usb", "lsb"], requirements=["packet"], service=True, squelch=False + "packet", + "Packet", + underlying=["nfm", "usb", "lsb"], + bandpass=Bandpass(-6250, 6250), + requirements=["packet"], + service=True, + squelch=False, ), DigitalMode( "pocsag", diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index 981ecc8..c3182b7 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -30,7 +30,7 @@ class PskReporter(object): sharedInstance = None creationLock = threading.Lock() interval = 300 - supportedModes = ["FT8", "FT4", "JT9", "JT65", "JS8"] + supportedModes = ["FT8", "FT4", "JT9", "JT65", "FST4", "FST4W", "JS8"] @staticmethod def getSharedInstance(): diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py index 98cf557..5984050 100644 --- a/owrx/service/__init__.py +++ b/owrx/service/__init__.py @@ -155,18 +155,14 @@ class ServiceHandler(SdrSourceEventClient): ) else: for group in groups: - frequencies = sorted([f["frequency"] for f in group]) - min = frequencies[0] - max = frequencies[-1] - cf = (min + max) / 2 - bw = max - min + cf = self.get_center_frequency(group) + bw = self.get_bandwidth(group) logger.debug( "group center frequency: {0}, bandwidth: {1}".format(cf, bw) ) resampler_props = PropertyLayer() resampler_props["center_freq"] = cf - # TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths - resampler_props["samp_rate"] = bw + 24000 + resampler_props["samp_rate"] = bw resampler = Resampler(resampler_props, self.source) resampler.start() @@ -180,6 +176,23 @@ class ServiceHandler(SdrSourceEventClient): # resampler goes in after the services since it must not be shutdown as long as the services are still running self.services.append(resampler) + def get_min_max(self, group): + frequencies = sorted(group, key=lambda f: f["frequency"]) + lowest = frequencies[0] + min = lowest["frequency"] + Modes.findByModulation(lowest["mode"]).get_bandpass().low_cut + highest = frequencies[-1] + max = highest["frequency"] + Modes.findByModulation(highest["mode"]).get_bandpass().high_cut + return min, max + + def get_center_frequency(self, group): + min, max = self.get_min_max(group) + return (min + max) / 2 + + def get_bandwidth(self, group): + minFreq, maxFreq = self.get_min_max(group) + # minimum bandwidth for a resampler: 25kHz + return max(maxFreq - minFreq, 25000) + def optimizeResampling(self, freqs, bandwidth): freqs = sorted(freqs, key=lambda f: f["frequency"]) distances = [ @@ -203,12 +216,10 @@ class ServiceHandler(SdrSourceEventClient): previous = split groups.append([f for f in freqs if previous < f["frequency"]]) - def get_bandwitdh(group): - freqs = sorted([f["frequency"] for f in group]) - # the group will process the full BW once, plus the reduced BW once for each group member - return bandwidth + len(group) * (freqs[-1] - freqs[0] + 24000) + def get_total_bandwidth(group): + return bandwidth + len(group) * self.get_bandwidth(group) - total_bandwidth = sum([get_bandwitdh(group) for group in groups]) + total_bandwidth = sum([get_total_bandwidth(group) for group in groups]) return { "num_splits": num_splits, "total_bandwidth": total_bandwidth, @@ -250,16 +261,9 @@ class ServiceHandler(SdrSourceEventClient): center_freq = source.getProps()["center_freq"] d.set_offset_freq(frequency - center_freq) d.set_center_freq(center_freq) - if mode == "packet": - d.set_demodulator("nfm") - d.set_bpf(-4000, 4000) - elif mode == "wspr": - d.set_demodulator("usb") - # WSPR only samples between 1400 and 1600 Hz - d.set_bpf(1350, 1650) - else: - d.set_demodulator("usb") - d.set_bpf(0, 3000) + modeObject = Modes.findByModulation(mode) + d.set_demodulator(modeObject.get_modulation()) + d.set_bandpass(modeObject.get_bandpass()) d.set_secondary_demodulator(mode) d.set_audio_compression("none") d.set_samp_rate(source.getProps()["samp_rate"]) diff --git a/owrx/source/__init__.py b/owrx/source/__init__.py index c71c5a3..cf5e198 100644 --- a/owrx/source/__init__.py +++ b/owrx/source/__init__.py @@ -59,10 +59,7 @@ class SdrSource(ABC): self.activateProfile() self.wireEvents() - if "port" in props and props["port"] is not None: - self.port = props["port"] - else: - self.port = getAvailablePort() + self.port = getAvailablePort() self.monitor = None self.clients = [] self.spectrumClients = [] diff --git a/owrx/source/connector.py b/owrx/source/connector.py index f28da98..47eec5d 100644 --- a/owrx/source/connector.py +++ b/owrx/source/connector.py @@ -23,7 +23,7 @@ class ConnectorSource(SdrSource): "controlPort": Option("-c"), "device": Option("-d"), "iqswap": Flag("-i"), - "rtltcp_compat": Flag("-r"), + "rtltcp_compat": Option("-r"), "ppm": Option("-P"), "rf_gain": Option("-g"), } diff --git a/owrx/source/eb200.py b/owrx/source/eb200.py new file mode 100644 index 0000000..a234a59 --- /dev/null +++ b/owrx/source/eb200.py @@ -0,0 +1,15 @@ +from owrx.source.connector import ConnectorSource +from owrx.command import Argument, Flag + + +class Eb200Source(ConnectorSource): + def getCommandMapper(self): + return ( + super() + .getCommandMapper() + .setBase("eb200_connector") + .setMappings({ + "long": Flag("-l"), + "remote": Argument(), + }) + ) diff --git a/owrx/source/sddc.py b/owrx/source/sddc.py new file mode 100644 index 0000000..58e7380 --- /dev/null +++ b/owrx/source/sddc.py @@ -0,0 +1,6 @@ +from owrx.source.connector import ConnectorSource + + +class SddcSource(ConnectorSource): + def getCommandMapper(self): + return super().getCommandMapper().setBase("sddc_connector") diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 0ba8160..cb9c15e 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -85,8 +85,56 @@ class Ft4Profile(WsjtProfile): return ["jt9", "--ft4", "-d", str(self.decoding_depth("ft4")), file] +class Fst4Profile(WsjtProfile): + availableIntervals = [15, 30, 60, 120, 300, 900, 1800] + + def __init__(self, interval): + self.interval = interval + + def getInterval(self): + return self.interval + + def getFileTimestampFormat(self): + if self.interval < 60: + return "%y%m%d_%H%M%S" + return "%y%m%d_%H%M" + + def decoder_commandline(self, file): + return ["jt9", "--fst4", "-p", str(self.interval), "-d", str(self.decoding_depth("fst4")), file] + + @staticmethod + def getEnabledProfiles(): + config = Config.get() + profiles = config["fst4_enabled_intervals"] if "fst4_enabled_intervals" in config else [] + return [Fst4Profile(i) for i in profiles if i in Fst4Profile.availableIntervals] + + +class Fst4wProfile(WsjtProfile): + availableIntervals = [120, 300, 900, 1800] + + def __init__(self, interval): + self.interval = interval + + def getInterval(self): + return self.interval + + def getFileTimestampFormat(self): + if self.interval < 60: + return "%y%m%d_%H%M%S" + return "%y%m%d_%H%M" + + def decoder_commandline(self, file): + return ["jt9", "--fst4w", "-p", str(self.interval), "-d", str(self.decoding_depth("fst4w")), file] + + @staticmethod + def getEnabledProfiles(): + config = Config.get() + profiles = config["fst4w_enabled_intervals"] if "fst4w_enabled_intervals" in config else [] + return [Fst4wProfile(i) for i in profiles if i in Fst4wProfile.availableIntervals] + + class WsjtParser(Parser): - modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"} + modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4", "`": "FST4"} def parse(self, messages): for data in messages: @@ -115,7 +163,7 @@ class WsjtParser(Parser): PskReporter.getSharedInstance().spot(out) self.handler.write_wsjt_message(out) - except ValueError: + except (ValueError, IndexError): logger.exception("error while parsing wsjt message") def pushDecode(self, mode): @@ -139,6 +187,8 @@ class WsjtParser(Parser): class Decoder(ABC): + locator_pattern = re.compile(".*\\s([A-Z0-9]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$") + def parse_timestamp(self, instring, dateformat): ts = datetime.strptime(instring, dateformat) return int( @@ -149,23 +199,36 @@ class Decoder(ABC): def parse(self, msg, dial_freq): pass + def parseMessage(self, msg): + m = Decoder.locator_pattern.match(msg) + if m is None: + return {} + # this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very + # likely this just means roger roger goodbye. + if m.group(3) == "RR73": + return {"callsign": m.group(1)} + return {"callsign": m.group(1), "locator": m.group(3)} + class Jt9Decoder(Decoder): - locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") - def parse(self, msg, dial_freq): # ft8 sample # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' # jt65 sample # '2352 -7 0.4 1801 # R0WAS R2ABM KO85' # '0003 -4 0.4 1762 # CQ R2ABM KO85' + # fst4 sample + # '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV' modes = list(WsjtParser.modes.keys()) if msg[19] in modes: dateformat = "%H%M" else: dateformat = "%H%M%S" - timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat) - msg = msg[len(dateformat) + 1 :] + try: + timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat) + except ValueError: + timestamp = None + msg = msg[len(dateformat) + 1:] modeChar = msg[14:15] mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" wsjt_msg = msg[17:53].strip() @@ -181,16 +244,6 @@ class Jt9Decoder(Decoder): result.update(self.parseMessage(wsjt_msg)) return result - def parseMessage(self, msg): - m = Jt9Decoder.locator_pattern.match(msg) - if m is None: - return {} - # this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very - # likely this just means roger roger goodbye. - if m.group(2) == "RR73": - return {"callsign": m.group(1)} - return {"callsign": m.group(1), "locator": m.group(2)} - class WsprDecoder(Decoder): wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")