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

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",
"lower_bound": 1810000,
@ -9,7 +27,9 @@
"wspr": 1836600,
"jt65": 1838000,
"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:
# "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

View File

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

14
debian/changelog vendored
View File

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

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

@ -76,4 +76,4 @@ class Option(CommandMapping):
class Argument(CommandMapping):
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"],
"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")

View File

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

View File

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

View File

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

View File

@ -59,9 +59,6 @@ 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.monitor = None
self.clients = []

View File

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

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]
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"
try:
timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat)
msg = msg[len(dateformat) + 1 :]
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]+)")