22 Commits

Author SHA1 Message Date
258aebd0c3 correctly handle bookmarks with underlying mode in receiver 2023-02-28 17:07:13 +01:00
a54a5fd560 allow underlying mode to be specified in bandplan 2023-02-28 15:30:31 +01:00
cb5b2e64af change chunking to work with actual byte-sizes 2023-02-22 17:23:11 +01:00
685b9970d2 switch frequency field to 5 bytes to support QO-100 2023-02-22 16:19:18 +01:00
c385a8f6b1 update changelogs 2023-02-22 14:13:01 +01:00
aa60b9d4a7 add msk144decoder to docker build 2023-02-21 19:50:44 +01:00
bff09e3363 add msk144decoder to recommended packages 2023-02-21 17:42:33 +01:00
774f8bd91a remove debugging messages 2023-02-21 17:41:55 +01:00
b1684908a4 remove todo 2023-02-19 16:18:49 +01:00
ed76fd7606 add MSK144 service demodulator 2023-02-19 16:18:08 +01:00
7b3f212ccb improve error handling during service initialization 2023-02-19 16:14:08 +01:00
216a3db45d add MSK144 to list of pskreporter modes 2023-02-16 19:47:46 +01:00
c16de474c6 route msk144 data to the wsjt message panel 2023-02-14 18:45:51 +01:00
afcd8277d1 add MSK144 parsing 2023-02-14 18:36:17 +01:00
525b70d495 add msk144 frequencies 2023-02-14 15:40:20 +01:00
f58023f3e5 add msk144demodulator chain 2023-02-14 15:39:59 +01:00
252edb7a5a add feature detection 2023-02-14 15:38:33 +01:00
2993cc4279 update wsjt-x homepage url 2023-02-14 15:37:37 +01:00
cc4f3c6c1d correctly commit patch? 2023-01-28 19:23:46 +01:00
0de597481c update to wsjt-x 2.6.1 2023-01-28 19:13:10 +01:00
2342bb5d29 update WSJT-X download location 2023-01-28 17:36:45 +01:00
b1ac8caf9b replace with a more robust state engine implementation 2023-01-28 16:37:32 +01:00
23 changed files with 608 additions and 399 deletions

View File

@ -1,5 +1,6 @@
**unreleased** **unreleased**
- SDR device log messages are now available in the web configuration to simplify troubleshooting - SDR device log messages are now available in the web configuration to simplify troubleshooting
- Added support for the MSK144 digimode
**1.2.1** **1.2.1**
- FifiSDR support fixed (pipeline formats now line up correctly) - FifiSDR support fixed (pipeline formats now line up correctly)

View File

@ -14,7 +14,7 @@ It has the following features:
- supports a wide range of [SDR hardware](https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices) - supports a wide range of [SDR hardware](https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices)
- Multiple SDR devices can be used simultaneously - Multiple SDR devices can be used simultaneously
- [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag, D-Star, NXDN) - [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag, D-Star, NXDN)
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9, FST4, - [wsjt-x](https://wsjt.sourceforge.io/) based demodulators (FT8, FT4, WSPR, JT65, JT9, FST4,
FST4W) FST4W)
- [direwolf](https://github.com/wb2osz/direwolf) based demodulation of APRS packets - [direwolf](https://github.com/wb2osz/direwolf) based demodulation of APRS packets
- [JS8Call](http://js8call.com/) support - [JS8Call](http://js8call.com/) support

View File

@ -177,7 +177,8 @@
"jt9": 50312000, "jt9": 50312000,
"ft4": 50318000, "ft4": 50318000,
"js8": 50318000, "js8": 50318000,
"q65": [50211000, 50275000] "q65": [50211000, 50275000],
"msk144": 50260000
}, },
"tags": ["hamradio"] "tags": ["hamradio"]
}, },
@ -186,7 +187,8 @@
"lower_bound": 70150000, "lower_bound": 70150000,
"upper_bound": 70200000, "upper_bound": 70200000,
"frequencies": { "frequencies": {
"wspr": 70091000 "wspr": 70091000,
"msk144": 70230000
}, },
"tags": ["hamradio"] "tags": ["hamradio"]
}, },
@ -200,7 +202,8 @@
"ft4": 144170000, "ft4": 144170000,
"jt65": 144120000, "jt65": 144120000,
"packet": 144800000, "packet": 144800000,
"q65": 144116000 "q65": 144116000,
"msk144": 144360000
}, },
"tags": ["hamradio"] "tags": ["hamradio"]
}, },
@ -210,7 +213,8 @@
"upper_bound": 440000000, "upper_bound": 440000000,
"frequencies": { "frequencies": {
"pocsag": 439987500, "pocsag": 439987500,
"q65": 432065000 "q65": 432065000,
"msk144": 432360000
}, },
"tags": ["hamradio"] "tags": ["hamradio"]
}, },

View File

@ -1,4 +1,5 @@
from csdr.chain.demodulator import ServiceDemodulator, SecondaryDemodulator, DialFrequencyReceiver, SecondarySelectorChain from csdr.chain.demodulator import ServiceDemodulator, SecondaryDemodulator, DialFrequencyReceiver, SecondarySelectorChain
from csdr.module.msk144 import Msk144Module, ParserAdapter
from owrx.audio.chopper import AudioChopper, AudioChopperParser from owrx.audio.chopper import AudioChopper, AudioChopperParser
from owrx.aprs.kiss import KissDeframer from owrx.aprs.kiss import KissDeframer
from owrx.aprs import Ax25Parser, AprsParser from owrx.aprs import Ax25Parser, AprsParser
@ -20,6 +21,23 @@ class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver):
self.chopper.setDialFrequency(frequency) self.chopper.setDialFrequency(frequency)
class Msk144Demodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self):
self.parser = ParserAdapter()
workers = [
Convert(Format.FLOAT, Format.SHORT),
Msk144Module(),
self.parser,
]
super().__init__(workers)
def getFixedAudioRate(self) -> int:
return 12000
def setDialFrequency(self, frequency: int) -> None:
self.parser.setDialFrequency(frequency)
class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver): class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self, service: bool = False): def __init__(self, service: bool = False):
self.parser = AprsParser() self.parser = AprsParser()

View File

@ -126,7 +126,7 @@ class PopenModule(AutoStartModule, metaclass=ABCMeta):
# resume in case the reader has been stop()ed before # resume in case the reader has been stop()ed before
self.reader.resume() self.reader.resume()
Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start() 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() Thread(target=self.pump(partial(self.process.stdout.read1, 1024), self.writer.write)).start()
def stop(self): def stop(self):
if self.process is not None: if self.process is not None:

57
csdr/module/msk144.py Normal file
View File

@ -0,0 +1,57 @@
from pycsdr.types import Format
from csdr.module import PopenModule, ThreadModule
from owrx.wsjt import WsjtParser, Msk144Profile
import pickle
import logging
logger = logging.getLogger(__name__)
class Msk144Module(PopenModule):
def getCommand(self):
return ["msk144decoder"]
def getInputFormat(self) -> Format:
return Format.SHORT
def getOutputFormat(self) -> Format:
return Format.CHAR
class ParserAdapter(ThreadModule):
def __init__(self):
self.retained = bytes()
self.parser = WsjtParser()
self.dialFrequency = 0
super().__init__()
def run(self):
profile = Msk144Profile()
while self.doRun:
data = self.reader.read()
if data is None:
self.doRun = False
else:
self.retained += data
lines = self.retained.split(b"\n")
# keep the last line
# this should either be empty if the last char was \n
# or an incomplete line if the read returned early
self.retained = lines[-1]
# parse all completed lines
for line in lines[0:-1]:
# actual messages from msk144decoder should start with "*** "
if line[0:4] == b"*** ":
self.writer.write(pickle.dumps(self.parser.parse(profile, self.dialFrequency, line[4:])))
def getInputFormat(self) -> Format:
return Format.CHAR
def getOutputFormat(self) -> Format:
return Format.CHAR
def setDialFrequency(self, frequency: int) -> None:
self.dialFrequency = frequency

1
debian/changelog vendored
View File

@ -1,6 +1,7 @@
openwebrx (1.3.0) UNRELEASED; urgency=low openwebrx (1.3.0) UNRELEASED; urgency=low
* SDR device log messages are now available in the web configuration to * SDR device log messages are now available in the web configuration to
simplify troubleshooting simplify troubleshooting
* Added support for the MSK144 digimode
-- Jakob Ketterl <jakob.ketterl@gmx.de> Fri, 30 Sep 2022 16:47:00 +0000 -- Jakob Ketterl <jakob.ketterl@gmx.de> Fri, 30 Sep 2022 16:47:00 +0000

2
debian/control vendored
View File

@ -11,6 +11,6 @@ Vcs-Git: https://github.com/jketterl/openwebrx.git
Package: openwebrx Package: openwebrx
Architecture: all Architecture: all
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, owrx-connector (>= 0.5), soapysdr-tools, python3-csdr (>= 0.18), ${python3:Depends}, ${misc:Depends} 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, js8call, runds-connector (>= 0.2), hpsdrconnector, aprs-symbols, m17-demod, js8call, python3-js8py (>= 0.2), nmux (>= 0.18), codecserver (>= 0.1) Recommends: python3-digiham (>= 0.6), direwolf (>= 1.4), wsjtx, js8call, runds-connector (>= 0.2), hpsdrconnector, aprs-symbols, m17-demod, js8call, python3-js8py (>= 0.2), nmux (>= 0.18), codecserver (>= 0.1), msk144decoder
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

@ -1,6 +1,6 @@
diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
--- wsjtx-orig/CMakeLists.txt 2021-11-02 16:34:09.361811689 +0100 --- wsjtx-orig/CMakeLists.txt 2023-01-28 17:43:05.586124507 +0100
+++ wsjtx/CMakeLists.txt 2021-11-02 16:38:36.696088115 +0100 +++ wsjtx/CMakeLists.txt 2023-01-28 17:56:07.108634912 +0100
@@ -122,7 +122,7 @@ @@ -122,7 +122,7 @@
option (WSJT_QDEBUG_TO_FILE "Redirect Qt debuging messages to a trace file.") 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) option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON)
@ -10,7 +10,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
option (WSJT_RIG_NONE_CAN_SPLIT "Allow split operation with \"None\" as rig.") option (WSJT_RIG_NONE_CAN_SPLIT "Allow split operation with \"None\" as rig.")
option (WSJT_TRACE_UDP "Debugging option that turns on UDP message protocol diagnostics.") option (WSJT_TRACE_UDP "Debugging option that turns on UDP message protocol diagnostics.")
option (WSJT_BUILD_UTILS "Build simulators and code demonstrators." ON) option (WSJT_BUILD_UTILS "Build simulators and code demonstrators." ON)
@@ -169,76 +169,7 @@ @@ -170,77 +170,7 @@
) )
set (wsjt_qt_CXXSRCS set (wsjt_qt_CXXSRCS
@ -34,6 +34,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
- widgets/FrequencyDeltaLineEdit.cpp - widgets/FrequencyDeltaLineEdit.cpp
- item_delegates/CandidateKeyFilter.cpp - item_delegates/CandidateKeyFilter.cpp
- item_delegates/ForeignKeyDelegate.cpp - item_delegates/ForeignKeyDelegate.cpp
- item_delegates/MessageItemDelegate.cpp
- validators/LiveFrequencyValidator.cpp - validators/LiveFrequencyValidator.cpp
- GetUserId.cpp - GetUserId.cpp
- Audio/AudioDevice.cpp - Audio/AudioDevice.cpp
@ -87,7 +88,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
) )
set (wsjt_qtmm_CXXSRCS set (wsjt_qtmm_CXXSRCS
@@ -1079,9 +1010,6 @@ @@ -1089,9 +1019,6 @@
if (WSJT_GENERATE_DOCS) if (WSJT_GENERATE_DOCS)
add_subdirectory (doc) add_subdirectory (doc)
endif (WSJT_GENERATE_DOCS) endif (WSJT_GENERATE_DOCS)
@ -97,7 +98,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# build a library of package functionality (without and optionally with OpenMP support) # build a library of package functionality (without and optionally with OpenMP support)
add_library (wsjt_cxx STATIC ${wsjt_CSRCS} ${wsjt_CXXSRCS}) add_library (wsjt_cxx STATIC ${wsjt_CSRCS} ${wsjt_CXXSRCS})
@@ -1340,10 +1268,7 @@ @@ -1357,10 +1284,7 @@
add_library (wsjt_qt STATIC ${wsjt_qt_CXXSRCS} ${wsjt_qt_GENUISRCS} ${GENAXSRCS}) add_library (wsjt_qt STATIC ${wsjt_qt_CXXSRCS} ${wsjt_qt_GENUISRCS} ${GENAXSRCS})
# set wsjtx_udp exports to static variants # set wsjtx_udp exports to static variants
target_compile_definitions (wsjt_qt PUBLIC UDP_STATIC_DEFINE) target_compile_definitions (wsjt_qt PUBLIC UDP_STATIC_DEFINE)
@ -109,7 +110,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# build a library of package Qt functionality used in Fortran utilities # build a library of package Qt functionality used in Fortran utilities
add_library (fort_qt STATIC ${fort_qt_CXXSRCS}) add_library (fort_qt STATIC ${fort_qt_CXXSRCS})
@@ -1408,60 +1333,6 @@ @@ -1425,90 +1349,6 @@
add_subdirectory (map65) add_subdirectory (map65)
endif () endif ()
@ -146,7 +147,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
- ) - )
- -
-target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS}) -target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS})
-if (APPLE) -if ((NOT ${OPENMP_FOUND}) OR APPLE)
- target_link_libraries (wsjtx wsjt_fort) - target_link_libraries (wsjtx wsjt_fort)
-else () -else ()
- target_link_libraries (wsjtx wsjt_fort_omp) - target_link_libraries (wsjtx wsjt_fort_omp)
@ -167,10 +168,40 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
-endif () -endif ()
-target_link_libraries (wsjtx Qt5::SerialPort wsjt_cxx wsjt_qt wsjt_qtmm ${FFTW3_LIBRARIES} ${LIBM_LIBRARIES}) -target_link_libraries (wsjtx Qt5::SerialPort wsjt_cxx wsjt_qt wsjt_qtmm ${FFTW3_LIBRARIES} ${LIBM_LIBRARIES})
- -
# make a library for WSJT-X UDP servers -# make a library for WSJT-X UDP servers
# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS}) -# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS})
add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS}) -add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS})
@@ -1501,47 +1372,9 @@ -#target_include_directories (wsjtx_udp
-# INTERFACE
-# $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/wsjtx>
-# )
-target_include_directories (wsjtx_udp-static
- INTERFACE
- $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/wsjtx>
- )
-#set_target_properties (wsjtx_udp PROPERTIES
-# PUBLIC_HEADER "${UDP_library_HEADERS}"
-# )
-set_target_properties (wsjtx_udp-static PROPERTIES
- OUTPUT_NAME wsjtx_udp
- )
-target_compile_definitions (wsjtx_udp-static PUBLIC UDP_STATIC_DEFINE)
-target_link_libraries (wsjtx_udp-static Qt5::Network Qt5::Gui)
-generate_export_header (wsjtx_udp-static BASE_NAME udp)
-
-generate_version_info (udp_daemon_VERSION_RESOURCES
- NAME udp_daemon
- BUNDLE ${PROJECT_BUNDLE_NAME}
- ICON ${WSJTX_ICON_FILE}
- FILE_DESCRIPTION "Example WSJT-X UDP Message Protocol daemon"
- )
-add_executable (udp_daemon UDPExamples/UDPDaemon.cpp ${udp_daemon_VERSION_RESOURCES})
-target_link_libraries (udp_daemon wsjtx_udp-static)
-
generate_version_info (wsjtx_app_version_VERSION_RESOURCES
NAME wsjtx_app_version
BUNDLE ${PROJECT_BUNDLE_NAME}
@@ -1518,47 +1358,9 @@
add_executable (wsjtx_app_version AppVersion/AppVersion.cpp ${wsjtx_app_version_VERSION_RESOURCES}) add_executable (wsjtx_app_version AppVersion/AppVersion.cpp ${wsjtx_app_version_VERSION_RESOURCES})
target_link_libraries (wsjtx_app_version wsjt_qt) target_link_libraries (wsjtx_app_version wsjt_qt)
@ -218,7 +249,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# install (TARGETS wsjtx_udp EXPORT udp # install (TARGETS wsjtx_udp EXPORT udp
# RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
@@ -1560,12 +1393,7 @@ @@ -1577,12 +1379,7 @@
# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx # DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx
# ) # )
@ -232,7 +263,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
) )
@@ -1578,38 +1406,6 @@ @@ -1595,38 +1392,6 @@
) )
endif(WSJT_BUILD_UTILS) endif(WSJT_BUILD_UTILS)
@ -271,7 +302,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
install (FILES install (FILES
cty.dat cty.dat
cty.dat_copyright.txt cty.dat_copyright.txt
@@ -1618,13 +1414,6 @@ @@ -1635,13 +1400,6 @@
#COMPONENT runtime #COMPONENT runtime
) )
@ -285,7 +316,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# #
# Mac installer files # Mac installer files
# #
@@ -1676,22 +1465,6 @@ @@ -1693,22 +1451,6 @@
"${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h" "${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
) )

View File

@ -7,6 +7,9 @@ function cmakebuild() {
if [[ ! -z "${2:-}" ]]; then if [[ ! -z "${2:-}" ]]; then
git checkout $2 git checkout $2
fi fi
if [[ -f ".gitmodules" ]]; then
git submodule update --init
fi
mkdir build mkdir build
cd build cd build
cmake ${CMAKE_ARGS:-} .. cmake ${CMAKE_ARGS:-} ..
@ -18,8 +21,8 @@ function cmakebuild() {
cd /tmp cd /tmp
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" 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 libcurl4"
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" 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 libcurl4-openssl-dev"
apt-get update apt-get update
apt-get -y install auto-apt-proxy apt-get -y install auto-apt-proxy
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
@ -51,15 +54,19 @@ rm /js8call-hamlib.patch
cmakebuild ${JS8CALL_DIR} cmakebuild ${JS8CALL_DIR}
rm ${JS8CALL_TGZ} rm ${JS8CALL_TGZ}
WSJT_DIR=wsjtx-2.5.4 WSJT_DIR=wsjtx-2.6.1
WSJT_TGZ=${WSJT_DIR}.tgz WSJT_TGZ=${WSJT_DIR}.tgz
wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ} wget https://downloads.sourceforge.net/project/wsjt/${WSJT_DIR}/${WSJT_TGZ}
tar xfz ${WSJT_TGZ} tar xfz ${WSJT_TGZ}
patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch
mv /wsjtx.patch ${WSJT_DIR} mv /wsjtx.patch ${WSJT_DIR}
cmakebuild ${WSJT_DIR} cmakebuild ${WSJT_DIR}
rm ${WSJT_TGZ} rm ${WSJT_TGZ}
git clone https://github.com/alexander-sholohov/msk144decoder.git
# latest from main as of 2023-02-21
MAKEFLAGS="" cmakebuild msk144decoder fe2991681e455636e258e83c29fd4b2a72d16095
git clone --depth 1 -b 1.6 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.

View File

@ -1265,6 +1265,7 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
@ -1275,7 +1276,8 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-select-channel #openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-select-channel
{ {
display: none; display: none;
} }
@ -1290,7 +1292,8 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-canvas-container #openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-canvas-container
{ {
height: 200px; height: 200px;
margin: -10px; margin: -10px;

View File

@ -331,7 +331,9 @@ ImaAdpcmCodec.prototype.reset = function() {
this.synchronized = 0; this.synchronized = 0;
this.syncWord = "SYNC"; this.syncWord = "SYNC";
this.syncCounter = 0; this.syncCounter = 0;
this.skip = 0; this.phase = 0;
this.syncBuffer = new Uint8Array(4);
this.syncBufferIndex = 0;
}; };
ImaAdpcmCodec.imaIndexTable = [ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 ]; ImaAdpcmCodec.imaIndexTable = [ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 ];
@ -359,38 +361,45 @@ ImaAdpcmCodec.prototype.decode = function(data) {
ImaAdpcmCodec.prototype.decodeWithSync = function(data) { ImaAdpcmCodec.prototype.decodeWithSync = function(data) {
var output = new Int16Array(data.length * 2); var output = new Int16Array(data.length * 2);
var index = this.skip;
var oi = 0; var oi = 0;
while (index < data.length) { for (var index = 0; index < data.length; index++) {
while (this.synchronized < 4 && index < data.length) { switch (this.phase) {
if (data[index] === this.syncWord.charCodeAt(this.synchronized)) { case 0:
this.synchronized++; // search for sync word
} else { if (data[index] !== this.syncWord.charCodeAt(this.synchronized++)) {
this.synchronized = 0; // reset if data is unexpected
} this.synchronized = 0;
index++; }
if (this.synchronized === 4) { // if sync word has been found pass on to next phase
if (index + 4 < data.length) { if (this.synchronized === 4) {
var syncData = new Int16Array(data.buffer.slice(index, index + 4)); this.syncBufferIndex = 0;
this.phase = 1;
}
break;
case 1:
// read codec runtime data from stream
this.syncBuffer[this.syncBufferIndex++] = data[index];
// if data is complete, apply and pass on to next phase
if (this.syncBufferIndex === 4) {
var syncData = new Int16Array(this.syncBuffer.buffer);
this.stepIndex = syncData[0]; this.stepIndex = syncData[0];
this.predictor = syncData[1]; this.predictor = syncData[1];
this.syncCounter = 1000;
this.phase = 2;
} }
this.syncCounter = 1000;
index += 4;
break; break;
} case 2:
} // decode actual audio data
while (index < data.length) { output[oi++] = this.decodeNibble(data[index] & 0x0F);
if (this.syncCounter-- < 0) { output[oi++] = this.decodeNibble(data[index] >> 4);
this.synchronized = 0; // if the next sync keyword is due, reset and return to phase 0
if (this.syncCounter-- === 0) {
this.synchronized = 0;
this.phase = 0;
}
break; break;
}
output[oi++] = this.decodeNibble(data[index] & 0x0F);
output[oi++] = this.decodeNibble(data[index] >> 4);
index++;
} }
} }
this.skip = index - data.length;
return output.slice(0, oi); return output.slice(0, oi);
}; };

View File

@ -11,7 +11,7 @@ function BookmarkBar() {
if (!b || !b.frequency || !b.modulation) return; if (!b || !b.frequency || !b.modulation) return;
me.getDemodulator().set_offset_frequency(b.frequency - center_freq); me.getDemodulator().set_offset_frequency(b.frequency - center_freq);
if (b.modulation) { if (b.modulation) {
me.getDemodulatorPanel().setMode(b.modulation); me.getDemodulatorPanel().setMode(b.modulation, b.underlying);
} }
$bookmark.addClass('selected'); $bookmark.addClass('selected');
}); });

View File

@ -18,7 +18,12 @@ function DemodulatorPanel(el) {
el.on('click', '.openwebrx-demodulator-button', function() { el.on('click', '.openwebrx-demodulator-button', function() {
var modulation = $(this).data('modulation'); var modulation = $(this).data('modulation');
if (modulation) { if (modulation) {
self.setMode(modulation); if (self.mode && self.mode.type === 'digimode' && self.mode.underlying.indexOf(modulation) >= 0) {
// keep the mode, just switch underlying modulation
self.setMode(self.mode.modulation, modulation)
} else {
self.setMode(modulation);
}
} else { } else {
self.disableDigiMode(); self.disableDigiMode();
} }
@ -80,12 +85,13 @@ DemodulatorPanel.prototype.render = function() {
this.el.find(".openwebrx-modes").html(html); this.el.find(".openwebrx-modes").html(html);
}; };
DemodulatorPanel.prototype.setMode = function(requestedModulation) { DemodulatorPanel.prototype.setMode = function(requestedModulation, underlyingModulation) {
var mode = Modes.findByModulation(requestedModulation); var mode = Modes.findByModulation(requestedModulation);
if (!mode) { if (!mode) {
return; return;
} }
if (this.mode === mode) {
if (this.mode === mode && this.underlyingModulation === underlyingModulation) {
return; return;
} }
if (!mode.isAvailable()) { if (!mode.isAvailable()) {
@ -93,16 +99,15 @@ DemodulatorPanel.prototype.setMode = function(requestedModulation) {
return; return;
} }
var modulation;
if (mode.type === 'digimode') { if (mode.type === 'digimode') {
modulation = mode.underlying[0]; if (underlyingModulation) {
} else { modulation = underlyingModulation
if (this.mode && this.mode.type === 'digimode' && this.mode.underlying.indexOf(requestedModulation) >= 0) {
// keep the mode, just switch underlying modulation
mode = this.mode;
modulation = requestedModulation;
} else { } else {
modulation = mode.modulation; modulation = mode.underlying[0];
} }
} else {
modulation = mode.modulation;
} }
var current = this.collectParams(); var current = this.collectParams();
@ -142,6 +147,7 @@ DemodulatorPanel.prototype.setMode = function(requestedModulation) {
this.demodulator.start(); this.demodulator.start();
this.mode = mode; this.mode = mode;
this.underlyingModulation = modulation;
this.updateButtons(); this.updateButtons();
this.updatePanels(); this.updatePanels();
@ -149,8 +155,6 @@ DemodulatorPanel.prototype.setMode = function(requestedModulation) {
}; };
DemodulatorPanel.prototype.disableDigiMode = function() { DemodulatorPanel.prototype.disableDigiMode = function() {
// just a little trick to get out of the digimode
delete this.mode;
this.setMode(this.getDemodulator().get_modulation()); this.setMode(this.getDemodulator().get_modulation());
}; };
@ -158,8 +162,8 @@ DemodulatorPanel.prototype.updatePanels = function() {
var modulation = this.getDemodulator().get_secondary_demod(); var modulation = this.getDemodulator().get_secondary_demod();
$('#openwebrx-panel-digimodes').attr('data-mode', modulation); $('#openwebrx-panel-digimodes').attr('data-mode', modulation);
toggle_panel("openwebrx-panel-digimodes", !!modulation); toggle_panel("openwebrx-panel-digimodes", !!modulation);
toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w', "q65"].indexOf(modulation) >= 0); toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w', "q65", "msk144"].indexOf(modulation) >= 0);
toggle_panel("openwebrx-panel-js8-message", modulation == "js8"); toggle_panel("openwebrx-panel-js8-message", modulation === "js8");
toggle_panel("openwebrx-panel-packet-message", modulation === "packet"); toggle_panel("openwebrx-panel-packet-message", modulation === "packet");
toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag"); toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag");
@ -203,7 +207,11 @@ DemodulatorPanel.prototype.stopDemodulator = function() {
} }
DemodulatorPanel.prototype._apply = function(params) { DemodulatorPanel.prototype._apply = function(params) {
this.setMode(params.mod); if (params.secondary_mod) {
this.setMode(params.secondary_mod, params.mod)
} else {
this.setMode(params.mod);
}
this.getDemodulator().set_offset_frequency(params.offset_frequency); this.getDemodulator().set_offset_frequency(params.offset_frequency);
this.getDemodulator().setSquelch(params.squelch_level); this.getDemodulator().setSquelch(params.squelch_level);
this.updateButtons(); this.updateButtons();
@ -223,8 +231,9 @@ DemodulatorPanel.prototype.onHashChange = function() {
DemodulatorPanel.prototype.transformHashParams = function(params) { DemodulatorPanel.prototype.transformHashParams = function(params) {
var ret = { var ret = {
mod: params.secondary_mod || params.mod mod: params.mod
}; };
if (typeof(params.secondary_mod) !== 'undefined') ret.secondary_mod = params.secondary_mod;
if (typeof(params.offset_frequency) !== 'undefined') ret.offset_frequency = params.offset_frequency; if (typeof(params.offset_frequency) !== 'undefined') ret.offset_frequency = params.offset_frequency;
if (typeof(params.sql) !== 'undefined') ret.squelch_level = parseInt(params.sql); if (typeof(params.sql) !== 'undefined') ret.squelch_level = parseInt(params.sql);
return ret; return ret;
@ -329,7 +338,7 @@ DemodulatorPanel.prototype.updateHash = function() {
freq: demod.get_offset_frequency() + self.center_freq, freq: demod.get_offset_frequency() + self.center_freq,
mod: demod.get_modulation(), mod: demod.get_modulation(),
secondary_mod: demod.get_secondary_demod(), secondary_mod: demod.get_secondary_demod(),
sql: demod.getSquelch(), sql: demod.getSquelch()
}, function(value, key){ }, function(value, key){
if (typeof(value) === 'undefined' || value === false) return undefined; if (typeof(value) === 'undefined' || value === false) return undefined;
return key + '=' + value; return key + '=' + value;

View File

@ -50,7 +50,7 @@ MessagePanel.prototype.initClearButton = function() {
function WsjtMessagePanel(el) { function WsjtMessagePanel(el) {
MessagePanel.call(this, el); MessagePanel.call(this, el);
this.initClearTimer(); this.initClearTimer();
this.qsoModes = ['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65']; this.qsoModes = ['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65', 'MSK144'];
this.beaconModes = ['WSPR', 'FST4W']; this.beaconModes = ['WSPR', 'FST4W'];
this.modes = [].concat(this.qsoModes, this.beaconModes); this.modes = [].concat(this.qsoModes, this.beaconModes);
} }

View File

@ -831,7 +831,8 @@ function on_ws_recv(evt) {
return { return {
name: d['mode'].toUpperCase(), name: d['mode'].toUpperCase(),
modulation: d['mode'], modulation: d['mode'],
frequency: d['frequency'] frequency: d['frequency'],
underlying: d['underlying']
}; };
}); });
bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies'); bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies');

View File

@ -1,4 +1,4 @@
from owrx.modes import Modes from owrx.modes import Modes, DigitalMode
from datetime import datetime, timezone from datetime import datetime, timezone
import json import json
import os import os
@ -9,14 +9,14 @@ logger = logging.getLogger(__name__)
class Band(object): class Band(object):
def __init__(self, dict): def __init__(self, b_dict):
self.name = dict["name"] self.name = b_dict["name"]
self.lower_bound = dict["lower_bound"] self.lower_bound = b_dict["lower_bound"]
self.upper_bound = dict["upper_bound"] self.upper_bound = b_dict["upper_bound"]
self.frequencies = [] self.frequencies = []
if "frequencies" in dict: if "frequencies" in b_dict:
availableModes = [mode.modulation for mode in Modes.getAvailableModes()] availableModes = [mode.modulation for mode in Modes.getAvailableModes()]
for (mode, freqs) in dict["frequencies"].items(): for (mode, freqs) in b_dict["frequencies"].items():
if mode not in availableModes: if mode not in availableModes:
logger.info( logger.info(
'Modulation "{mode}" is not available, bandplan bookmark will not be displayed'.format( 'Modulation "{mode}" is not available, bandplan bookmark will not be displayed'.format(
@ -27,14 +27,30 @@ class Band(object):
if not isinstance(freqs, list): if not isinstance(freqs, list):
freqs = [freqs] freqs = [freqs]
for f in freqs: for f in freqs:
if not self.inBand(f): f_dict = {"frequency": f} if not isinstance(f, dict) else f
f_dict["mode"] = mode
if not self.inBand(f_dict["frequency"]):
logger.warning( logger.warning(
"Frequency for {mode} on {band} is not within band limits: {frequency}".format( "Frequency for {mode} on {band} is not within band limits: {frequency}".format(
mode=mode, frequency=f, band=self.name mode=mode, frequency=f_dict["frequency"], band=self.name
) )
) )
continue continue
self.frequencies.append({"mode": mode, "frequency": f})
if "underlying" in f_dict:
m = Modes.findByModulation(mode)
if not isinstance(m, DigitalMode):
logger.warning("%s is not a digital mode, cannot be used with \"underlying\" config", mode)
continue
if f_dict["underlying"] not in m.underlying:
logger.warning(
"%s is not a valid underlying mode for %s; skipping",
f_dict["underlying"],
mode
)
self.frequencies.append(f_dict)
def inBand(self, freq): def inBand(self, freq):
return self.lower_bound <= freq <= self.upper_bound return self.lower_bound <= freq <= self.upper_bound

View File

@ -581,6 +581,9 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
from csdr.chain.digimodes import AudioChopperDemodulator from csdr.chain.digimodes import AudioChopperDemodulator
from owrx.wsjt import WsjtParser from owrx.wsjt import WsjtParser
return AudioChopperDemodulator(mod, WsjtParser()) return AudioChopperDemodulator(mod, WsjtParser())
elif mod == "msk144":
from csdr.chain.digimodes import Msk144Demodulator
return Msk144Demodulator()
elif mod == "js8": elif mod == "js8":
from csdr.chain.digimodes import AudioChopperDemodulator from csdr.chain.digimodes import AudioChopperDemodulator
from owrx.js8 import Js8Parser from owrx.js8 import Js8Parser

View File

@ -80,6 +80,7 @@ class FeatureDetector(object):
"wsjt-x": ["wsjtx"], "wsjt-x": ["wsjtx"],
"wsjt-x-2-3": ["wsjtx_2_3"], "wsjt-x-2-3": ["wsjtx_2_3"],
"wsjt-x-2-4": ["wsjtx_2_4"], "wsjt-x-2-4": ["wsjtx_2_4"],
"msk144": ["msk144decoder"],
"packet": ["direwolf"], "packet": ["direwolf"],
"pocsag": ["digiham"], "pocsag": ["digiham"],
"js8call": ["js8", "js8py"], "js8call": ["js8", "js8py"],
@ -428,7 +429,7 @@ class FeatureDetector(object):
def has_wsjtx(self): def has_wsjtx(self):
""" """
To decode FT8 and other digimodes, you need to install the WSJT-X software suite. Please check the To decode FT8 and other digimodes, you need to install the WSJT-X software suite. Please check the
[WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions [WSJT-X homepage](https://wsjt.sourceforge.io/) for ready-made packages or instructions
on how to build from source. on how to build from source.
""" """
return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True) return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True)
@ -459,6 +460,13 @@ class FeatureDetector(object):
""" """
return self.has_wsjtx() and self._has_wsjtx_version(LooseVersion("2.4")) return self.has_wsjtx() and self._has_wsjtx_version(LooseVersion("2.4"))
def has_msk144decoder(self):
"""
To decode the MSK144 digimode please install the "msk144decoder". See the
[project page](https://github.com/alexander-sholohov/msk144decoder) for more details.
"""
return self.command_is_runnable("msk144decoder")
def has_js8(self): def has_js8(self):
""" """
To decode JS8, you will need to install [JS8Call](http://js8call.com/) To decode JS8, you will need to install [JS8Call](http://js8call.com/)

View File

@ -55,6 +55,13 @@ class DigitalMode(Mode):
def get_modulation(self): def get_modulation(self):
return self.get_underlying_mode().get_modulation() return self.get_underlying_mode().get_modulation()
def for_underlying(self, underlying: str):
if underlying not in self.underlying:
raise ValueError("{} is not a valid underlying mode for {}".format(underlying, self.modulation))
return DigitalMode(
self.modulation, self.name, [underlying], self.bandpass, self.requirements, self.service, self.squelch
)
class AudioChopperMode(DigitalMode, metaclass=ABCMeta): class AudioChopperMode(DigitalMode, metaclass=ABCMeta):
def __init__(self, modulation, name, bandpass=None, requirements=None): def __init__(self, modulation, name, bandpass=None, requirements=None):
@ -120,6 +127,7 @@ class Modes(object):
WsjtMode("fst4", "FST4", requirements=["wsjt-x-2-3"]), WsjtMode("fst4", "FST4", requirements=["wsjt-x-2-3"]),
WsjtMode("fst4w", "FST4W", bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"]), WsjtMode("fst4w", "FST4W", bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"]),
WsjtMode("q65", "Q65", requirements=["wsjt-x-2-4"]), WsjtMode("q65", "Q65", requirements=["wsjt-x-2-4"]),
DigitalMode("msk144", "MSK144", requirements=["msk144"], underlying=["usb"], service=True),
Js8Mode("js8", "JS8Call"), Js8Mode("js8", "JS8Call"),
DigitalMode( DigitalMode(
"packet", "packet",

View File

@ -27,9 +27,9 @@ class PskReporter(Reporter):
Supports all valid MODE and SUBMODE values from the ADIF standard. Supports all valid MODE and SUBMODE values from the ADIF standard.
Current version at the time of the last change: Current version at the time of the last change:
https://www.adif.org/312/ADIF_312.htm#Mode_Enumeration https://www.adif.org/314/ADIF_314.htm#Mode_Enumeration
""" """
return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65", "WSPR", "FST4W"] return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65", "WSPR", "FST4W", "MSK144"]
def stop(self): def stop(self):
self.cancelTimer() self.cancelTimer()
@ -105,27 +105,34 @@ class Uploader(object):
# filter out any erroneous encodes # filter out any erroneous encodes
encoded = [e for e in encoded if e is not None] encoded = [e for e in encoded if e is not None]
def chunks(l, n): def chunks(block, max_size):
"""Yield successive n-sized chunks from l.""" size = 0
for i in range(0, len(l), n): current = []
yield l[i : i + n] for r in block:
if size + len(r) > max_size:
yield current
current = []
size = 0
size += len(r)
current.append(r)
yield current
rHeader = self.getReceiverInformationHeader() rHeader = self.getReceiverInformationHeader()
rInfo = self.getReceiverInformation() rInfo = self.getReceiverInformation()
sHeader = self.getSenderInformationHeader() sHeader = self.getSenderInformationHeader()
packets = [] packets = []
# 50 seems to be a safe bet # 1200 bytes of sender data should keep the packet size below MTU for most cases
for chunk in chunks(encoded, 50): for chunk in chunks(encoded, 1200):
sInfo = self.getSenderInformation(chunk) sInfo = self.getSenderInformation(chunk)
length = 16 + len(rHeader) + len(sHeader) + len(rInfo) + len(sInfo) length = 16 + len(rHeader) + len(sHeader) + len(rInfo) + len(sInfo)
header = self.getHeader(length) header = self.getHeader(length)
packets.append(header + rHeader + sHeader + rInfo + sInfo) packets.append(header + rHeader + sHeader + rInfo + sInfo)
self.sequence = (self.sequence + len(chunk)) % (1 << 32)
return packets return packets
def getHeader(self, length): def getHeader(self, length):
self.sequence += 1
return bytes( return bytes(
# protocol version # protocol version
[0x00, 0x0A] [0x00, 0x0A]
@ -142,7 +149,7 @@ class Uploader(object):
try: try:
return bytes( return bytes(
self.encodeString(spot["source"]["callsign"]) self.encodeString(spot["source"]["callsign"])
+ list(int(spot["freq"]).to_bytes(4, "big")) + list(int(spot["freq"]).to_bytes(5, "big"))
+ list(int(spot["db"]).to_bytes(1, "big", signed=True)) + list(int(spot["db"]).to_bytes(1, "big", signed=True))
+ self.encodeString(spot["mode"]) + self.encodeString(spot["mode"])
+ self.encodeString(spot["locator"]) + self.encodeString(spot["locator"])
@ -208,7 +215,7 @@ class Uploader(object):
# senderCallsign # senderCallsign
+ [0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# frequency # frequency
+ [0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x05, 0x00, 0x05, 0x00, 0x00, 0x76, 0x8F]
# sNR # sNR
+ [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F]
# mode # mode

View File

@ -122,6 +122,13 @@ class ServiceHandler(SdrSourceEventClient):
self.startupTimer.start() self.startupTimer.start()
def updateServices(self): def updateServices(self):
def addService(dial, source):
try:
service = self.setupService(dial, source)
self.services.append(service)
except Exception:
logger.exception("Error setting up service {mode} on frequency {frequency}".format(**dial))
with self.lock: with self.lock:
logger.debug("re-scheduling services due to sdr changes") logger.debug("re-scheduling services due to sdr changes")
self.stopServices() self.stopServices()
@ -146,7 +153,7 @@ class ServiceHandler(SdrSourceEventClient):
groups = self.optimizeResampling(dials, sr) groups = self.optimizeResampling(dials, sr)
if groups is None: if groups is None:
for dial in dials: for dial in dials:
self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source)) addService(dial, self.source)
else: else:
for group in groups: for group in groups:
if len(group) > 1: if len(group) > 1:
@ -157,14 +164,14 @@ class ServiceHandler(SdrSourceEventClient):
resampler = Resampler(resampler_props, self.source) resampler = Resampler(resampler_props, self.source)
for dial in group: for dial in group:
self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler)) addService(dial, resampler)
# resampler goes in after the services since it must not be shutdown as long as the services are # resampler goes in after the services since it must not be shutdown as long as the services are
# still running # still running
self.services.append(resampler) self.services.append(resampler)
else: else:
dial = group[0] dial = group[0]
self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source)) addService(dial, self.source)
def get_min_max(self, group): def get_min_max(self, group):
frequencies = sorted(group, key=lambda f: f["frequency"]) frequencies = sorted(group, key=lambda f: f["frequency"])
@ -238,23 +245,26 @@ class ServiceHandler(SdrSourceEventClient):
return None return None
return best["groups"] return best["groups"]
def setupService(self, mode, frequency, source): def setupService(self, dial, source):
logger.debug("setting up service {0} on frequency {1}".format(mode, frequency)) logger.debug("setting up service {mode} on frequency {frequency}".format(**dial))
modeObject = Modes.findByModulation(mode) modeObject = Modes.findByModulation(dial["mode"])
if not isinstance(modeObject, DigitalMode): if not isinstance(modeObject, DigitalMode):
logger.warning("mode is not a digimode: %s", mode) logger.warning("mode is not a digimode: %s", dial["mode"])
return None return None
if "underlying" in dial:
modeObject = modeObject.for_underlying(dial["underlying"])
demod = self._getDemodulator(modeObject.get_modulation()) demod = self._getDemodulator(modeObject.get_modulation())
secondaryDemod = self._getSecondaryDemodulator(modeObject.modulation) secondaryDemod = self._getSecondaryDemodulator(modeObject.modulation)
center_freq = source.getProps()["center_freq"] center_freq = source.getProps()["center_freq"]
sampleRate = source.getProps()["samp_rate"] sampleRate = source.getProps()["samp_rate"]
bandpass = modeObject.get_bandpass() bandpass = modeObject.get_bandpass()
if isinstance(secondaryDemod, DialFrequencyReceiver): if isinstance(secondaryDemod, DialFrequencyReceiver):
secondaryDemod.setDialFrequency(frequency) secondaryDemod.setDialFrequency(dial["frequency"])
chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, frequency - center_freq) chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, dial["frequency"] - center_freq)
chain.setBandPass(bandpass.low_cut, bandpass.high_cut) chain.setBandPass(bandpass.low_cut, bandpass.high_cut)
chain.setReader(source.getBuffer().getReader()) chain.setReader(source.getBuffer().getReader())
@ -279,11 +289,13 @@ class ServiceHandler(SdrSourceEventClient):
def _getSecondaryDemodulator(self, mod) -> Optional[ServiceDemodulator]: def _getSecondaryDemodulator(self, mod) -> Optional[ServiceDemodulator]:
if isinstance(mod, ServiceDemodulatorChain): if isinstance(mod, ServiceDemodulatorChain):
return mod return mod
# TODO add remaining modes
if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]:
from csdr.chain.digimodes import AudioChopperDemodulator from csdr.chain.digimodes import AudioChopperDemodulator
from owrx.wsjt import WsjtParser from owrx.wsjt import WsjtParser
return AudioChopperDemodulator(mod, WsjtParser()) return AudioChopperDemodulator(mod, WsjtParser())
elif mod == "msk144":
from csdr.chain.digimodes import Msk144Demodulator
return Msk144Demodulator()
elif mod == "js8": elif mod == "js8":
from csdr.chain.digimodes import AudioChopperDemodulator from csdr.chain.digimodes import AudioChopperDemodulator
from owrx.js8 import Js8Parser from owrx.js8 import Js8Parser
@ -291,7 +303,8 @@ class ServiceHandler(SdrSourceEventClient):
elif mod == "packet": elif mod == "packet":
from csdr.chain.digimodes import PacketDemodulator from csdr.chain.digimodes import PacketDemodulator
return PacketDemodulator(service=True) return PacketDemodulator(service=True)
return None
raise ValueError("unsupported service modulation: {}".format(mod))
class Services(object): class Services(object):

View File

@ -245,6 +245,17 @@ class Q65Profile(WsjtProfile):
return ["jt9", "--q65", "-p", str(self.interval), "-b", self.mode.name, "-d", str(self.decoding_depth()), file] return ["jt9", "--q65", "-p", str(self.interval), "-b", self.mode.name, "-d", str(self.decoding_depth()), file]
class Msk144Profile(WsjtProfile):
def getMode(self):
return "MSK144"
def getInterval(self):
return 15
def decoder_commandline(self, file):
return None
class WsjtParser(AudioChopperParser): class WsjtParser(AudioChopperParser):
def parse(self, profile: WsjtProfile, freq: int, raw_msg: bytes): def parse(self, profile: WsjtProfile, freq: int, raw_msg: bytes):
try: try:
@ -366,6 +377,8 @@ class Jt9Decoder(Decoder):
# '0003 -4 0.4 1762 # CQ R2ABM KO85' # '0003 -4 0.4 1762 # CQ R2ABM KO85'
# fst4 sample # fst4 sample
# '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV' # '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV'
# MSK144 sample
# '221602 8 0.4 1488 & K1JT WA4CQG EM72'
msg, timestamp = self.parse_timestamp(msg) msg, timestamp = self.parse_timestamp(msg)
wsjt_msg = msg[17:53].strip() wsjt_msg = msg[17:53].strip()