From 738106cb93316cf3daa131fbb44a349d97f50cd2 Mon Sep 17 00:00:00 2001 From: jopohl Date: Sun, 18 Oct 2020 12:13:19 +0200 Subject: [PATCH 01/16] use costas loop for PSK demod --- src/urh/ainterpretation/AutoInterpretation.py | 6 +- .../controller/dialogs/CostaOptionsDialog.py | 27 ++++ src/urh/controller/widgets/SignalFrame.py | 28 +++- src/urh/cythonext/signal_functions.pyx | 136 +++++++++--------- src/urh/signalprocessing/Signal.py | 22 ++- src/urh/ui/ui_costa.py | 49 +++++++ src/urh/util/ProjectManager.py | 2 + tests/PlotTests.py | 2 +- 8 files changed, 195 insertions(+), 77 deletions(-) create mode 100644 src/urh/controller/dialogs/CostaOptionsDialog.py create mode 100644 src/urh/ui/ui_costa.py diff --git a/src/urh/ainterpretation/AutoInterpretation.py b/src/urh/ainterpretation/AutoInterpretation.py index 9f87c2fd1c..8ed3a623b3 100644 --- a/src/urh/ainterpretation/AutoInterpretation.py +++ b/src/urh/ainterpretation/AutoInterpretation.py @@ -382,11 +382,11 @@ def estimate(iq_array: IQArray, noise: float = None, modulation: str = None) -> message_indices = merge_message_segments_for_ook(message_indices) if modulation == "OOK" or modulation == "ASK": - data = signal_functions.afp_demod(iq_array.data, noise, "ASK") + data = signal_functions.afp_demod(iq_array.data, noise, "ASK", 2) elif modulation == "FSK": - data = signal_functions.afp_demod(iq_array.data, noise, "FSK") + data = signal_functions.afp_demod(iq_array.data, noise, "FSK", 2) elif modulation == "PSK": - data = signal_functions.afp_demod(iq_array.data, noise, "PSK") + data = signal_functions.afp_demod(iq_array.data, noise, "PSK", 2) else: raise ValueError("Unsupported Modulation") diff --git a/src/urh/controller/dialogs/CostaOptionsDialog.py b/src/urh/controller/dialogs/CostaOptionsDialog.py new file mode 100644 index 0000000000..39c9709a54 --- /dev/null +++ b/src/urh/controller/dialogs/CostaOptionsDialog.py @@ -0,0 +1,27 @@ +from PyQt5.QtCore import Qt, pyqtSlot +from PyQt5.QtWidgets import QDialog + +from urh.ui.ui_costa import Ui_DialogCosta + + +class CostaOptionsDialog(QDialog): + def __init__(self, loop_bandwidth, parent=None): + super().__init__(parent) + self.ui = Ui_DialogCosta() + self.ui.setupUi(self) + self.setAttribute(Qt.WA_DeleteOnClose) + self.setWindowFlags(Qt.Window) + + self.costas_loop_bandwidth = loop_bandwidth + self.ui.doubleSpinBoxLoopBandwidth.setValue(self.costas_loop_bandwidth) + + self.create_connects() + + def create_connects(self): + self.ui.buttonBox.accepted.connect(self.accept) + self.ui.buttonBox.rejected.connect(self.reject) + self.ui.doubleSpinBoxLoopBandwidth.valueChanged.connect(self.on_spinbox_loop_bandwidth_value_changed) + + @pyqtSlot(float) + def on_spinbox_loop_bandwidth_value_changed(self, value): + self.costas_loop_bandwidth = value diff --git a/src/urh/controller/widgets/SignalFrame.py b/src/urh/controller/widgets/SignalFrame.py index 61def6d0d8..2c1c05ccb6 100644 --- a/src/urh/controller/widgets/SignalFrame.py +++ b/src/urh/controller/widgets/SignalFrame.py @@ -10,6 +10,7 @@ from urh import settings from urh.controller.dialogs.AdvancedModulationOptionsDialog import AdvancedModulationOptionsDialog +from urh.controller.dialogs.CostaOptionsDialog import CostaOptionsDialog from urh.controller.dialogs.FilterDialog import FilterDialog from urh.controller.dialogs.SendDialog import SendDialog from urh.controller.dialogs.SignalDetailsDialog import SignalDetailsDialog @@ -276,7 +277,7 @@ def refresh_signal_information(self, block=True): self.ui.spinBoxSamplesPerSymbol.setValue(self.signal.samples_per_symbol) self.ui.spinBoxNoiseTreshold.setValue(self.signal.noise_threshold_relative) self.ui.cbModulationType.setCurrentText(self.signal.modulation_type) - self.ui.btnAdvancedModulationSettings.setVisible(self.ui.cbModulationType.currentText() == "ASK") + self.ui.btnAdvancedModulationSettings.setVisible(self.ui.cbModulationType.currentText() in ("ASK", "PSK")) self.ui.spinBoxCenterSpacing.setValue(self.signal.center_spacing) self.ui.spinBoxBitsPerSymbol.setValue(self.signal.bits_per_symbol) @@ -524,7 +525,10 @@ def update_protocol(self): self.ui.txtEdProto.setText("Demodulating...") qApp.processEvents() - self.proto_analyzer.get_protocol_from_signal() + try: + self.proto_analyzer.get_protocol_from_signal() + except Exception as e: + Errors.exception(e) def show_protocol(self, old_view=-1, refresh=False): if not self.proto_analyzer: @@ -1093,7 +1097,7 @@ def on_combobox_modulation_type_text_changed(self, txt: str): self.scene_manager.init_scene() self.on_slider_y_scale_value_changed() - self.ui.btnAdvancedModulationSettings.setVisible(self.ui.cbModulationType.currentText() == "ASK") + self.ui.btnAdvancedModulationSettings.setVisible(self.ui.cbModulationType.currentText() in ("ASK", "PSK")) @pyqtSlot() def on_signal_data_changed_before_save(self): @@ -1301,9 +1305,25 @@ def get_advanced_modulation_settings_dialog(self): dialog.message_length_divisor_edited.connect(self.on_message_length_divisor_edited) return dialog + def get_costas_dialog(self): + dialog = CostaOptionsDialog(self.signal.costas_loop_bandwidth, parent=self) + dialog.accepted.connect(self.on_costas_dialog_accepted) + return dialog + + @pyqtSlot() + def on_costas_dialog_accepted(self): + sender = self.sender() + assert isinstance(sender, CostaOptionsDialog) + self.signal.costas_loop_bandwidth = sender.costas_loop_bandwidth + @pyqtSlot() def on_btn_advanced_modulation_settings_clicked(self): - dialog = self.get_advanced_modulation_settings_dialog() + if self.ui.cbModulationType.currentText() == "ASK": + dialog = self.get_advanced_modulation_settings_dialog() + elif self.ui.cbModulationType.currentText() == "PSK": + dialog = self.get_costas_dialog() + else: + raise ValueError("No additional settings available") dialog.exec_() @pyqtSlot() diff --git a/src/urh/cythonext/signal_functions.pyx b/src/urh/cythonext/signal_functions.pyx index baa59c2ae5..4f04353b44 100644 --- a/src/urh/cythonext/signal_functions.pyx +++ b/src/urh/cythonext/signal_functions.pyx @@ -242,17 +242,27 @@ cdef np.ndarray[np.float32_t, ndim=1] gauss_fir(float sample_rate, uint32_t samp -(((np.sqrt(2) * np.pi) / np.sqrt(np.log(2)) * bt * k / samples_per_symbol) ** 2)) return h / h.sum() -cdef void phase_demod(IQ samples, float[::1] result, float noise_sqrd, bool qam, long long num_samples): - cdef long long i = 0 - cdef float real = 0, imag = 0, magnitude = 0 +cdef float clamp(float x) nogil: + if x < -1.0: + x = -1.0 + elif x > 1.0: + x = 1.0 + return x + +cdef float[::1] costa_demod(IQ samples, float noise_sqrd, int loop_order, float bandwidth=0.1, float damping=sqrt(2.0) / 2.0): + cdef float alpha = (4 * damping * bandwidth) / (1.0 + 2.0 * damping * bandwidth + bandwidth * bandwidth) + cdef float beta = (4 * bandwidth * bandwidth) / (1.0 + 2.0 * damping * bandwidth + bandwidth * bandwidth) + + cdef long long i = 0, num_samples = len(samples) + cdef float real = 0, imag = 0 cdef float scale, shift, real_float, imag_float, ref_real, ref_imag - cdef float phi = 0, current_arg = 0, f_curr = 0, f_prev = 0 + cdef float f1, f2, costa_freq = 0, costa_error = 0, costa_phase = 1.5 - cdef float complex current_sample, conj_previous_sample, current_nco + cdef float complex current_sample, nco_out, nco_times_sample - cdef float alpha = 0.1 + cdef float[::1] result = np.empty(num_samples, dtype=np.float32) if str(cython.typeof(samples)) == "char[:, ::1]": scale = 127.5 @@ -275,9 +285,8 @@ cdef void phase_demod(IQ samples, float[::1] result, float noise_sqrd, bool qam, for i in range(1, num_samples): real = samples[i, 0] imag = samples[i, 1] - - magnitude = real * real + imag * imag - if magnitude <= noise_sqrd: + + if real * real + imag * imag <= noise_sqrd: result[i] = NOISE_FSK_PSK continue @@ -285,49 +294,50 @@ cdef void phase_demod(IQ samples, float[::1] result, float noise_sqrd, bool qam, imag_float = (imag + shift) / scale current_sample = real_float + imag_unit * imag_float - conj_previous_sample = (samples[i-1, 0] + shift) / scale - imag_unit * ((samples[i-1, 1] + shift) / scale) - f_curr = arg(current_sample * conj_previous_sample) - - if abs(f_curr) < M_PI / 4: # TODO: For PSK with order > 4 this needs to be adapted - f_prev = f_curr - current_arg += f_curr + nco_out = cosf(-costa_phase) + imag_unit * sinf(-costa_phase) + nco_times_sample = nco_out * current_sample + + if loop_order == 2: + costa_error = nco_times_sample.imag * nco_times_sample.real + elif loop_order == 4: + f1 = 1.0 if nco_times_sample.real > 0.0 else -1.0 + f2 = 1.0 if nco_times_sample.imag > 0.0 else -1.0 + costa_error = f1 * nco_times_sample.imag - f2 * nco_times_sample.real else: - current_arg += f_prev + raise NotImplementedError("PSK Demodulation of order {} is currently unsupported".format(loop_order)) - # Reference oscillator cos(current_arg) + j * sin(current_arg) - current_nco = cosf(current_arg) + imag_unit * sinf(current_arg) - phi = arg(current_sample * conj(current_nco)) + costa_error = clamp(costa_error) + + # advance the loop + costa_freq += beta * costa_error + costa_phase += costa_freq + alpha * costa_error + + # wrap the phase + while costa_phase > (2 * M_PI): + costa_phase -= 2 * M_PI + while costa_phase < (-2 * M_PI): + costa_phase += 2 * M_PI + + costa_freq = clamp(costa_freq) + + if loop_order == 2: + result[i] = nco_times_sample.real + elif loop_order == 4: + result[i] = 2 * nco_times_sample.real + nco_times_sample.imag + + return result - if qam: - result[i] = phi * magnitude - else: - result[i] = phi -cpdef np.ndarray[np.float32_t, ndim=1] afp_demod(IQ samples, float noise_mag, str mod_type): +cpdef np.ndarray[np.float32_t, ndim=1] afp_demod(IQ samples, float noise_mag, + str mod_type, int mod_order, float costas_loop_bandwidth=0.1): if len(samples) <= 2: return np.zeros(len(samples), dtype=np.float32) cdef long long i = 0, ns = len(samples) - cdef float current_arg = 0 - cdef float noise_sqrd = 0 - cdef float complex_phase = 0 - cdef float prev_phase = 0 - cdef float NOISE = 0 - cdef float real = 0 - cdef float imag = 0 - - cdef float[::1] result = np.zeros(ns, dtype=np.float32, order="C") - cdef float costa_freq = 0 - cdef float costa_phase = 0 - cdef complex nco_out = 0 + cdef float NOISE = get_noise_for_mod_type(mod_type) + cdef float noise_sqrd = noise_mag * noise_mag, real = 0, imag = 0, magnitude = 0, max_magnitude cdef float complex tmp - cdef float phase_error = 0 - cdef float costa_alpha = 0 - cdef float costa_beta = 0 - cdef complex nco_times_sample = 0 - cdef float magnitude = 0 - cdef float max_magnitude # ensure all magnitudes of ASK demod between 0 and 1 if str(cython.typeof(samples)) == "char[:, ::1]": max_magnitude = sqrt(127*127 + 128*128) elif str(cython.typeof(samples)) == "unsigned char[:, ::1]": @@ -341,35 +351,27 @@ cpdef np.ndarray[np.float32_t, ndim=1] afp_demod(IQ samples, float noise_mag, st else: raise ValueError("Unsupported dtype") - # Atan2 yields values from -Pi to Pi - # We use the Magic Constant NOISE_FSK_PSK to cut off noise - noise_sqrd = noise_mag * noise_mag - NOISE = get_noise_for_mod_type(mod_type) - result[0] = NOISE - - cdef bool qam = False - if mod_type in ("PSK", "QAM", "OQPSK"): - if mod_type == "QAM": - qam = True + if mod_type == "PSK": + return np.asarray(costa_demod(samples, noise_sqrd, mod_order, bandwidth=costas_loop_bandwidth)) - phase_demod(samples, result, noise_sqrd, qam, ns) + cdef float[::1] result = np.zeros(ns, dtype=np.float32, order="C") + result[0] = NOISE - else: - for i in prange(1, ns, nogil=True, schedule="static"): - real = samples[i, 0] - imag = samples[i, 1] - magnitude = real * real + imag * imag - if magnitude <= noise_sqrd: # |c| <= mag_treshold - result[i] = NOISE - continue + for i in prange(1, ns, nogil=True, schedule="static"): + real = samples[i, 0] + imag = samples[i, 1] + magnitude = real * real + imag * imag + if magnitude <= noise_sqrd: # |c| <= mag_treshold + result[i] = NOISE + continue - if mod_type == "ASK": - result[i] = sqrt(magnitude) / max_magnitude - elif mod_type == "FSK": - #tmp = samples[i - 1].conjugate() * c - tmp = (samples[i-1, 0] - imag_unit * samples[i-1, 1]) * (real + imag_unit * imag) - result[i] = atan2(tmp.imag, tmp.real) # Freq + if mod_type == "ASK": + result[i] = sqrt(magnitude) / max_magnitude + elif mod_type == "FSK": + #tmp = samples[i - 1].conjugate() * c + tmp = (samples[i-1, 0] - imag_unit * samples[i-1, 1]) * (real + imag_unit * imag) + result[i] = atan2(tmp.imag, tmp.real) # Freq return np.asarray(result) diff --git a/src/urh/signalprocessing/Signal.py b/src/urh/signalprocessing/Signal.py index 9fcfdfc6c7..d3d5392289 100644 --- a/src/urh/signalprocessing/Signal.py +++ b/src/urh/signalprocessing/Signal.py @@ -43,6 +43,7 @@ def __init__(self, filename: str, name="Signal", modulation: str = None, sample_ self.__samples_per_symbol = 100 self.__pause_threshold = 8 self.__message_length_divisor = 1 + self.__costas_loop_bandwidth = 0.1 self._qad = None self.__center = 0 self._noise_threshold = 0 @@ -255,6 +256,18 @@ def pause_threshold(self, value: int): if not self.block_protocol_update: self.protocol_needs_update.emit() + @property + def costas_loop_bandwidth(self): + return self.__costas_loop_bandwidth + + @costas_loop_bandwidth.setter + def costas_loop_bandwidth(self, value: float): + if self.__costas_loop_bandwidth != value: + self.__costas_loop_bandwidth = value + self._qad = None + if not self.block_protocol_update: + self.protocol_needs_update.emit() + @property def message_length_divisor(self) -> int: return self.__message_length_divisor @@ -362,7 +375,9 @@ def save_as(self, filename: str): QApplication.instance().restoreOverrideCursor() def quad_demod(self): - return signal_functions.afp_demod(self.iq_array.data, self.noise_threshold, self.modulation_type) + return signal_functions.afp_demod(self.iq_array.data, self.noise_threshold, + self.modulation_type, self.modulation_order, + self.costas_loop_bandwidth) def calc_relative_noise_threshold_from_range(self, noise_start: int, noise_end: int): num_digits = 4 @@ -501,7 +516,10 @@ def crop_to_range(self, start: int, end: int): def filter_range(self, start: int, end: int, fir_filter: Filter): self.iq_array[start:end] = fir_filter.work(self.iq_array[start:end]) self._qad[start:end] = signal_functions.afp_demod(self.iq_array[start:end], - self.noise_threshold, self.modulation_type) + self.noise_threshold, + self.modulation_type, + self.modulation_order, + self.costas_loop_bandwidth) self.__invalidate_after_edit() def __invalidate_after_edit(self): diff --git a/src/urh/ui/ui_costa.py b/src/urh/ui/ui_costa.py new file mode 100644 index 0000000000..a5a8a59e18 --- /dev/null +++ b/src/urh/ui/ui_costa.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_DialogCosta(object): + def setupUi(self, DialogCosta): + DialogCosta.setObjectName("DialogCosta") + DialogCosta.resize(400, 300) + self.verticalLayout = QtWidgets.QVBoxLayout(DialogCosta) + self.verticalLayout.setObjectName("verticalLayout") + self.label = QtWidgets.QLabel(DialogCosta) + self.label.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.label.setWordWrap(True) + self.label.setObjectName("label") + self.verticalLayout.addWidget(self.label) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.labelLoopBandwidth = QtWidgets.QLabel(DialogCosta) + self.labelLoopBandwidth.setObjectName("labelLoopBandwidth") + self.horizontalLayout.addWidget(self.labelLoopBandwidth) + self.doubleSpinBoxLoopBandwidth = QtWidgets.QDoubleSpinBox(DialogCosta) + self.doubleSpinBoxLoopBandwidth.setDecimals(4) + self.doubleSpinBoxLoopBandwidth.setObjectName("doubleSpinBoxLoopBandwidth") + self.horizontalLayout.addWidget(self.doubleSpinBoxLoopBandwidth) + self.verticalLayout.addLayout(self.horizontalLayout) + self.buttonBox = QtWidgets.QDialogButtonBox(DialogCosta) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + + self.retranslateUi(DialogCosta) + self.buttonBox.accepted.connect(DialogCosta.accept) + self.buttonBox.rejected.connect(DialogCosta.reject) + + def retranslateUi(self, DialogCosta): + _translate = QtCore.QCoreApplication.translate + DialogCosta.setWindowTitle(_translate("DialogCosta", "Configure Costas Loop")) + self.label.setText(_translate("DialogCosta", "URH uses a Costas loop for PSK demodulation. Configure the loop bandwidth below.")) + self.labelLoopBandwidth.setText(_translate("DialogCosta", "Loop Bandwidth:")) diff --git a/src/urh/util/ProjectManager.py b/src/urh/util/ProjectManager.py index 5a33747ff9..d9d42f5bb9 100644 --- a/src/urh/util/ProjectManager.py +++ b/src/urh/util/ProjectManager.py @@ -330,6 +330,7 @@ def write_signal_information_to_project_file(self, signal: Signal, tree=None): signal_tag.set("pause_threshold", str(signal.pause_threshold)) signal_tag.set("message_length_divisor", str(signal.message_length_divisor)) signal_tag.set("bits_per_symbol", str(signal.bits_per_symbol)) + signal_tag.set("costas_loop_bandwidth", str(signal.costas_loop_bandwidth)) messages = ET.SubElement(signal_tag, "messages") for message in messages: @@ -495,6 +496,7 @@ def read_project_file_for_signal(self, signal: Signal): signal.center_spacing = float(sig_tag.get("center_spacing", 0.1)) signal.tolerance = int(sig_tag.get("tolerance", 5)) signal.bits_per_symbol = int(sig_tag.get("bits_per_symbol", 1)) + signal.costas_loop_bandwidth = float(sig_tag.get("costas_loop_bandwidth", 0.1)) signal.noise_threshold = float(sig_tag.get("noise_threshold", 0.1)) signal.sample_rate = float(sig_tag.get("sample_rate", 1e6)) diff --git a/tests/PlotTests.py b/tests/PlotTests.py index de7081e3dd..ffce688141 100644 --- a/tests/PlotTests.py +++ b/tests/PlotTests.py @@ -34,7 +34,7 @@ def test_plot(self): plt.title("Modulated Wave") plt.subplot(2, 1, 2) - qad = signal_functions.afp_demod(np.ascontiguousarray(data), 0, "FSK") + qad = signal_functions.afp_demod(np.ascontiguousarray(data), 0, "FSK", 2) plt.plot(qad) plt.title("Quad Demod") From 9f8af3c49d58e53a4ad92d44a17b30fd2c51dd40 Mon Sep 17 00:00:00 2001 From: jopohl Date: Sun, 18 Oct 2020 12:22:04 +0200 Subject: [PATCH 02/16] fix tests --- .../test_center_detection.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/auto_interpretation/test_center_detection.py b/tests/auto_interpretation/test_center_detection.py index f310e6263f..35cbcdbd98 100644 --- a/tests/auto_interpretation/test_center_detection.py +++ b/tests/auto_interpretation/test_center_detection.py @@ -26,7 +26,7 @@ def generate_rectangular_signal(bits: str, bit_len: int): def test_noisy_rect(self): data = Signal(get_path_for_data_file("fsk.complex")).iq_array.data - rect = afp_demod(data, 0.008, "FSK")[5:15000] + rect = afp_demod(data, 0.008, "FSK", 2)[5:15000] center = detect_center(rect) self.assertGreaterEqual(center, -0.0587) @@ -34,7 +34,7 @@ def test_noisy_rect(self): def test_ask_center_detection(self): data = Signal(get_path_for_data_file("ask.complex")).iq_array.data - rect = afp_demod(data, 0.01111, "ASK") + rect = afp_demod(data, 0.01111, "ASK", 2) center = detect_center(rect) self.assertGreaterEqual(center, 0) @@ -42,7 +42,7 @@ def test_ask_center_detection(self): def test_enocean_center_detection(self): data = Signal(get_path_for_data_file("enocean.complex")).iq_array.data - rect = afp_demod(data, 0.05, "ASK") + rect = afp_demod(data, 0.05, "ASK", 2) messages = [rect[2107:5432], rect[20428:23758], rect[44216:47546]] for i, msg in enumerate(messages): @@ -54,7 +54,7 @@ def test_ask_50_center_detection(self): message_indices = [(0, 8000), (18000, 26000), (36000, 44000), (54000, 62000), (72000, 80000)] data = Signal(get_path_for_data_file("ask50.complex")).iq_array.data - rect = afp_demod(data, 0.0509, "ASK") + rect = afp_demod(data, 0.0509, "ASK", 2) for start, end in message_indices: center = detect_center(rect[start:end]) @@ -63,7 +63,7 @@ def test_ask_50_center_detection(self): def test_homematic_center_detection(self): data = Signal(get_path_for_data_file("homematic.complex32s"), "").iq_array.data - rect = afp_demod(data, 0.0012, "FSK") + rect = afp_demod(data, 0.0012, "FSK", 2) msg1 = rect[17719:37861] msg2 = rect[70412:99385] @@ -78,7 +78,7 @@ def test_homematic_center_detection(self): def test_noised_homematic_center_detection(self): data = Signal(get_path_for_data_file("noised_homematic.complex"), "").iq_array.data - rect = afp_demod(data, 0.0, "FSK") + rect = afp_demod(data, 0.0, "FSK", 2) center = detect_center(rect) @@ -87,14 +87,14 @@ def test_noised_homematic_center_detection(self): def test_fsk_15db_center_detection(self): data = Signal(get_path_for_data_file("FSK15.complex"), "").iq_array.data - rect = afp_demod(data, 0, "FSK") + rect = afp_demod(data, 0, "FSK", 2) center = detect_center(rect) self.assertGreaterEqual(center, -0.1979) self.assertLessEqual(center, 0.1131) def test_fsk_10db_center_detection(self): data = Signal(get_path_for_data_file("FSK10.complex"), "").iq_array.data - rect = afp_demod(data, 0, "FSK") + rect = afp_demod(data, 0, "FSK", 2) center = detect_center(rect) self.assertGreaterEqual(center, -0.1413) self.assertLessEqual(center, 0.05) @@ -107,12 +107,12 @@ def test_fsk_live_capture(self): filtered_data = moving_average_filter.apply_fir_filter(data.flatten()).view(np.float32) filtered_data = filtered_data.reshape((len(filtered_data)//2, 2)) - rect = afp_demod(filtered_data, 0.0175, "FSK") + rect = afp_demod(filtered_data, 0.0175, "FSK", 2) center = detect_center(rect) self.assertGreaterEqual(center, -0.0148, msg="Filtered") self.assertLessEqual(center, 0.01, msg="Filtered") - rect = afp_demod(data, 0.0175, "FSK") + rect = afp_demod(data, 0.0175, "FSK", 2) center = detect_center(rect) self.assertGreaterEqual(center, -0.02, msg="Original") self.assertLessEqual(center, 0.01, msg="Original") From 34ce30f40f126067ce0721d4ff32b7a8fa0237d4 Mon Sep 17 00:00:00 2001 From: jopohl Date: Sun, 18 Oct 2020 12:31:33 +0200 Subject: [PATCH 03/16] fix test --- tests/test_demodulations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_demodulations.py b/tests/test_demodulations.py index be831384ab..0ad6f55b7f 100644 --- a/tests/test_demodulations.py +++ b/tests/test_demodulations.py @@ -69,7 +69,7 @@ def test_psk(self): signal = Signal(get_path_for_data_file("psk_gen_noisy.complex"), "PSK-Test") signal.modulation_type = "PSK" signal.samples_per_symbol = 300 - signal.center = -1.2886 + signal.center = 0 signal.noise_threshold = 0 signal.tolerance = 10 From a778ab0df4edda5e9b1afe94231c3b4412cd6cce Mon Sep 17 00:00:00 2001 From: Johannes Pohl Date: Sun, 18 Oct 2020 12:41:15 +0200 Subject: [PATCH 04/16] osx: install wheel --- data/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/azure-pipelines.yml b/data/azure-pipelines.yml index 4bb10c6e60..6de0fb15a5 100644 --- a/data/azure-pipelines.yml +++ b/data/azure-pipelines.yml @@ -242,7 +242,7 @@ jobs: - script: | python -m pip install --upgrade pip python -m pip install --upgrade -r data/requirements.txt - python -m pip install --upgrade pytest pytest-faulthandler + python -m pip install --upgrade wheel pytest pytest-faulthandler displayName: 'Install dependencies' - script: | From 74c85d1ca7cb30b0eb96b42397654ff884b31eec Mon Sep 17 00:00:00 2001 From: jopohl Date: Sun, 18 Oct 2020 12:51:18 +0200 Subject: [PATCH 05/16] change order --- data/azure-pipelines.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/data/azure-pipelines.yml b/data/azure-pipelines.yml index 6de0fb15a5..3a0028e5de 100644 --- a/data/azure-pipelines.yml +++ b/data/azure-pipelines.yml @@ -242,14 +242,10 @@ jobs: - script: | python -m pip install --upgrade pip python -m pip install --upgrade -r data/requirements.txt - python -m pip install --upgrade wheel pytest pytest-faulthandler - displayName: 'Install dependencies' - - - script: | - brew install airspy hackrf librtlsdr portaudio uhd - python -m pip install --upgrade wheel twine six appdirs packaging setuptools pyinstaller pyaudio + python -m pip install --upgrade wheel pytest pytest-faulthandler twine six appdirs packaging setuptools pyinstaller pyaudio python -c "import tempfile, os; open(os.path.join(tempfile.gettempdir(), 'urh_releasing'), 'w').close()" - displayName: "Install build dependencies" + brew install airspy hackrf librtlsdr portaudio uhd + displayName: 'Install dependencies' - script: python src/urh/cythonext/build.py displayName: "Build extensions" From d867a97c45f23291558482897f94bdd9003c5b4f Mon Sep 17 00:00:00 2001 From: jopohl Date: Sun, 18 Oct 2020 12:54:01 +0200 Subject: [PATCH 06/16] change order --- data/azure-pipelines.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/data/azure-pipelines.yml b/data/azure-pipelines.yml index 3a0028e5de..dc359fc8eb 100644 --- a/data/azure-pipelines.yml +++ b/data/azure-pipelines.yml @@ -240,11 +240,12 @@ jobs: displayName: "download and unpack SDR drivers" - script: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip wheel setuptools python -m pip install --upgrade -r data/requirements.txt - python -m pip install --upgrade wheel pytest pytest-faulthandler twine six appdirs packaging setuptools pyinstaller pyaudio + python -m pip install --upgrade pytest pytest-faulthandler twine six appdirs packaging pyinstaller python -c "import tempfile, os; open(os.path.join(tempfile.gettempdir(), 'urh_releasing'), 'w').close()" brew install airspy hackrf librtlsdr portaudio uhd + python -m pip install --upgrade pyaudio displayName: 'Install dependencies' - script: python src/urh/cythonext/build.py From 83bbe752fdd2eccb5a48f0ad808988266cd653c5 Mon Sep 17 00:00:00 2001 From: jopohl Date: Sun, 18 Oct 2020 12:58:18 +0200 Subject: [PATCH 07/16] force no install cleanup --- data/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/azure-pipelines.yml b/data/azure-pipelines.yml index dc359fc8eb..9790b0564b 100644 --- a/data/azure-pipelines.yml +++ b/data/azure-pipelines.yml @@ -244,7 +244,7 @@ jobs: python -m pip install --upgrade -r data/requirements.txt python -m pip install --upgrade pytest pytest-faulthandler twine six appdirs packaging pyinstaller python -c "import tempfile, os; open(os.path.join(tempfile.gettempdir(), 'urh_releasing'), 'w').close()" - brew install airspy hackrf librtlsdr portaudio uhd + HOMEBREW_NO_INSTALL_CLEANUP=TRUE brew install airspy hackrf librtlsdr portaudio uhd python -m pip install --upgrade pyaudio displayName: 'Install dependencies' From 367d2ab367715827c71a11dd6cea33b386d099f8 Mon Sep 17 00:00:00 2001 From: jopohl Date: Sun, 18 Oct 2020 13:03:27 +0200 Subject: [PATCH 08/16] cleanup --- data/azure-pipelines.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/data/azure-pipelines.yml b/data/azure-pipelines.yml index 9790b0564b..3d93c00531 100644 --- a/data/azure-pipelines.yml +++ b/data/azure-pipelines.yml @@ -242,10 +242,9 @@ jobs: - script: | python -m pip install --upgrade pip wheel setuptools python -m pip install --upgrade -r data/requirements.txt - python -m pip install --upgrade pytest pytest-faulthandler twine six appdirs packaging pyinstaller - python -c "import tempfile, os; open(os.path.join(tempfile.gettempdir(), 'urh_releasing'), 'w').close()" HOMEBREW_NO_INSTALL_CLEANUP=TRUE brew install airspy hackrf librtlsdr portaudio uhd - python -m pip install --upgrade pyaudio + python -m pip install --upgrade pytest pytest-faulthandler twine six appdirs packaging pyinstaller pyaudio + python -c "import tempfile, os; open(os.path.join(tempfile.gettempdir(), 'urh_releasing'), 'w').close()" displayName: 'Install dependencies' - script: python src/urh/cythonext/build.py From 38e9aa3087f12a4923667527e793cc0824a20535 Mon Sep 17 00:00:00 2001 From: jopohl Date: Sun, 18 Oct 2020 13:08:00 +0200 Subject: [PATCH 09/16] add python39 --- data/azure-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/azure-pipelines.yml b/data/azure-pipelines.yml index 3d93c00531..aee392ddcb 100644 --- a/data/azure-pipelines.yml +++ b/data/azure-pipelines.yml @@ -19,6 +19,8 @@ jobs: python.version: '3.7' Python38: python.version: '3.8' + Python39: + python.version: '3.9' steps: - task: UsePythonVersion@0 From 4cce8b06810c6ae5a3f73bddda6073a5f548e4a8 Mon Sep 17 00:00:00 2001 From: jopohl Date: Tue, 20 Oct 2020 14:17:48 +0200 Subject: [PATCH 10/16] fallback instead of throwing error --- src/urh/cythonext/signal_functions.pyx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/urh/cythonext/signal_functions.pyx b/src/urh/cythonext/signal_functions.pyx index 4f04353b44..e9f4c2ab25 100644 --- a/src/urh/cythonext/signal_functions.pyx +++ b/src/urh/cythonext/signal_functions.pyx @@ -282,6 +282,10 @@ cdef float[::1] costa_demod(IQ samples, float noise_sqrd, int loop_order, float else: raise ValueError("Unsupported dtype") + if loop_order > 4: + # TODO: Adapt this when PSK demodulation with order > 4 shall be supported + loop_order = 4 + for i in range(1, num_samples): real = samples[i, 0] imag = samples[i, 1] @@ -303,8 +307,6 @@ cdef float[::1] costa_demod(IQ samples, float noise_sqrd, int loop_order, float f1 = 1.0 if nco_times_sample.real > 0.0 else -1.0 f2 = 1.0 if nco_times_sample.imag > 0.0 else -1.0 costa_error = f1 * nco_times_sample.imag - f2 * nco_times_sample.real - else: - raise NotImplementedError("PSK Demodulation of order {} is currently unsupported".format(loop_order)) costa_error = clamp(costa_error) From b040b0eeaba68751d53cc0d8d42355334c23765c Mon Sep 17 00:00:00 2001 From: jopohl Date: Tue, 20 Oct 2020 14:30:36 +0200 Subject: [PATCH 11/16] update build script --- src/urh/cythonext/build.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/urh/cythonext/build.py b/src/urh/cythonext/build.py index f68ffc9a61..35494ef3ae 100644 --- a/src/urh/cythonext/build.py +++ b/src/urh/cythonext/build.py @@ -10,8 +10,9 @@ def main(): cur_dir = os.path.realpath(__file__) os.chdir(os.path.realpath(os.path.join(cur_dir, "..", "..", "..", ".."))) # call([sys.executable, "setup.py", "clean", "--all"]) - call([sys.executable, "setup.py", "build_ext", "--inplace", "-j{}".format(os.cpu_count())]) + rc = call([sys.executable, "setup.py", "build_ext", "--inplace", "-j{}".format(os.cpu_count())]) + return rc if __name__ == "__main__": - main() + sys.exit(main()) From 3b8ea2757114814334d52127ce60b48cd80b5716 Mon Sep 17 00:00:00 2001 From: jopohl Date: Wed, 21 Oct 2020 11:51:54 +0200 Subject: [PATCH 12/16] add unittest for 4 psk demod --- tests/test_demodulations.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_demodulations.py b/tests/test_demodulations.py index 0ad6f55b7f..d2c3e69485 100644 --- a/tests/test_demodulations.py +++ b/tests/test_demodulations.py @@ -77,6 +77,37 @@ def test_psk(self): proto_analyzer.get_protocol_from_signal() self.assertTrue(proto_analyzer.plain_bits_str[0].startswith("1011"), msg=proto_analyzer.plain_bits_str[0]) + def test_4_psk(self): + bits = array.array("B", [1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1]) + angles_degree = [-135, -45, 45, 135] + + parameters = array.array("f", [np.pi*a/180 for a in angles_degree]) + result = modulate_c(bits, 100, "PSK", parameters, 2, 1, 40e3, 0, 1e6, 1000, 0) + + signal = Signal("") + signal.iq_array = IQArray(result) + signal.bits_per_symbol = 2 + signal.center = 0 + signal.center_spacing = 1 + signal.modulation_type = "PSK" + + proto_analyzer = ProtocolAnalyzer(signal) + proto_analyzer.get_protocol_from_signal() + demod_bits = proto_analyzer.plain_bits_str[0] + self.assertEqual(len(demod_bits), len(bits)) + self.assertTrue(demod_bits.startswith("10101010")) + + np.random.seed(42) + noised = result + 0.1 * np.random.normal(loc=0, scale=np.sqrt(2)/2, size=(len(result), 2)) + signal.iq_array = IQArray(noised.astype(np.float32)) + signal.center_spacing = 1.5 + signal.noise_threshold = 0.2 + signal._qad = None + proto_analyzer.get_protocol_from_signal() + demod_bits = proto_analyzer.plain_bits_str[0] + self.assertEqual(len(demod_bits), len(bits)) + self.assertTrue(demod_bits.startswith("10101010")) + def test_4_fsk(self): bits = array.array("B", [1, 0, 1, 0, 1, 1, 0, 0, 0, 1]) parameters = array.array("f", [-20e3, -10e3, 10e3, 20e3]) From fb0c353e8abda949be3043d5f254133cbcfcd58f Mon Sep 17 00:00:00 2001 From: jopohl Date: Wed, 21 Oct 2020 11:59:09 +0200 Subject: [PATCH 13/16] use latest windows image --- data/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/azure-pipelines.yml b/data/azure-pipelines.yml index aee392ddcb..6fcf9752b5 100644 --- a/data/azure-pipelines.yml +++ b/data/azure-pipelines.yml @@ -116,7 +116,7 @@ jobs: - job: 'Windows' pool: - vmImage: 'vs2017-win2016' + vmImage: 'windows-latest' strategy: matrix: Python37-64: From 634f0298276ac20827ad3fd63895e7c500ef6fd7 Mon Sep 17 00:00:00 2001 From: jopohl Date: Wed, 21 Oct 2020 12:00:08 +0200 Subject: [PATCH 14/16] use windows 2019 --- data/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/azure-pipelines.yml b/data/azure-pipelines.yml index 6fcf9752b5..ce6c58218a 100644 --- a/data/azure-pipelines.yml +++ b/data/azure-pipelines.yml @@ -116,7 +116,7 @@ jobs: - job: 'Windows' pool: - vmImage: 'windows-latest' + vmImage: 'windows-2019' strategy: matrix: Python37-64: From 5773b11d9283c320473fd54366a68290e3b4fb1b Mon Sep 17 00:00:00 2001 From: jopohl Date: Wed, 21 Oct 2020 12:09:27 +0200 Subject: [PATCH 15/16] use ubuntu 18.04 --- data/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/azure-pipelines.yml b/data/azure-pipelines.yml index ce6c58218a..7ebbe417fa 100644 --- a/data/azure-pipelines.yml +++ b/data/azure-pipelines.yml @@ -8,7 +8,7 @@ variables: jobs: - job: 'Linux' pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'Ubuntu-18.04' strategy: matrix: Python35: From 7c2767a7666f49f86a76dccb8cd97f6e769152d1 Mon Sep 17 00:00:00 2001 From: jopohl Date: Wed, 21 Oct 2020 12:23:36 +0200 Subject: [PATCH 16/16] back to 16.04 --- data/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/azure-pipelines.yml b/data/azure-pipelines.yml index 7ebbe417fa..ce6c58218a 100644 --- a/data/azure-pipelines.yml +++ b/data/azure-pipelines.yml @@ -8,7 +8,7 @@ variables: jobs: - job: 'Linux' pool: - vmImage: 'Ubuntu-18.04' + vmImage: 'Ubuntu-16.04' strategy: matrix: Python35: