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**
|
||||
- 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)
|
||||
|
@ -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
|
||||
|
12
bands.json
12
bands.json
@ -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"]
|
||||
},
|
||||
|
@ -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()
|
||||
|
@ -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
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
|
||||
* 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
2
debian/control
vendored
@ -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
|
||||
|
@ -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"
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 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
|
||||
|
@ -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/)
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
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]
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
@ -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