Merge branch 'develop' into m17

This commit is contained in:
Jakob Ketterl 2020-12-08 16:57:00 +01:00
commit 6af19f44e8
29 changed files with 380 additions and 88 deletions

View File

@ -1,5 +1,14 @@
**unreleased** **unreleased**
- Introduced `squelch_auto_margin` config option that allows configuring the auto squelch level - 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** **0.20.0**
- Added the ability to sign multiple keys in a single request, thus enabling multiple users to claim a single receiver - Added the ability to sign multiple keys in a single request, thus enabling multiple users to claim a single receiver

View File

@ -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", "name": "160m",
"lower_bound": 1810000, "lower_bound": 1810000,
@ -9,7 +27,9 @@
"wspr": 1836600, "wspr": 1836600,
"jt65": 1838000, "jt65": 1838000,
"jt9": 1839000, "jt9": 1839000,
"js8": 1842000 "js8": 1842000,
"fst4": 1839000,
"fst4w": 1836800
} }
}, },
{ {

View File

@ -117,7 +117,7 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
# Currently supported types of sdr receivers: # Currently supported types of sdr receivers:
# "rtl_sdr", "rtl_sdr_soapy", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr", # "rtl_sdr", "rtl_sdr_soapy", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr",
# "perseussdr", "lime_sdr", "pluto_sdr", "soapy_remote", "hpsdr", "red_pitaya", "uhd", # "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: # For more details on specific types, please checkout the wiki:
# https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices # 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 # jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent
wsjt_decoding_depths = {"jt65": 1} 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 comes in different speeds: normal, slow, fast, turbo. This setting controls which ones are enabled.
js8_enabled_profiles = ["normal", "slow"] js8_enabled_profiles = ["normal", "slow"]
# JS8 decoding depth; higher value will get more results, but will also consume more cpu # 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 aprs_igate_beacon = False
# path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols) # 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 === # === PSK Reporter setting ===
# enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info # enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info

View File

@ -29,7 +29,7 @@ import math
from functools import partial from functools import partial
from owrx.kiss import KissClient, DirewolfConfig 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.js8 import Js8Profiles
from owrx.audio import AudioChopper from owrx.audio import AudioChopper
@ -421,19 +421,23 @@ class dsp(object):
if self.isWsjtMode(): if self.isWsjtMode():
smd = self.get_secondary_demodulator() smd = self.get_secondary_demodulator()
chopper_profile = None chopper_profiles = None
if smd == "ft8": if smd == "ft8":
chopper_profile = Ft8Profile() chopper_profiles = [Ft8Profile()]
elif smd == "wspr": elif smd == "wspr":
chopper_profile = WsprProfile() chopper_profiles = [WsprProfile()]
elif smd == "jt65": elif smd == "jt65":
chopper_profile = Jt65Profile() chopper_profiles = [Jt65Profile()]
elif smd == "jt9": elif smd == "jt9":
chopper_profile = Jt9Profile() chopper_profiles = [Jt9Profile()]
elif smd == "ft4": elif smd == "ft4":
chopper_profile = Ft4Profile() chopper_profiles = [Ft4Profile()]
if chopper_profile is not None: elif smd == "fst4":
chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile) 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() chopper.start()
self.output.send_output("wsjt_demod", chopper.read) self.output.send_output("wsjt_demod", chopper.read)
elif self.isJs8(): elif self.isJs8():
@ -575,7 +579,7 @@ class dsp(object):
def isWsjtMode(self, demodulator=None): def isWsjtMode(self, demodulator=None):
if demodulator is None: if demodulator is None:
demodulator = self.get_secondary_demodulator() 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): def isJs8(self, demodulator = None):
if demodulator is None: if demodulator is None:
@ -665,6 +669,9 @@ class dsp(object):
def get_operating_freq(self): def get_operating_freq(self):
return self.center_freq + self.offset_freq 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): def set_bpf(self, low_cut, high_cut):
self.low_cut = low_cut self.low_cut = low_cut
self.high_cut = high_cut self.high_cut = high_cut

14
debian/changelog vendored
View File

@ -1,9 +1,23 @@
openwebrx (0.21.0) UNRELEASED; urgency=low openwebrx (0.21.0) UNRELEASED; urgency=low
* Introduced `squelch_auto_margin` config option that allows configuring the * Introduced `squelch_auto_margin` config option that allows configuring the
auto squelch level 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 <jakob.ketterl@gmx.de> Sun, 11 Oct 2020 21:12:00 +0000 -- Jakob Ketterl <jakob.ketterl@gmx.de> Sun, 11 Oct 2020 21:12:00 +0000
openwebrx (0.20.1) buster focal; urgency=low
* Remove broken OSM map fallback
-- Jakob Ketterl <jakob.ketterl@gmx.de> Mon, 30 Nov 2020 17:29:00 +0000
openwebrx (0.20.0) buster focal; urgency=low openwebrx (0.20.0) buster focal; urgency=low
* Added the ability to sign multiple keys in a single request, thus enabling * Added the ability to sign multiple keys in a single request, thus enabling

4
debian/control vendored
View File

@ -10,7 +10,7 @@ Vcs-Git: https://github.com/jketterl/openwebrx.git
Package: openwebrx Package: openwebrx
Architecture: all 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} 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, soapysdr-tools Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, eb200-connector, hpsdrconnector, aprs-symbols
Description: multi-user web sdr Description: multi-user web sdr
Open source, multi-user SDR receiver with a web interface Open source, multi-user SDR receiver with a web interface

View File

@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
ARCH=$(uname -m) 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" ALL_ARCHS="x86_64 armv7l aarch64"
TAG=${TAG:-"latest"} TAG=${TAG:-"latest"}
ARCHTAG="$TAG-$ARCH" ARCHTAG="$TAG-$ARCH"

View File

@ -4,6 +4,7 @@ COPY docker/files/js8call/js8call-hamlib.patch \
docker/files/wsjtx/wsjtx.patch \ docker/files/wsjtx/wsjtx.patch \
docker/files/wsjtx/wsjtx-hamlib.patch \ docker/files/wsjtx/wsjtx-hamlib.patch \
docker/files/dream/dream.patch \ docker/files/dream/dream.patch \
docker/files/direwolf/direwolf-hamlib.patch \
docker/scripts/install-dependencies.sh / docker/scripts/install-dependencies.sh /
RUN /install-dependencies.sh && \ RUN /install-dependencies.sh && \
rm /install-dependencies.sh && \ rm /install-dependencies.sh && \

View File

@ -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

View File

@ -19,7 +19,9 @@ RUN /install-dependencies-rtlsdr.sh &&\
/install-dependencies-radioberry.sh &&\ /install-dependencies-radioberry.sh &&\
/install-dependencies-uhd.sh &&\ /install-dependencies-uhd.sh &&\
/install-dependencies-redpitaya.sh &&\ /install-dependencies-redpitaya.sh &&\
/install-dependencies-hpsdr.sh &&\
/install-connectors.sh &&\ /install-connectors.sh &&\
/install-dependencies-eb200.sh &&\
rm /install-dependencies-*.sh &&\ rm /install-dependencies-*.sh &&\
rm /install-lib.*.patch && \ rm /install-lib.*.patch && \
rm /install-connectors.sh rm /install-connectors.sh

View File

@ -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

View File

@ -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)

View File

@ -24,7 +24,8 @@ apt-get update
apt-get -y install --no-install-recommends $BUILD_PACKAGES apt-get -y install --no-install-recommends $BUILD_PACKAGES
git clone https://github.com/jketterl/owrx_connector.git 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 -y purge --autoremove $BUILD_PACKAGES
apt-get clean apt-get clean

View File

@ -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/*

View File

@ -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/*

View File

@ -69,13 +69,17 @@ mv /wsjtx.patch ${WSJT_DIR}
cmakebuild ${WSJT_DIR} cmakebuild ${WSJT_DIR}
rm ${WSJT_TGZ} 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 cd direwolf
# hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need. # 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. # this patch prevents direwolf from linking to it, and it can be stripped at the end of the script.
make enable_hamlib= patch -Np1 < /direwolf-hamlib.patch
mkdir build
cd build
cmake ..
make
make install make install
cd .. cd ../..
rm -rf direwolf rm -rf direwolf
# strip lots of generic documentation that will never be read inside a docker container # strip lots of generic documentation that will never be read inside a docker container
rm /usr/local/share/doc/direwolf/*.pdf rm /usr/local/share/doc/direwolf/*.pdf
@ -106,8 +110,8 @@ popd
rm -rf dream rm -rf dream
rm dream-2.1.1-svn808.tar.gz rm dream-2.1.1-svn808.tar.gz
git clone https://github.com/hessu/aprs-symbols /opt/aprs-symbols git clone https://github.com/hessu/aprs-symbols /usr/share/aprs-symbols
pushd /opt/aprs-symbols pushd /usr/share/aprs-symbols
git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802 git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
popd popd

View File

@ -11,6 +11,8 @@ Filter.prototype.getLimits = function() {
max_bw = 80000; max_bw = 80000;
} else if (this.demodulator.get_modulation() === 'drm') { } else if (this.demodulator.get_modulation() === 'drm') {
max_bw = 100000; max_bw = 100000;
} else if (this.demodulator.get_secondary_demod() === 'packet') {
max_bw = 12500;
} else { } else {
max_bw = (audioEngine.getOutputRate() / 2) - 1; max_bw = (audioEngine.getOutputRate() / 2) - 1;
} }

View File

@ -223,26 +223,14 @@
case "config": case "config":
var config = json.value; var config = json.value;
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ 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], { map = new google.maps.Map($('.openwebrx-map')[0], {
center: { center: {
lat: config.receiver_gps.lat, lat: config.receiver_gps.lat,
lng: config.receiver_gps.lon lng: config.receiver_gps.lon
}, },
zoom: 5, 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(){ $.getScript("static/lib/nite-overlay.js").done(function(){
nite.init(map); nite.init(map);
setInterval(function() { nite.refresh() }, 10000); // every 10s setInterval(function() { nite.refresh() }, 10000); // every 10s

View File

@ -194,10 +194,11 @@ class AudioWriter(object):
try: try:
rc = decoder.wait(timeout=10) rc = decoder.wait(timeout=10)
if rc != 0: if rc != 0:
logger.warning("decoder return code: %i", rc) raise RuntimeError("decoder return code: {0}".format(rc))
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid) logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid)
decoder.kill() decoder.kill()
raise
def start(self): def start(self):
(self.wavefilename, self.wavefile) = self.getWaveFile() (self.wavefilename, self.wavefile) = self.getWaveFile()

View File

@ -76,4 +76,4 @@ class Option(CommandMapping):
class Argument(CommandMapping): class Argument(CommandMapping):
def map(self, value): def map(self, value):
return value return str(value)

View File

@ -68,7 +68,9 @@ class FeatureDetector(object):
"red_pitaya": ["soapy_connector", "soapy_red_pitaya"], "red_pitaya": ["soapy_connector", "soapy_red_pitaya"],
"radioberry": ["soapy_connector", "soapy_radioberry"], "radioberry": ["soapy_connector", "soapy_radioberry"],
"fcdpp": ["soapy_connector", "soapy_fcdpp"], "fcdpp": ["soapy_connector", "soapy_fcdpp"],
"sddc": ["sddc_connector"],
"hpsdr": ["hpsdr_connector"], "hpsdr": ["hpsdr_connector"],
"eb200": ["eb200_connector"],
# optional features and their requirements # optional features and their requirements
"digital_voice_digiham": ["digiham", "sox"], "digital_voice_digiham": ["digiham", "sox"],
"digital_voice_dsd": ["dsd", "sox", "digiham"], "digital_voice_dsd": ["dsd", "sox", "digiham"],
@ -256,7 +258,7 @@ class FeatureDetector(object):
) )
def _check_connector(self, command): def _check_connector(self, command):
required_version = LooseVersion("0.3") required_version = LooseVersion("0.4")
owrx_connector_version_regex = re.compile("^owrx-connector version (.*)$") owrx_connector_version_regex = re.compile("^owrx-connector version (.*)$")
@ -499,9 +501,25 @@ class FeatureDetector(object):
""" """
return self.command_is_runnable("dream --help", 0) 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): def has_hpsdr_connector(self):
""" """
In order to use the HPSDR connector, you will need to install [hpsdrconnector] In order to use the HPSDR connector, you will need to install [hpsdrconnector]
(https://github.com/jancona/hpsdrconnector). (https://github.com/jancona/hpsdrconnector).
""" """
return self.command_is_runnable("hpsdrconnector -h") 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")

View File

@ -24,6 +24,12 @@ class Mode(object):
def is_service(self): def is_service(self):
return self.service return self.service
def get_bandpass(self):
return self.bandpass
def get_modulation(self):
return self.modulation
class AnalogMode(Mode): class AnalogMode(Mode):
pass pass
@ -36,6 +42,14 @@ class DigitalMode(Mode):
super().__init__(modulation, name, bandpass, requirements, service, squelch) super().__init__(modulation, name, bandpass, requirements, service, squelch)
self.underlying = underlying 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): class Modes(object):
mappings = [ mappings = [
@ -54,16 +68,24 @@ class Modes(object):
AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False), AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False),
DigitalMode("bpsk31", "BPSK31", underlying=["usb"]), DigitalMode("bpsk31", "BPSK31", underlying=["usb"]),
DigitalMode("bpsk63", "BPSK63", underlying=["usb"]), DigitalMode("bpsk63", "BPSK63", underlying=["usb"]),
DigitalMode("ft8", "FT8", 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"], requirements=["wsjt-x"], service=True), DigitalMode("ft4", "FT4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True),
DigitalMode("jt65", "JT65", underlying=["usb"], requirements=["wsjt-x"], service=True), DigitalMode("jt65", "JT65", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True),
DigitalMode("jt9", "JT9", underlying=["usb"], requirements=["wsjt-x"], service=True), DigitalMode("jt9", "JT9", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True),
DigitalMode( DigitalMode(
"wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True "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( 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( DigitalMode(
"pocsag", "pocsag",

View File

@ -30,7 +30,7 @@ class PskReporter(object):
sharedInstance = None sharedInstance = None
creationLock = threading.Lock() creationLock = threading.Lock()
interval = 300 interval = 300
supportedModes = ["FT8", "FT4", "JT9", "JT65", "JS8"] supportedModes = ["FT8", "FT4", "JT9", "JT65", "FST4", "FST4W", "JS8"]
@staticmethod @staticmethod
def getSharedInstance(): def getSharedInstance():

View File

@ -155,18 +155,14 @@ class ServiceHandler(SdrSourceEventClient):
) )
else: else:
for group in groups: for group in groups:
frequencies = sorted([f["frequency"] for f in group]) cf = self.get_center_frequency(group)
min = frequencies[0] bw = self.get_bandwidth(group)
max = frequencies[-1]
cf = (min + max) / 2
bw = max - min
logger.debug( logger.debug(
"group center frequency: {0}, bandwidth: {1}".format(cf, bw) "group center frequency: {0}, bandwidth: {1}".format(cf, bw)
) )
resampler_props = PropertyLayer() resampler_props = PropertyLayer()
resampler_props["center_freq"] = cf 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
resampler_props["samp_rate"] = bw + 24000
resampler = Resampler(resampler_props, self.source) resampler = Resampler(resampler_props, self.source)
resampler.start() 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 # resampler goes in after the services since it must not be shutdown as long as the services are still running
self.services.append(resampler) 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): def optimizeResampling(self, freqs, bandwidth):
freqs = sorted(freqs, key=lambda f: f["frequency"]) freqs = sorted(freqs, key=lambda f: f["frequency"])
distances = [ distances = [
@ -203,12 +216,10 @@ class ServiceHandler(SdrSourceEventClient):
previous = split previous = split
groups.append([f for f in freqs if previous < f["frequency"]]) groups.append([f for f in freqs if previous < f["frequency"]])
def get_bandwitdh(group): def get_total_bandwidth(group):
freqs = sorted([f["frequency"] for f in group]) return bandwidth + len(group) * self.get_bandwidth(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)
total_bandwidth = sum([get_bandwitdh(group) for group in groups]) total_bandwidth = sum([get_total_bandwidth(group) for group in groups])
return { return {
"num_splits": num_splits, "num_splits": num_splits,
"total_bandwidth": total_bandwidth, "total_bandwidth": total_bandwidth,
@ -250,16 +261,9 @@ class ServiceHandler(SdrSourceEventClient):
center_freq = source.getProps()["center_freq"] center_freq = source.getProps()["center_freq"]
d.set_offset_freq(frequency - center_freq) d.set_offset_freq(frequency - center_freq)
d.set_center_freq(center_freq) d.set_center_freq(center_freq)
if mode == "packet": modeObject = Modes.findByModulation(mode)
d.set_demodulator("nfm") d.set_demodulator(modeObject.get_modulation())
d.set_bpf(-4000, 4000) d.set_bandpass(modeObject.get_bandpass())
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)
d.set_secondary_demodulator(mode) d.set_secondary_demodulator(mode)
d.set_audio_compression("none") d.set_audio_compression("none")
d.set_samp_rate(source.getProps()["samp_rate"]) d.set_samp_rate(source.getProps()["samp_rate"])

View File

@ -59,10 +59,7 @@ class SdrSource(ABC):
self.activateProfile() self.activateProfile()
self.wireEvents() self.wireEvents()
if "port" in props and props["port"] is not None: self.port = getAvailablePort()
self.port = props["port"]
else:
self.port = getAvailablePort()
self.monitor = None self.monitor = None
self.clients = [] self.clients = []
self.spectrumClients = [] self.spectrumClients = []

View File

@ -23,7 +23,7 @@ class ConnectorSource(SdrSource):
"controlPort": Option("-c"), "controlPort": Option("-c"),
"device": Option("-d"), "device": Option("-d"),
"iqswap": Flag("-i"), "iqswap": Flag("-i"),
"rtltcp_compat": Flag("-r"), "rtltcp_compat": Option("-r"),
"ppm": Option("-P"), "ppm": Option("-P"),
"rf_gain": Option("-g"), "rf_gain": Option("-g"),
} }

15
owrx/source/eb200.py Normal file
View File

@ -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(),
})
)

6
owrx/source/sddc.py Normal file
View File

@ -0,0 +1,6 @@
from owrx.source.connector import ConnectorSource
class SddcSource(ConnectorSource):
def getCommandMapper(self):
return super().getCommandMapper().setBase("sddc_connector")

View File

@ -85,8 +85,56 @@ class Ft4Profile(WsjtProfile):
return ["jt9", "--ft4", "-d", str(self.decoding_depth("ft4")), file] 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): class WsjtParser(Parser):
modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"} modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4", "`": "FST4"}
def parse(self, messages): def parse(self, messages):
for data in messages: for data in messages:
@ -115,7 +163,7 @@ class WsjtParser(Parser):
PskReporter.getSharedInstance().spot(out) PskReporter.getSharedInstance().spot(out)
self.handler.write_wsjt_message(out) self.handler.write_wsjt_message(out)
except ValueError: except (ValueError, IndexError):
logger.exception("error while parsing wsjt message") logger.exception("error while parsing wsjt message")
def pushDecode(self, mode): def pushDecode(self, mode):
@ -139,6 +187,8 @@ class WsjtParser(Parser):
class Decoder(ABC): 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): def parse_timestamp(self, instring, dateformat):
ts = datetime.strptime(instring, dateformat) ts = datetime.strptime(instring, dateformat)
return int( return int(
@ -149,23 +199,36 @@ class Decoder(ABC):
def parse(self, msg, dial_freq): def parse(self, msg, dial_freq):
pass 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): class Jt9Decoder(Decoder):
locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$")
def parse(self, msg, dial_freq): def parse(self, msg, dial_freq):
# ft8 sample # ft8 sample
# '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66'
# jt65 sample # jt65 sample
# '2352 -7 0.4 1801 # R0WAS R2ABM KO85' # '2352 -7 0.4 1801 # R0WAS R2ABM KO85'
# '0003 -4 0.4 1762 # CQ R2ABM KO85' # '0003 -4 0.4 1762 # CQ R2ABM KO85'
# fst4 sample
# '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV'
modes = list(WsjtParser.modes.keys()) modes = list(WsjtParser.modes.keys())
if msg[19] in modes: if msg[19] in modes:
dateformat = "%H%M" dateformat = "%H%M"
else: else:
dateformat = "%H%M%S" dateformat = "%H%M%S"
timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat) try:
msg = msg[len(dateformat) + 1 :] timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat)
except ValueError:
timestamp = None
msg = msg[len(dateformat) + 1:]
modeChar = msg[14:15] modeChar = msg[14:15]
mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown"
wsjt_msg = msg[17:53].strip() wsjt_msg = msg[17:53].strip()
@ -181,16 +244,6 @@ class Jt9Decoder(Decoder):
result.update(self.parseMessage(wsjt_msg)) result.update(self.parseMessage(wsjt_msg))
return result 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): class WsprDecoder(Decoder):
wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")