diff --git a/setup.py b/setup.py index 1a37e8a7..91d5a49a 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ COMPILER_DIRECTIVES = {'language_level': 3, } UI_SUBDIRS = ("actions", "delegates", "views") -PLUGINS = ("AmbleAnalyzer", "MessageBreak", "ZeroHide", "NetworkSDRInterface") +PLUGINS = ("AmbleAnalyzer", "MessageBreak", "ZeroHide", "NetworkSDRInterface", "InsertSine") URH_DIR = "urh" try: diff --git a/src/urh/plugins/InsertSine/InsertSinePlugin.py b/src/urh/plugins/InsertSine/InsertSinePlugin.py new file mode 100644 index 00000000..78a027cf --- /dev/null +++ b/src/urh/plugins/InsertSine/InsertSinePlugin.py @@ -0,0 +1,96 @@ +import os + +import numpy as np +from PyQt5 import uic +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QDialog + +from urh.plugins.Plugin import SignalEditorPlugin +from urh.util.Formatter import Formatter + + +class InsertSinePlugin(SignalEditorPlugin): + insert_sine_wave_clicked = pyqtSignal() + + def __init__(self): + dirname = os.path.dirname(os.readlink(__file__)) if os.path.islink(__file__) else os.path.dirname(__file__) + self.dialog_ui = uic.loadUi(os.path.realpath(os.path.join(dirname, "insert_sine_dialog.ui"))) # type: QDialog + + self.complex_wave = None + self.amplitude = 0.5 + self.frequency = 10 + self.phase = 0 + self.sample_rate = 1e6 + self.num_samples = 1e6 + + self.dialog_ui.doubleSpinBoxAmplitude.setValue(self.amplitude) + self.dialog_ui.doubleSpinBoxFrequency.setValue(self.frequency) + self.dialog_ui.doubleSpinBoxPhase.setValue(self.phase) + self.dialog_ui.doubleSpinBoxSampleRate.setValue(self.sample_rate) + self.dialog_ui.doubleSpinBoxNSamples.setValue(self.num_samples) + self.dialog_ui.labelTime.setText(Formatter.science_time(self.num_samples/self.sample_rate, decimals=3)) + + super().__init__(name="InsertSine") + + def create_connects(self): + pass + + def __create_dialog_connects(self): + self.dialog_ui.doubleSpinBoxAmplitude.valueChanged.connect(self.on_double_spin_box_amplitude_value_changed) + self.dialog_ui.doubleSpinBoxFrequency.valueChanged.connect(self.on_double_spin_box_frequency_value_changed) + self.dialog_ui.doubleSpinBoxPhase.valueChanged.connect(self.on_double_spin_box_phase_value_changed) + self.dialog_ui.doubleSpinBoxSampleRate.valueChanged.connect(self.on_double_spin_box_sample_rate_value_changed) + self.dialog_ui.doubleSpinBoxNSamples.valueChanged.connect(self.on_spin_box_n_samples_value_changed) + self.dialog_ui.btnAbort.clicked.connect(self.on_btn_abort_clicked) + self.dialog_ui.btnOK.clicked.connect(self.on_btn_ok_clicked) + + def show_insert_sine_dialog(self): + self.dialog_ui.show() + self.__create_dialog_connects() + self.draw_sine_wave() + + def draw_sine_wave(self): + self.complex_wave = np.empty(int(self.num_samples), dtype=np.complex64) + t = (np.arange(0, self.num_samples) / self.sample_rate) + self.complex_wave.real = self.amplitude * np.cos(2 * np.pi * self.frequency * t + self.phase) + self.complex_wave.imag = self.amplitude * np.sin(2 * np.pi * self.frequency * t + self.phase) + + self.dialog_ui.graphicsViewSineWave.plot_data(self.complex_wave.imag.astype(np.float32)) + self.dialog_ui.graphicsViewSineWave.draw_full() + + @pyqtSlot(float) + def on_double_spin_box_amplitude_value_changed(self, value: float): + self.amplitude = value + self.draw_sine_wave() + + @pyqtSlot(float) + def on_double_spin_box_frequency_value_changed(self, value: float): + self.frequency = value + self.draw_sine_wave() + + @pyqtSlot(float) + def on_double_spin_box_phase_value_changed(self, value: float): + self.phase = value + self.draw_sine_wave() + + @pyqtSlot(float) + def on_double_spin_box_sample_rate_value_changed(self, value: float): + self.sample_rate = value + self.dialog_ui.labelTime.setText(Formatter.science_time(self.num_samples / self.sample_rate, decimals=3)) + self.draw_sine_wave() + + @pyqtSlot(float) + def on_spin_box_n_samples_value_changed(self, value: float): + self.num_samples = int(value) + self.dialog_ui.labelTime.setText(Formatter.science_time(self.num_samples / self.sample_rate, decimals=3)) + self.draw_sine_wave() + + @pyqtSlot() + def on_btn_abort_clicked(self): + self.dialog_ui.close() + + @pyqtSlot() + def on_btn_ok_clicked(self): + self.insert_sine_wave_clicked.emit() + self.dialog_ui.close() \ No newline at end of file diff --git a/src/urh/plugins/InsertSine/__init__.py b/src/urh/plugins/InsertSine/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/urh/plugins/InsertSine/descr.txt b/src/urh/plugins/InsertSine/descr.txt new file mode 100644 index 00000000..bf2ab2ca --- /dev/null +++ b/src/urh/plugins/InsertSine/descr.txt @@ -0,0 +1,3 @@ +This plugin enables you to insert custom sine waves into your signal. +You will find a new context menu entry in interpretation signal view. +Transform URH into a full fledged signal editor! \ No newline at end of file diff --git a/src/urh/plugins/InsertSine/insert_sine_dialog.ui b/src/urh/plugins/InsertSine/insert_sine_dialog.ui new file mode 100644 index 00000000..baf47e82 --- /dev/null +++ b/src/urh/plugins/InsertSine/insert_sine_dialog.ui @@ -0,0 +1,200 @@ + + + DialogCustomSine + + + + 0 + 0 + 601 + 308 + + + + Insert sine wave + + + + + + Qt::Horizontal + + + + + + + Sample Rate: + + + + + + + 3 + + + 0.000000000000000 + + + 9999999999999999455752309870428160.000000000000000 + + + + + + + ° + + + 3 + + + 360.000000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + 3 + + + 1.000000000000000 + + + 0.001000000000000 + + + 1.000000000000000 + + + + + + + Frequency (Hz): + + + + + + + 3 + + + 0.000000000000000 + + + 9999999999999999931398190359470212947659194368.000000000000000 + + + + + + + Phase: + + + + + + + 3 + + + 0.000000000000000 + + + 99999999999999991433150857216.000000000000000 + + + + + + + Samples: + + + + + + + Amplitude: + + + + + + + Qt::Horizontal + + + + + + + Time: + + + + + + + 20s + + + + + + + Ok + + + + + + + Abort + + + + + + + + QPainter::Antialiasing|QPainter::HighQualityAntialiasing|QPainter::TextAntialiasing + + + + + + + + + ZoomableGraphicView + QGraphicsView +
urh.ui.views.ZoomableGraphicView.h
+
+ + KillerDoubleSpinBox + QDoubleSpinBox +
urh.ui.KillerDoubleSpinBox.h
+
+
+ + +
diff --git a/src/urh/plugins/InsertSine/settings.ui b/src/urh/plugins/InsertSine/settings.ui new file mode 100644 index 00000000..d47e3378 --- /dev/null +++ b/src/urh/plugins/InsertSine/settings.ui @@ -0,0 +1,47 @@ + + + InsertSineWaveSettings + + + + 0 + 0 + 400 + 300 + + + + Frame + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + No settings available. + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/urh/plugins/NetworkSDRInterface/NetworkSDRInterfacePlugin.py b/src/urh/plugins/NetworkSDRInterface/NetworkSDRInterfacePlugin.py index e61d1191..246817d1 100644 --- a/src/urh/plugins/NetworkSDRInterface/NetworkSDRInterfacePlugin.py +++ b/src/urh/plugins/NetworkSDRInterface/NetworkSDRInterfacePlugin.py @@ -2,10 +2,9 @@ import socketserver import threading import time -from PyQt5.QtCore import QRegExp, pyqtSlot +from PyQt5.QtCore import pyqtSlot from PyQt5.QtCore import QTimer from PyQt5.QtCore import pyqtSignal -from PyQt5.QtGui import QRegExpValidator import socket from urh.plugins.Plugin import SDRPlugin diff --git a/src/urh/plugins/Plugin.py b/src/urh/plugins/Plugin.py index 0c2f95d1..72b87a12 100644 --- a/src/urh/plugins/Plugin.py +++ b/src/urh/plugins/Plugin.py @@ -70,4 +70,10 @@ class LabelAssignPlugin(Plugin): class SDRPlugin(Plugin): def __init__(self, name: str): - Plugin.__init__(self, name) \ No newline at end of file + Plugin.__init__(self, name) + + +class SignalEditorPlugin(Plugin): + def __init__(self, name: str): + Plugin.__init__(self, name) + diff --git a/src/urh/signalprocessing/Signal.py b/src/urh/signalprocessing/Signal.py index 92738d15..83f9f8f6 100644 --- a/src/urh/signalprocessing/Signal.py +++ b/src/urh/signalprocessing/Signal.py @@ -378,3 +378,4 @@ class Signal(QObject): self._num_samples = len(self.data) self.data_edited.emit() self.protocol_needs_update.emit() + self.changed = True diff --git a/src/urh/ui/views/EpicGraphicView.py b/src/urh/ui/views/EpicGraphicView.py index 8272a000..b35a94e6 100644 --- a/src/urh/ui/views/EpicGraphicView.py +++ b/src/urh/ui/views/EpicGraphicView.py @@ -5,6 +5,8 @@ from PyQt5.QtWidgets import QAction from PyQt5.QtWidgets import QApplication, QMenu, QActionGroup from urh import constants +from urh.plugins.InsertSine.InsertSinePlugin import InsertSinePlugin +from urh.plugins.PluginManager import PluginManager from urh.ui.ROI import ROI from urh.ui.views.SelectableGraphicView import SelectableGraphicView @@ -66,6 +68,16 @@ class EpicGraphicView(SelectableGraphicView): self.delete_action.triggered.connect(self.on_delete_action_triggered) self.addAction(self.delete_action) + self.insert_sine_action = QAction(self.tr("Insert sine wave...")) + font = self.insert_sine_action.font() + font.setBold(True) + self.insert_sine_action.setFont(font) + self.insert_sine_action.triggered.connect(self.on_insert_sine_action_triggered) + + self.insert_sine_plugin = InsertSinePlugin() + self.insert_sine_plugin.insert_sine_wave_clicked.connect(self.on_insert_sine_wave_clicked) + + @property def signal(self): return self.parent_frame.signal @@ -118,6 +130,7 @@ class EpicGraphicView(SelectableGraphicView): menu = QMenu(self) menu.addAction(self.save_action) menu.addAction(self.save_as_action) + menu.addSeparator() zoom_action = None create_action = None @@ -131,6 +144,11 @@ class EpicGraphicView(SelectableGraphicView): self.paste_action.setEnabled(self.stored_item is not None) menu.addSeparator() + if PluginManager().is_plugin_enabled("InsertSine"): + menu.addAction(self.insert_sine_action) + if not self.selection_area.is_empty: + menu.addSeparator() + #autorangeAction = menu.addAction(self.tr("Show Autocrop Range")) if not self.selection_area.is_empty: @@ -353,3 +371,11 @@ class EpicGraphicView(SelectableGraphicView): def on_save_as_action_triggered(self): self.save_as_clicked.emit() + @pyqtSlot() + def on_insert_sine_action_triggered(self): + self.insert_sine_plugin.show_insert_sine_dialog() + + @pyqtSlot() + def on_insert_sine_wave_clicked(self): + if self.insert_sine_plugin.complex_wave is not None: + self.signal.insert_data(self.paste_position, self.insert_sine_plugin.complex_wave) diff --git a/src/urh/ui/views/ZoomableGraphicView.py b/src/urh/ui/views/ZoomableGraphicView.py index cb281127..4ee073f3 100644 --- a/src/urh/ui/views/ZoomableGraphicView.py +++ b/src/urh/ui/views/ZoomableGraphicView.py @@ -1,4 +1,5 @@ from PyQt5.QtCore import pyqtSignal, QRectF +from PyQt5.QtGui import QResizeEvent from PyQt5.QtGui import QWheelEvent from PyQt5.QtWidgets import QGraphicsScene @@ -71,4 +72,8 @@ class ZoomableGraphicView(SelectableGraphicView): if self.scene_creator is not None: x1 = self.view_rect().x() x2 = x1 + self.view_rect().width() - self.scene_creator.show_scene_section(x1, x2) \ No newline at end of file + self.scene_creator.show_scene_section(x1, x2) + + def resizeEvent(self, event: QResizeEvent): + self.draw_full() + event.accept() \ No newline at end of file diff --git a/src/urh/util/Formatter.py b/src/urh/util/Formatter.py index 04434e2a..6ebd92c2 100644 --- a/src/urh/util/Formatter.py +++ b/src/urh/util/Formatter.py @@ -12,7 +12,7 @@ class Formatter: @staticmethod - def science_time(time_in_seconds: float) -> str: + def science_time(time_in_seconds: float, decimals=2) -> str: if time_in_seconds < 1e-6: suffix = "n" value = time_in_seconds * 1e9 @@ -26,7 +26,7 @@ class Formatter: suffix = "" value = time_in_seconds - return locale.format_string("%.2f " + suffix, value) + "s" + return locale.format_string("%.{0}f ".format(decimals) + suffix, value) + "s" @staticmethod def big_value_with_suffix(value: float, decimals=3) -> str: @@ -47,4 +47,4 @@ class Formatter: return dtype(str_val) except (ValueError, TypeError): logger.warning("The {0} is not a valid {1}, assuming {2}".format(str_val, str(dtype), str(default))) - return default \ No newline at end of file + return default