Compare commits

..

22 Commits

Author SHA1 Message Date
Jakob Ketterl
57f55bbdd5
Merge pull request #342 from bd5rv/develop
Add --no-http-keep-alive to wget's arguments
2023-03-10 21:11:31 +01:00
Michael Chen
e8ba61bb81
Add --no-http-keep-alive to wget's arguments
When downloading SDRPlay API from sdrplay.com'server, it reports 403 forbidden error when the environment variable https_proxy is set. Adding --no-http-keep-live argument to wget will make the download working.
2023-03-11 00:31:19 +08:00
Jakob Ketterl
cb5b2e64af change chunking to work with actual byte-sizes 2023-02-22 17:23:11 +01:00
Jakob Ketterl
685b9970d2 switch frequency field to 5 bytes to support QO-100 2023-02-22 16:19:18 +01:00
Jakob Ketterl
c385a8f6b1 update changelogs 2023-02-22 14:13:01 +01:00
Jakob Ketterl
aa60b9d4a7 add msk144decoder to docker build 2023-02-21 19:50:44 +01:00
Jakob Ketterl
bff09e3363 add msk144decoder to recommended packages 2023-02-21 17:42:33 +01:00
Jakob Ketterl
774f8bd91a remove debugging messages 2023-02-21 17:41:55 +01:00
Jakob Ketterl
b1684908a4 remove todo 2023-02-19 16:18:49 +01:00
Jakob Ketterl
ed76fd7606 add MSK144 service demodulator 2023-02-19 16:18:08 +01:00
Jakob Ketterl
7b3f212ccb improve error handling during service initialization 2023-02-19 16:14:08 +01:00
Jakob Ketterl
216a3db45d add MSK144 to list of pskreporter modes 2023-02-16 19:47:46 +01:00
Jakob Ketterl
c16de474c6 route msk144 data to the wsjt message panel 2023-02-14 18:45:51 +01:00
Jakob Ketterl
afcd8277d1 add MSK144 parsing 2023-02-14 18:36:17 +01:00
Jakob Ketterl
525b70d495 add msk144 frequencies 2023-02-14 15:40:20 +01:00
Jakob Ketterl
f58023f3e5 add msk144demodulator chain 2023-02-14 15:39:59 +01:00
Jakob Ketterl
252edb7a5a add feature detection 2023-02-14 15:38:33 +01:00
Jakob Ketterl
2993cc4279 update wsjt-x homepage url 2023-02-14 15:37:37 +01:00
Jakob Ketterl
cc4f3c6c1d correctly commit patch? 2023-01-28 19:23:46 +01:00
Jakob Ketterl
0de597481c update to wsjt-x 2.6.1 2023-01-28 19:13:10 +01:00
Jakob Ketterl
2342bb5d29 update WSJT-X download location 2023-01-28 17:36:45 +01:00
Jakob Ketterl
b1ac8caf9b replace with a more robust state engine implementation 2023-01-28 16:37:32 +01:00
47 changed files with 542 additions and 594 deletions

View File

@ -1,5 +1,6 @@
**unreleased**
- SDR device log messages are now available in the web configuration to simplify troubleshooting
- Added support for the MSK144 digimode
**1.2.1**
- 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)
- Multiple SDR devices can be used simultaneously
- [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)
- [direwolf](https://github.com/wb2osz/direwolf) based demodulation of APRS packets
- [JS8Call](http://js8call.com/) support

View File

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

View File

@ -1,4 +1,5 @@
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.aprs.kiss import KissDeframer
from owrx.aprs import Ax25Parser, AprsParser
@ -20,6 +21,23 @@ class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver):
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):
def __init__(self, service: bool = False):
self.parser = AprsParser()

View File

@ -126,7 +126,7 @@ class PopenModule(AutoStartModule, metaclass=ABCMeta):
# resume in case the reader has been stop()ed before
self.reader.resume()
Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start()
Thread(target=self.pump(partial(self.process.stdout.read, 1024), self.writer.write)).start()
Thread(target=self.pump(partial(self.process.stdout.read1, 1024), self.writer.write)).start()
def stop(self):
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
* SDR device log messages are now available in the web configuration to
simplify troubleshooting
* Added support for the MSK144 digimode
-- 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
Architecture: all
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, owrx-connector (>= 0.5), soapysdr-tools, python3-csdr (>= 0.18), ${python3:Depends}, ${misc:Depends}
Recommends: python3-digiham (>= 0.6), direwolf (>= 1.4), wsjtx, 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
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
--- wsjtx-orig/CMakeLists.txt 2021-11-02 16:34:09.361811689 +0100
+++ wsjtx/CMakeLists.txt 2021-11-02 16:38:36.696088115 +0100
--- wsjtx-orig/CMakeLists.txt 2023-01-28 17:43:05.586124507 +0100
+++ wsjtx/CMakeLists.txt 2023-01-28 17:56:07.108634912 +0100
@@ -122,7 +122,7 @@
option (WSJT_QDEBUG_TO_FILE "Redirect Qt debuging messages to a trace file.")
option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON)
@ -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_TRACE_UDP "Debugging option that turns on UDP message protocol diagnostics.")
option (WSJT_BUILD_UTILS "Build simulators and code demonstrators." ON)
@@ -169,76 +169,7 @@
@@ -170,77 +170,7 @@
)
set (wsjt_qt_CXXSRCS
@ -34,6 +34,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
- widgets/FrequencyDeltaLineEdit.cpp
- item_delegates/CandidateKeyFilter.cpp
- item_delegates/ForeignKeyDelegate.cpp
- item_delegates/MessageItemDelegate.cpp
- validators/LiveFrequencyValidator.cpp
- GetUserId.cpp
- Audio/AudioDevice.cpp
@ -87,7 +88,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
)
set (wsjt_qtmm_CXXSRCS
@@ -1079,9 +1010,6 @@
@@ -1089,9 +1019,6 @@
if (WSJT_GENERATE_DOCS)
add_subdirectory (doc)
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)
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})
# set wsjtx_udp exports to static variants
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
add_library (fort_qt STATIC ${fort_qt_CXXSRCS})
@@ -1408,60 +1333,6 @@
@@ -1425,90 +1349,6 @@
add_subdirectory (map65)
endif ()
@ -146,7 +147,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
- )
-
-target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS})
-if (APPLE)
-if ((NOT ${OPENMP_FOUND}) OR APPLE)
- target_link_libraries (wsjtx wsjt_fort)
-else ()
- target_link_libraries (wsjtx wsjt_fort_omp)
@ -167,10 +168,40 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
-endif ()
-target_link_libraries (wsjtx Qt5::SerialPort wsjt_cxx wsjt_qt wsjt_qtmm ${FFTW3_LIBRARIES} ${LIBM_LIBRARIES})
-
# make a library for WSJT-X UDP servers
# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS})
add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS})
@@ -1501,47 +1372,9 @@
-# make a library for WSJT-X UDP servers
-# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS})
-add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS})
-#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})
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
# RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
@@ -1560,12 +1393,7 @@
@@ -1577,12 +1379,7 @@
# 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
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
)
@@ -1578,38 +1406,6 @@
@@ -1595,38 +1392,6 @@
)
endif(WSJT_BUILD_UTILS)
@ -271,7 +302,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
install (FILES
cty.dat
cty.dat_copyright.txt
@@ -1618,13 +1414,6 @@
@@ -1635,13 +1400,6 @@
#COMPONENT runtime
)
@ -285,7 +316,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
#
# Mac installer files
#
@@ -1676,22 +1465,6 @@
@@ -1693,22 +1451,6 @@
"${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
)

View File

@ -38,7 +38,7 @@ case $ARCH in
;;
esac
wget https://www.sdrplay.com/software/$BINARY
wget --no-http-keep-alive https://www.sdrplay.com/software/$BINARY
sh $BINARY --noexec --target sdrplay
patch --verbose -Np0 < /install-lib.$ARCH.patch

View File

@ -7,6 +7,9 @@ function cmakebuild() {
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
if [[ -f ".gitmodules" ]]; then
git submodule update --init
fi
mkdir build
cd build
cmake ${CMAKE_ARGS:-} ..
@ -18,8 +21,8 @@ function cmakebuild() {
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"
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"
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 libcurl4-openssl-dev"
apt-get update
apt-get -y install auto-apt-proxy
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
@ -51,15 +54,19 @@ rm /js8call-hamlib.patch
cmakebuild ${JS8CALL_DIR}
rm ${JS8CALL_TGZ}
WSJT_DIR=wsjtx-2.5.4
WSJT_DIR=wsjtx-2.6.1
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}
patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch
mv /wsjtx.patch ${WSJT_DIR}
cmakebuild ${WSJT_DIR}
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
cd direwolf
# 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="fst4w"] #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="wspr"] #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="fst4"] #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;
}
@ -1290,7 +1292,8 @@ img.openwebrx-mirror-img
#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="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;
margin: -10px;

View File

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

View File

@ -158,8 +158,8 @@ DemodulatorPanel.prototype.updatePanels = function() {
var modulation = this.getDemodulator().get_secondary_demod();
$('#openwebrx-panel-digimodes').attr('data-mode', 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-js8-message", modulation == "js8");
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-packet-message", modulation === "packet");
toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag");

View File

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

View File

@ -1,104 +0,0 @@
from abc import ABC, abstractmethod
import logging
logger = logging.getLogger(__name__)
class ActiveListChange(ABC):
pass
class ActiveListIndexUpdated(ActiveListChange):
def __init__(self, index: int, oldValue, newValue):
self.index = index
self.oldValue = oldValue
self.newValue = newValue
class ActiveListIndexAppended(ActiveListChange):
def __init__(self, index: int, newValue):
self.index = index
self.newValue = newValue
class ActiveListIndexDeleted(ActiveListChange):
def __init__(self, index: int, oldValue):
self.index = index
self.oldValue = oldValue
class ActiveListListener(ABC):
@abstractmethod
def onListChange(self, changes: list[ActiveListChange]):
pass
class ActiveListTransformationListener(ActiveListListener):
def __init__(self, transformation: callable, target: "ActiveList"):
self.transformation = transformation
self.target = target
def onListChange(self, changes: list[ActiveListChange]):
for change in changes:
if isinstance(change, ActiveListIndexUpdated):
self.target[change.index] = self.transformation(change.newValue)
elif isinstance(change, ActiveListIndexAppended):
self.target.append(self.transformation(change.newValue))
elif isinstance(change, ActiveListIndexDeleted):
del self.target[change.index]
class ActiveList:
def __init__(self, elements: list = None):
self.delegate = elements.copy() if elements is not None else []
self.listeners = []
def addListener(self, listener: ActiveListListener):
if listener in self.listeners:
return
self.listeners.append(listener)
def removeListener(self, listener: ActiveListListener):
if listener not in self.listeners:
return
self.listeners.remove(listener)
def append(self, value):
self.delegate.append(value)
self.__fireChanges([ActiveListIndexAppended(len(self) - 1, value)])
def __fireChanges(self, changes: list[ActiveListChange]):
for listener in self.listeners:
try:
listener.onListChange(changes)
except Exception:
logger.exception("Exception during onListChange notification")
def remove(self, value):
self.__delitem__(self.delegate.index(value))
def map(self, transform: callable):
res = ActiveList([transform(v) for v in self])
self.addListener(ActiveListTransformationListener(transform, res))
return res
def __setitem__(self, key, value):
if self.delegate[key] == value:
return
oldValue = self.delegate[key]
self.delegate[key] = value
self.__fireChanges([ActiveListIndexUpdated(key, oldValue, value)])
def __delitem__(self, key):
oldValue = self.delegate[key]
del self.delegate[key]
self.__fireChanges([ActiveListIndexDeleted(key, oldValue)])
def __getitem__(self, key):
return self.delegate[key]
def __len__(self):
return len(self.delegate)
def __iter__(self):
return self.delegate.__iter__()

View File

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

View File

@ -80,6 +80,7 @@ class FeatureDetector(object):
"wsjt-x": ["wsjtx"],
"wsjt-x-2-3": ["wsjtx_2_3"],
"wsjt-x-2-4": ["wsjtx_2_4"],
"msk144": ["msk144decoder"],
"packet": ["direwolf"],
"pocsag": ["digiham"],
"js8call": ["js8", "js8py"],
@ -428,7 +429,7 @@ class FeatureDetector(object):
def has_wsjtx(self):
"""
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.
"""
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"))
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):
"""
To decode JS8, you will need to install [JS8Call](http://js8call.com/)

View File

@ -120,6 +120,7 @@ class Modes(object):
WsjtMode("fst4", "FST4", 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"]),
DigitalMode("msk144", "MSK144", requirements=["msk144"], underlying=["usb"], service=True),
Js8Mode("js8", "JS8Call"),
DigitalMode(
"packet",

View File

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

View File

@ -122,6 +122,15 @@ class ServiceHandler(SdrSourceEventClient):
self.startupTimer.start()
def updateServices(self):
def addService(dial, source):
mode = dial["mode"]
frequency = dial["frequency"]
try:
service = self.setupService(mode, frequency, source)
self.services.append(service)
except Exception:
logger.exception("Error setting up service %s on frequency %d", mode, frequency)
with self.lock:
logger.debug("re-scheduling services due to sdr changes")
self.stopServices()
@ -146,7 +155,7 @@ class ServiceHandler(SdrSourceEventClient):
groups = self.optimizeResampling(dials, sr)
if groups is None:
for dial in dials:
self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source))
addService(dial, self.source)
else:
for group in groups:
if len(group) > 1:
@ -157,14 +166,14 @@ class ServiceHandler(SdrSourceEventClient):
resampler = Resampler(resampler_props, self.source)
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
# still running
self.services.append(resampler)
else:
dial = group[0]
self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source))
addService(dial, self.source)
def get_min_max(self, group):
frequencies = sorted(group, key=lambda f: f["frequency"])
@ -279,11 +288,13 @@ class ServiceHandler(SdrSourceEventClient):
def _getSecondaryDemodulator(self, mod) -> Optional[ServiceDemodulator]:
if isinstance(mod, ServiceDemodulatorChain):
return mod
# TODO add remaining modes
if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]:
from csdr.chain.digimodes import AudioChopperDemodulator
from owrx.wsjt import WsjtParser
return AudioChopperDemodulator(mod, WsjtParser())
elif mod == "msk144":
from csdr.chain.digimodes import Msk144Demodulator
return Msk144Demodulator()
elif mod == "js8":
from csdr.chain.digimodes import AudioChopperDemodulator
from owrx.js8 import Js8Parser
@ -291,7 +302,8 @@ class ServiceHandler(SdrSourceEventClient):
elif mod == "packet":
from csdr.chain.digimodes import PacketDemodulator
return PacketDemodulator(service=True)
return None
raise ValueError("unsupported service modulation: {}".format(mod))
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]
class Msk144Profile(WsjtProfile):
def getMode(self):
return "MSK144"
def getInterval(self):
return 15
def decoder_commandline(self, file):
return None
class WsjtParser(AudioChopperParser):
def parse(self, profile: WsjtProfile, freq: int, raw_msg: bytes):
try:
@ -366,6 +377,8 @@ class Jt9Decoder(Decoder):
# '0003 -4 0.4 1762 # CQ R2ABM KO85'
# fst4 sample
# '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV'
# MSK144 sample
# '221602 8 0.4 1488 & K1JT WA4CQG EM72'
msg, timestamp = self.parse_timestamp(msg)
wsjt_msg = msg[17:53].strip()

View File

@ -1,123 +0,0 @@
from owrx.active.list import ActiveList, ActiveListIndexUpdated, ActiveListIndexAppended, ActiveListIndexDeleted
from unittest import TestCase
from unittest.mock import Mock
class ActiveListTest(TestCase):
def testListIndexReadAccess(self):
list = ActiveList(["testvalue"])
self.assertEqual(list[0], "testvalue")
def testListIndexWriteAccess(self):
list = ActiveList(["initialvalue"])
list[0] = "testvalue"
self.assertEqual(list[0], "testvalue")
def testListLength(self):
list = ActiveList(["somevalue"])
self.assertEqual(len(list), 1)
def testListIndexChangeNotification(self):
list = ActiveList(["initialvalue"])
listenerMock = Mock()
list.addListener(listenerMock)
list[0] = "testvalue"
listenerMock.onListChange.assert_called_once()
changes, = listenerMock.onListChange.call_args.args
self.assertEqual(len(changes), 1)
self.assertIsInstance(changes[0], ActiveListIndexUpdated)
self.assertEqual(changes[0].index, 0)
self.assertEqual(changes[0].oldValue, "initialvalue")
self.assertEqual(changes[0].newValue, "testvalue")
def testListIndexChangeNotficationNotDisturbedByException(self):
list = ActiveList(["initialvalue"])
throwingMock = Mock()
throwingMock.onListChange.side_effect = RuntimeError("this is a drill")
list.addListener(throwingMock)
listenerMock = Mock()
list.addListener(listenerMock)
list[0] = "testvalue"
listenerMock.onListChange.assert_called_once()
def testListAppend(self):
list = ActiveList()
list.append("testvalue")
self.assertEqual(len(list), 1)
self.assertEqual(list[0], "testvalue")
def testListAppendNotification(self):
list = ActiveList()
listenerMock = Mock()
list.addListener(listenerMock)
list.append("testvalue")
listenerMock.onListChange.assert_called_once()
changes, = listenerMock.onListChange.call_args.args
self.assertEqual(len(changes), 1)
self.assertIsInstance(changes[0], ActiveListIndexAppended)
self.assertEqual(changes[0].index, 0)
self.assertEqual(changes[0].newValue, "testvalue")
def testListDelete(self):
list = ActiveList(["value1", "value2"])
del list[0]
self.assertEqual(len(list), 1)
self.assertEqual(list[0], "value2")
def testListDeleteNotification(self):
list = ActiveList(["value1", "value2"])
listenerMock = Mock()
list.addListener(listenerMock)
del list[0]
listenerMock.onListChange.assert_called_once()
changes, = listenerMock.onListChange.call_args.args
self.assertEqual(len(changes), 1)
self.assertIsInstance(changes[0], ActiveListIndexDeleted)
self.assertEqual(changes[0].index, 0)
self.assertEqual(changes[0].oldValue, 'value1')
def testListDeleteByValue(self):
list = ActiveList(["value1", "value2"])
list.remove("value1")
self.assertEqual(len(list), 1)
self.assertEqual(list[0], "value2")
def testListComprehension(self):
list = ActiveList(["initialvalue"])
x = [m for m in list]
self.assertEqual(len(x), 1)
self.assertEqual(x[0], "initialvalue")
def testListenerRemoval(self):
list = ActiveList(["initialvalue"])
listenerMock = Mock()
list.addListener(listenerMock)
list[0] = "testvalue"
listenerMock.onListChange.assert_called_once()
listenerMock.reset_mock()
list.removeListener(listenerMock)
list[0] = "someothervalue"
listenerMock.onListChange.assert_not_called()
def testListMapTransformation(self):
list = ActiveList(["somevalue"])
transformedList = list.map(lambda x: "prefix-{}".format(x))
self.assertEqual(transformedList[0], "prefix-somevalue")
def testActiveTransformationUpdate(self):
list = ActiveList(["initialvalue"])
transformedList = list.map(lambda x: "prefix-{}".format(x))
list[0] = "testvalue"
self.assertEqual(transformedList[0], "prefix-testvalue")
def testActiveTransformationAppend(self):
list = ActiveList(["initialvalue"])
transformedList = list.map(lambda x: "prefix-{}".format(x))
list.append("newvalue")
self.assertEqual(transformedList[1], "prefix-newvalue")
def testActiveTransformationDelete(self):
list = ActiveList(["value1", "value2"])
transformedList = list.map(lambda x: "prefix-{}".format(x))
del list[0]
self.assertEqual(transformedList[0], "prefix-value2")