Compare commits
22 Commits
active_arr
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
57f55bbdd5 | ||
|
e8ba61bb81 | ||
|
cb5b2e64af | ||
|
685b9970d2 | ||
|
c385a8f6b1 | ||
|
aa60b9d4a7 | ||
|
bff09e3363 | ||
|
774f8bd91a | ||
|
b1684908a4 | ||
|
ed76fd7606 | ||
|
7b3f212ccb | ||
|
216a3db45d | ||
|
c16de474c6 | ||
|
afcd8277d1 | ||
|
525b70d495 | ||
|
f58023f3e5 | ||
|
252edb7a5a | ||
|
2993cc4279 | ||
|
cc4f3c6c1d | ||
|
0de597481c | ||
|
2342bb5d29 | ||
|
b1ac8caf9b |
@ -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)
|
||||||
|
@ -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
|
||||||
|
12
bands.json
12
bands.json
@ -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"]
|
||||||
},
|
},
|
||||||
|
@ -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()
|
||||||
|
@ -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
57
csdr/module/msk144.py
Normal 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
1
debian/changelog
vendored
@ -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
2
debian/control
vendored
@ -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
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ case $ARCH in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
wget https://www.sdrplay.com/software/$BINARY
|
wget --no-http-keep-alive https://www.sdrplay.com/software/$BINARY
|
||||||
sh $BINARY --noexec --target sdrplay
|
sh $BINARY --noexec --target sdrplay
|
||||||
patch --verbose -Np0 < /install-lib.$ARCH.patch
|
patch --verbose -Np0 < /install-lib.$ARCH.patch
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -158,8 +158,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");
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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__()
|
|
@ -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
|
||||||
|
@ -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/)
|
||||||
|
@ -120,6 +120,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",
|
||||||
|
@ -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
|
||||||
|
@ -122,6 +122,15 @@ class ServiceHandler(SdrSourceEventClient):
|
|||||||
self.startupTimer.start()
|
self.startupTimer.start()
|
||||||
|
|
||||||
def updateServices(self):
|
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:
|
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 +155,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 +166,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"])
|
||||||
@ -279,11 +288,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 +302,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):
|
||||||
|
13
owrx/wsjt.py
13
owrx/wsjt.py
@ -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()
|
||||||
|
|
||||||
|
@ -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")
|
|
Loading…
Reference in New Issue
Block a user