Compare commits

..

5 Commits

Author SHA1 Message Date
Jakob Ketterl
b31581dc80 implement active list transformation 2022-12-14 01:22:48 +01:00
Jakob Ketterl
f73c62c5df change the list notification interface 2022-12-14 01:07:20 +01:00
Jakob Ketterl
e7e5af9a53 add a test for listener removal 2022-12-12 17:42:16 +01:00
Jakob Ketterl
c7d2a5502c add first shot at active list implementation 2022-12-12 17:39:07 +01:00
Jakob Ketterl
59759fa79d move tests to match folder structure 2022-12-12 16:06:15 +01:00
47 changed files with 594 additions and 542 deletions

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
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
@ -21,23 +20,6 @@ class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver):
self.chopper.setDialFrequency(frequency) self.chopper.setDialFrequency(frequency)
class Msk144Demodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self):
self.parser = ParserAdapter()
workers = [
Convert(Format.FLOAT, Format.SHORT),
Msk144Module(),
self.parser,
]
super().__init__(workers)
def getFixedAudioRate(self) -> int:
return 12000
def setDialFrequency(self, frequency: int) -> None:
self.parser.setDialFrequency(frequency)
class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver): class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self, service: bool = False): def __init__(self, service: bool = False):
self.parser = AprsParser() self.parser = AprsParser()

View File

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

View File

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

1
debian/changelog vendored
View File

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

2
debian/control vendored
View File

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

View File

@ -1,6 +1,6 @@
diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
--- wsjtx-orig/CMakeLists.txt 2023-01-28 17:43:05.586124507 +0100 --- wsjtx-orig/CMakeLists.txt 2021-11-02 16:34:09.361811689 +0100
+++ wsjtx/CMakeLists.txt 2023-01-28 17:56:07.108634912 +0100 +++ wsjtx/CMakeLists.txt 2021-11-02 16:38:36.696088115 +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)
@@ -170,77 +170,7 @@ @@ -169,76 +169,7 @@
) )
set (wsjt_qt_CXXSRCS set (wsjt_qt_CXXSRCS
@ -34,7 +34,6 @@ 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
@ -88,7 +87,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
) )
set (wsjt_qtmm_CXXSRCS set (wsjt_qtmm_CXXSRCS
@@ -1089,9 +1019,6 @@ @@ -1079,9 +1010,6 @@
if (WSJT_GENERATE_DOCS) if (WSJT_GENERATE_DOCS)
add_subdirectory (doc) add_subdirectory (doc)
endif (WSJT_GENERATE_DOCS) endif (WSJT_GENERATE_DOCS)
@ -98,7 +97,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})
@@ -1357,10 +1284,7 @@ @@ -1340,10 +1268,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)
@ -110,7 +109,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})
@@ -1425,90 +1349,6 @@ @@ -1408,60 +1333,6 @@
add_subdirectory (map65) add_subdirectory (map65)
endif () endif ()
@ -147,7 +146,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 ((NOT ${OPENMP_FOUND}) OR APPLE) -if (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)
@ -168,40 +167,10 @@ 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})
-#target_include_directories (wsjtx_udp @@ -1501,47 +1372,9 @@
-# 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)
@ -249,7 +218,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}
@@ -1577,12 +1379,7 @@ @@ -1560,12 +1393,7 @@
# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx # DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx
# ) # )
@ -263,7 +232,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
) )
@@ -1595,38 +1392,6 @@ @@ -1578,38 +1406,6 @@
) )
endif(WSJT_BUILD_UTILS) endif(WSJT_BUILD_UTILS)
@ -302,7 +271,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
@@ -1635,13 +1400,6 @@ @@ -1618,13 +1414,6 @@
#COMPONENT runtime #COMPONENT runtime
) )
@ -316,7 +285,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# #
# Mac installer files # Mac installer files
# #
@@ -1693,22 +1451,6 @@ @@ -1676,22 +1465,6 @@
"${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h" "${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
) )

View File

@ -38,7 +38,7 @@ case $ARCH in
;; ;;
esac esac
wget --no-http-keep-alive https://www.sdrplay.com/software/$BINARY wget 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

View File

@ -7,9 +7,6 @@ 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:-} ..
@ -21,8 +18,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 libcurl4" 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 libcurl4-openssl-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"
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
@ -54,19 +51,15 @@ rm /js8call-hamlib.patch
cmakebuild ${JS8CALL_DIR} cmakebuild ${JS8CALL_DIR}
rm ${JS8CALL_TGZ} rm ${JS8CALL_TGZ}
WSJT_DIR=wsjtx-2.6.1 WSJT_DIR=wsjtx-2.5.4
WSJT_TGZ=${WSJT_DIR}.tgz WSJT_TGZ=${WSJT_DIR}.tgz
wget https://downloads.sourceforge.net/project/wsjt/${WSJT_DIR}/${WSJT_TGZ} wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ}
tar xfz ${WSJT_TGZ} tar xfz ${WSJT_TGZ}
patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch
mv /wsjtx.patch ${WSJT_DIR} mv /wsjtx.patch ${WSJT_DIR}
cmakebuild ${WSJT_DIR} cmakebuild ${WSJT_DIR}
rm ${WSJT_TGZ} rm ${WSJT_TGZ}
git clone https://github.com/alexander-sholohov/msk144decoder.git
# latest from main as of 2023-02-21
MAKEFLAGS="" cmakebuild msk144decoder fe2991681e455636e258e83c29fd4b2a72d16095
git clone --depth 1 -b 1.6 https://github.com/wb2osz/direwolf.git git clone --depth 1 -b 1.6 https://github.com/wb2osz/direwolf.git
cd direwolf cd direwolf
# hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need. # hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need.

View File

@ -1265,7 +1265,6 @@ 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,
@ -1276,8 +1275,7 @@ 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;
} }
@ -1292,8 +1290,7 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-canvas-container
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-canvas-container
{ {
height: 200px; height: 200px;
margin: -10px; margin: -10px;

View File

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

View File

@ -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", "msk144"].indexOf(modulation) >= 0); 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-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");

View File

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

View File

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

View File

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

View File

@ -80,7 +80,6 @@ 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"],
@ -429,7 +428,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://wsjt.sourceforge.io/) for ready-made packages or instructions [WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) 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)
@ -460,13 +459,6 @@ class FeatureDetector(object):
""" """
return self.has_wsjtx() and self._has_wsjtx_version(LooseVersion("2.4")) return self.has_wsjtx() and self._has_wsjtx_version(LooseVersion("2.4"))
def has_msk144decoder(self):
"""
To decode the MSK144 digimode please install the "msk144decoder". See the
[project page](https://github.com/alexander-sholohov/msk144decoder) for more details.
"""
return self.command_is_runnable("msk144decoder")
def has_js8(self): def has_js8(self):
""" """
To decode JS8, you will need to install [JS8Call](http://js8call.com/) To decode JS8, you will need to install [JS8Call](http://js8call.com/)

View File

@ -120,7 +120,6 @@ class Modes(object):
WsjtMode("fst4", "FST4", requirements=["wsjt-x-2-3"]), WsjtMode("fst4", "FST4", requirements=["wsjt-x-2-3"]),
WsjtMode("fst4w", "FST4W", bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"]), WsjtMode("fst4w", "FST4W", bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"]),
WsjtMode("q65", "Q65", requirements=["wsjt-x-2-4"]), WsjtMode("q65", "Q65", requirements=["wsjt-x-2-4"]),
DigitalMode("msk144", "MSK144", requirements=["msk144"], underlying=["usb"], service=True),
Js8Mode("js8", "JS8Call"), Js8Mode("js8", "JS8Call"),
DigitalMode( DigitalMode(
"packet", "packet",

View File

@ -27,9 +27,9 @@ class PskReporter(Reporter):
Supports all valid MODE and SUBMODE values from the ADIF standard. Supports all valid MODE and SUBMODE values from the ADIF standard.
Current version at the time of the last change: Current version at the time of the last change:
https://www.adif.org/314/ADIF_314.htm#Mode_Enumeration https://www.adif.org/312/ADIF_312.htm#Mode_Enumeration
""" """
return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65", "WSPR", "FST4W", "MSK144"] return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65", "WSPR", "FST4W"]
def stop(self): def stop(self):
self.cancelTimer() self.cancelTimer()
@ -105,34 +105,27 @@ 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(block, max_size): def chunks(l, n):
size = 0 """Yield successive n-sized chunks from l."""
current = [] for i in range(0, len(l), n):
for r in block: yield l[i : i + n]
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 = []
# 1200 bytes of sender data should keep the packet size below MTU for most cases # 50 seems to be a safe bet
for chunk in chunks(encoded, 1200): for chunk in chunks(encoded, 50):
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]
@ -149,7 +142,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(5, "big")) + list(int(spot["freq"]).to_bytes(4, "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"])
@ -215,7 +208,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, 0x05, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F]
# sNR # sNR
+ [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F]
# mode # mode

View File

@ -122,15 +122,6 @@ 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()
@ -155,7 +146,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:
addService(dial, self.source) self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source))
else: else:
for group in groups: for group in groups:
if len(group) > 1: if len(group) > 1:
@ -166,14 +157,14 @@ class ServiceHandler(SdrSourceEventClient):
resampler = Resampler(resampler_props, self.source) resampler = Resampler(resampler_props, self.source)
for dial in group: for dial in group:
addService(dial, resampler) self.services.append(self.setupService(dial["mode"], dial["frequency"], 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]
addService(dial, self.source) self.services.append(self.setupService(dial["mode"], dial["frequency"], 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"])
@ -288,13 +279,11 @@ 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
@ -302,8 +291,7 @@ class ServiceHandler(SdrSourceEventClient):
elif mod == "packet": elif mod == "packet":
from csdr.chain.digimodes import PacketDemodulator from csdr.chain.digimodes import PacketDemodulator
return PacketDemodulator(service=True) return PacketDemodulator(service=True)
return None
raise ValueError("unsupported service modulation: {}".format(mod))
class Services(object): class Services(object):

View File

@ -245,17 +245,6 @@ 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:
@ -377,8 +366,6 @@ 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()

View File

@ -0,0 +1,123 @@
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")

View File

View File