diff --git a/physiolabxr/examples/LSLExampleOutlet.py b/physiolabxr/examples/LSLExampleOutlet.py index d1d940a3..44a1872c 100644 --- a/physiolabxr/examples/LSLExampleOutlet.py +++ b/physiolabxr/examples/LSLExampleOutlet.py @@ -16,7 +16,7 @@ def main(argv): name = 'Dummy-8Chan' print('Stream name is ' + name) type = 'EEG' - n_channels = 8 + n_channels = 800 help_string = 'SendData.py -s -n -t ' try: opts, args = getopt.getopt(argv, "hs:c:n:t:", longopts=["srate=", "channels=", "name=", "type"]) diff --git a/physiolabxr/examples/LSLExampleOutlet1.py b/physiolabxr/examples/LSLExampleOutlet1.py deleted file mode 100644 index f1df5611..00000000 --- a/physiolabxr/examples/LSLExampleOutlet1.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Example program to demonstrate how to send a multi-channel time series to -LSL.""" -import random -import sys -import getopt -import string -import numpy as np -import time -from random import random as rand - -from pylsl import StreamInfo, StreamOutlet, local_clock - - -def main(argv): - letters = string.digits - - srate = 128 - name = 'EEG-8Chan' - print('Stream name is ' + name) - type = 'EEG' - n_channels = 12 - help_string = 'SendData.py -s -n -t ' - try: - opts, args = getopt.getopt(argv, "hs:c:n:t:", longopts=["srate=", "channels=", "name=", "type"]) - except getopt.GetoptError: - print(help_string) - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print(help_string) - sys.exit() - elif opt in ("-s", "--srate"): - srate = float(arg) - elif opt in ("-c", "--channels"): - n_channels = int(arg) - elif opt in ("-n", "--name"): - name = arg - elif opt in ("-t", "--type"): - type = arg - - # first create a new stream info (here we set the name to BioSemi, - # the content-type to EEG, 8 channels, 100 Hz, and float-valued data) The - # last value would be the serial number of the device or some other more or - # less locally unique identifier for the stream as far as available (you - # could also omit it but interrupted connections wouldn't auto-recover) - info = StreamInfo(name, type, n_channels, srate, 'float32', 'someuuid1234') - - # next make an outlet - outlet = StreamOutlet(info) - - print("now sending data...") - start_time = local_clock() - sent_samples = 0 - while True: - elapsed_time = local_clock() - start_time - required_samples = int(srate * elapsed_time) - sent_samples - for sample_ix in range(required_samples): - # make a new random n_channels sample; this is converted into a - # pylsl.vectorf (the data type that is expected by push_sample) - mysample = [rand() * 200 for _ in range(n_channels)] - # now send it - mysample[0] = time.time() - mysample[-3] = rand() - mysample[-2] = rand() - mysample[-1] = rand() - - outlet.push_sample(mysample) - sent_samples += required_samples - # now send it and wait for a bit before trying again. - time.sleep(0.01) - - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/physiolabxr/examples/LSLExampleOutletJohn.py b/physiolabxr/examples/LSLExampleOutletJohn.py deleted file mode 100644 index f03196a9..00000000 --- a/physiolabxr/examples/LSLExampleOutletJohn.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Example program to demonstrate how to send a multi-channel time series to -LSL.""" -import random -import sys -import getopt -import string -import numpy as np -import time -from random import random as rand - -from pylsl import StreamInfo, StreamOutlet, local_clock - - -def main(argv): - letters = string.digits - - srate = 128 - name = 'Dummy-8Chan2' - print('Stream name is ' + name) - type = 'EEG' - n_channels = 12 - help_string = 'SendData.py -s -n -t ' - try: - opts, args = getopt.getopt(argv, "hs:c:n:t:", longopts=["srate=", "channels=", "name=", "type"]) - except getopt.GetoptError: - print(help_string) - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print(help_string) - sys.exit() - elif opt in ("-s", "--srate"): - srate = float(arg) - elif opt in ("-c", "--channels"): - n_channels = int(arg) - elif opt in ("-n", "--name"): - name = arg - elif opt in ("-t", "--type"): - type = arg - - # first create a new stream info (here we set the name to BioSemi, - # the content-type to EEG, 8 channels, 100 Hz, and float-valued data) The - # last value would be the serial number of the device or some other more or - # less locally unique identifier for the stream as far as available (you - # could also omit it but interrupted connections wouldn't auto-recover) - info = StreamInfo(name, type, n_channels, srate, 'float32', 'someuuid1234') - - # next make an outlet - outlet = StreamOutlet(info) - - print("now sending data...") - start_time = local_clock() - sent_samples = 0 - while True: - elapsed_time = local_clock() - start_time - required_samples = int(srate * elapsed_time) - sent_samples - for sample_ix in range(required_samples): - # make a new random n_channels sample; this is converted into a - # pylsl.vectorf (the data type that is expected by push_sample) - mysample = [rand()*100 for _ in range(n_channels)] - # now send it - outlet.push_sample(mysample) - sent_samples += required_samples - # now send it and wait for a bit before trying again. - time.sleep(0.01) - - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/physiolabxr/examples/WriteYourOwnDataSourceExamples/LSLExampleOutlet.py b/physiolabxr/examples/WriteYourOwnDataSourceExamples/LSLExampleOutlet.py index 0b6574d7..70bf75c8 100644 --- a/physiolabxr/examples/WriteYourOwnDataSourceExamples/LSLExampleOutlet.py +++ b/physiolabxr/examples/WriteYourOwnDataSourceExamples/LSLExampleOutlet.py @@ -4,7 +4,8 @@ # create a new stream info and outlet -stream_name = 'python_lsl_my_stream_name' +stream_name = 'stream' +stream_type = 'LSL' n_channels = 8 # using the local_clock() to track elapsed time @@ -14,7 +15,7 @@ # set the sampling rate to 100 Hz nominal_sampling_rate = 100 -info = StreamInfo('stream_name', 'my_stream_type', n_channels, nominal_sampling_rate, 'float32', +info = StreamInfo(stream_name, stream_type, n_channels, nominal_sampling_rate, 'float32', 'my_stream_id') outlet = StreamOutlet(info) diff --git a/physiolabxr/ui/BaseStreamWidget.py b/physiolabxr/ui/BaseStreamWidget.py index dbbf3ce5..b4fb7b06 100644 --- a/physiolabxr/ui/BaseStreamWidget.py +++ b/physiolabxr/ui/BaseStreamWidget.py @@ -6,7 +6,7 @@ from PyQt6 import QtWidgets, uic, QtCore from PyQt6.QtCore import QTimer, QThread, QMutex, Qt -from PyQt6.QtGui import QPixmap, QMovie +from PyQt6.QtGui import QPixmap, QMovie, QPainter, QPen from PyQt6.QtWidgets import QDialogButtonBox, QSplitter from physiolabxr.configs import config_ui @@ -134,6 +134,7 @@ def __init__(self, parent_widget, parent_layout, preset_type, stream_name, data_ # mutex for not update the settings while plotting self.setting_update_viz_mutex = QMutex() self.set_pop_button_icons() + self.show_border = False def start_timers(self): self.v_timer.start() @@ -532,4 +533,13 @@ def run_data_processor(self, data_dict): def get_viz_components(self): - return self.viz_components \ No newline at end of file + return self.viz_components + + def paintEvent(self, event): + super().paintEvent(event) + if self.show_border: + painter = QPainter(self) + pen = QPen(Qt.GlobalColor.red, 2) + painter.setPen(pen) + painter.drawRect(self.rect()) + painter.end() \ No newline at end of file diff --git a/physiolabxr/ui/GroupPlotWidget.py b/physiolabxr/ui/GroupPlotWidget.py index 1b675083..df6f461e 100644 --- a/physiolabxr/ui/GroupPlotWidget.py +++ b/physiolabxr/ui/GroupPlotWidget.py @@ -100,18 +100,27 @@ def init_line_chart(self): distinct_colors = get_distinct_colors(len(channel_indices)) self.legends = self.linechart_widget.addLegend() # self.linechart_widget.enableAutoRange(enable=False) - for channel_index_in_group, (channel_index, channel_name) in enumerate( - zip(channel_indices, self.channel_names)): - is_channel_shown = is_channels_shown[channel_index_in_group] - channel_plot_item = self.linechart_widget.plot([], [], pen=pg.mkPen(color=distinct_colors[channel_index_in_group]), name=channel_name) - self.channel_index_channel_dict[int(channel_index)] = channel_plot_item - if not is_channel_shown: - channel_plot_item.hide() # TODO does disable do what it should do: uncheck from the plots - downsample_method = 'mean' if self.sampling_rate > AppConfigs().downsample_method_mean_sr_threshold else 'subsample' - channel_plot_item.setDownsampling(auto=True, method=downsample_method) - channel_plot_item.setClipToView(True) - channel_plot_item.setSkipFiniteCheck(True) - self.channel_plot_item_dict[channel_name] = channel_plot_item + pens = [] + names = [] + for channel_index_in_group, (channel_index, channel_name) in enumerate(zip(channel_indices, self.channel_names)): + # is_channel_shown = is_channels_shown[channel_index_in_group] + pens.append(pg.mkPen(color=distinct_colors[channel_index_in_group])) + names.append(channel_name) + # channel_plot_item = self.linechart_widget.plot([], [], pen=pg.mkPen(color=distinct_colors[channel_index_in_group]), name=channel_name) + # self.channel_index_channel_dict[int(channel_index)] = channel_plot_item + # if not is_channel_shown: + # channel_plot_item.hide() # TODO does disable do what it should do: uncheck from the plots + # downsample_method = 'mean' if self.sampling_rate > AppConfigs().downsample_method_mean_sr_threshold else 'subsample' + # channel_plot_item.setDownsampling(auto=True, method=downsample_method) + # channel_plot_item.setClipToView(True) + # channel_plot_item.setSkipFiniteCheck(True) + # self.channel_plot_item_dict[channel_name] = channel_plot_item + group_plot_item = self.linechart_widget.plot([], [], pen=pens, name=names) + downsample_method = 'mean' if self.sampling_rate > AppConfigs().downsample_method_mean_sr_threshold else 'subsample' + group_plot_item.setDownsampling(auto=True, method=downsample_method) + # channel_plot_item.setClipToView(True) + group_plot_item.setSkipFiniteCheck(True) + # self.channel_plot_item_dict[channel_name] = channel_plot_item def init_image(self): self.plot_widget = pg.PlotWidget() @@ -193,12 +202,17 @@ def plot_data(self, data): # if line_chat_config.channels_constant_offset!=0: # data = data + - time_vector = np.linspace(0., duration, data.shape[1]) - for index_in_group, channel_index in enumerate(channel_indices): - plot_data_item = self.linechart_widget.plotItem.curves[index_in_group] - if plot_data_item.isVisible(): - plot_data_item.setData(time_vector, data[channel_index, :]+linechart_config.channels_constant_offset*index_in_group) + + y_vals = data[channel_indices] + channel_offsets = np.arange(y_vals.shape[0]) * linechart_config.channels_constant_offset + y_vals = y_vals + channel_offsets.reshape(-1, 1) + self.linechart_widget.plotItem.curves[0].setData(time_vector, y_vals) + # plot_data_item = self.linechart_widget.plotItem.curves[index_in_group] + # if plot_data_item.isVisible(): + # print('plotting channel', channel_index, 'in group', self.group_name) + # print(time_vector.shape, data[channel_index, :].shape) + # plot_data_item.setData(time_vector, data[channel_index, :]+linechart_config.channels_constant_offset*index_in_group) elif selected_plot_format == 1 and get_group_image_valid(self.stream_name, self.group_name): image_config = get_group_image_config(self.stream_name, self.group_name) @@ -277,9 +291,8 @@ def change_group_name(self, new_group_name): def change_channel_name(self, new_ch_name, old_ch_name, lsl_index): # change_plot_label(self.linechart_widget, self.channel_plot_item_dict[old_ch_name], new_ch_name) - self.channel_plot_item_dict[old_ch_name].setData(name=new_ch_name) - self.channel_plot_item_dict[new_ch_name] = self.channel_plot_item_dict.pop(old_ch_name) - + # self.channel_plot_item_dict[old_ch_name].setData(name=new_ch_name) + # self.channel_plot_item_dict[new_ch_name] = self.channel_plot_item_dict.pop(old_ch_name) # self.channel_plot_item_dict[old_ch_name].legend.setText(new_ch_name) channel_indices = get_group_channel_indices(self.stream_name, self.group_name) index_in_group = channel_indices.index(lsl_index) diff --git a/physiolabxr/ui/MainWindow.py b/physiolabxr/ui/MainWindow.py index a483b689..f54ebd60 100644 --- a/physiolabxr/ui/MainWindow.py +++ b/physiolabxr/ui/MainWindow.py @@ -4,7 +4,8 @@ from typing import Dict from PyQt6 import QtWidgets, uic -from PyQt6.QtCore import QTimer +from PyQt6.QtCore import QTimer, Qt +from PyQt6.QtGui import QKeyEvent, QKeySequence, QShortcutEvent from PyQt6.QtWidgets import QMessageBox, QDialogButtonBox from physiolabxr.configs.GlobalSignals import GlobalSignals @@ -38,7 +39,7 @@ from physiolabxr.ui.SettingsWidget import SettingsWidget from physiolabxr.ui.ReplayTab import ReplayTab from physiolabxr.utils.buffers import DataBuffer -from physiolabxr.utils.ui_utils import another_window +from physiolabxr.utils.ui_utils import another_window, ShortCutType from physiolabxr.ui.dialogs import dialog_popup import numpy as np @@ -145,6 +146,16 @@ def __init__(self, app, ask_to_close=True, *args, **kwargs): # notification pane self.notification_panel = NotificationPane(self) + # shortcut setup + QtWidgets.QApplication.instance().installEventFilter(self) + self.shortcut_ids = { + self.grabShortcut(QKeySequence('Alt+Tab'), context=Qt.ShortcutContext.ApplicationShortcut): ShortCutType.switch, + self.grabShortcut(QKeySequence('Alt+D'), context=Qt.ShortcutContext.ApplicationShortcut): ShortCutType.delete, + self.grabShortcut(QKeySequence('Alt+S'), context=Qt.ShortcutContext.ApplicationShortcut): ShortCutType.start, + self.grabShortcut(QKeySequence('Alt+P'), context=Qt.ShortcutContext.ApplicationShortcut): ShortCutType.pop + } + + # # fmri widget # # TODO: FMRI WIDGET @@ -503,4 +514,46 @@ def resizeEvent(self, a0): self.adjust_notification_panel_location() def adjust_notification_panel_location(self): - self.notification_panel.move(self.width() - self.notification_panel.width() - 9, self.height() - self.notification_panel.height() - self.recording_file_size_label.height() - 12) # substract 64 to account for margin \ No newline at end of file + self.notification_panel.move(self.width() - self.notification_panel.width() - 9, self.height() - self.notification_panel.height() - self.recording_file_size_label.height() - 12) # substract 64 to account for margin + + def eventFilter(self, source, event): + if event.type() == QKeyEvent.Type.KeyRelease: + if event.key() == Qt.Key.Key_Alt: + for widget in self.stream_widgets.values(): + if widget.show_border: + widget.show_border = False + widget.update() + return True + return super(MainWindow, self).eventFilter(source, event) + + def event(self, event): + if isinstance(event, QShortcutEvent): + shortcut = self.shortcut_ids.get(event.shortcutId()) + widgests = list(self.stream_widgets.values())[::-1] + index = next((i for i, widget in enumerate(widgests) if widget.hasFocus()), -1) + match shortcut: + case ShortCutType.switch: + widgests[index].show_border = False + widgests[index].update() + index = (index+1)%len(widgests) + if widgests[index].is_popped: + widgests[index].activateWindow() + else: + self.activateWindow() + widgests[index].show_border = True + widgests[index].update() + widgests[index].setFocus() + case ShortCutType.delete: + if widgests[index].hasFocus(): + widgests[index].try_close() + case ShortCutType.start: + if widgests[index].hasFocus(): + widgests[index].start_stop_stream_btn_clicked() + case ShortCutType.pop: + if widgests[index].hasFocus(): + if widgests[index].is_popped: + widgests[index].dock_window() + else: + widgests[index].pop_window() + widgests[index].setFocus() + return super().event(event) \ No newline at end of file diff --git a/physiolabxr/utils/ui_utils.py b/physiolabxr/utils/ui_utils.py index a3877b68..19c458f4 100644 --- a/physiolabxr/utils/ui_utils.py +++ b/physiolabxr/utils/ui_utils.py @@ -314,6 +314,11 @@ def set_back(): line_edit.textChanged.disconnect(set_back) line_edit.textChanged.connect(set_back) raise RenaError(f'{name} must be an integer') +class ShortCutType(Enum): + switch = 1 + delete = 2 + start = 3 + pop = 4 # def add_items(combobox: QComboBox, items: Iterable): # """ @@ -326,7 +331,4 @@ def set_back(): # combobox.addItems(items) # # remove the placeholder item from the combobox if it exists # if placeholder_index != -1: -# combobox.removeItem(placeholder_index) - - - +# combobox.removeItem(placeholder_index) \ No newline at end of file diff --git a/requirements.dev.txt b/requirements.dev.txt index 34d681de..390ae46f 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -5,7 +5,7 @@ pyserial pylsl scikit-learn scipy~=1.11.0 -pyqtgraph +git+https://github.com/wutwasthat/pyqtgraph.git@physio pyxdf pyscreeze opencv-python @@ -19,3 +19,4 @@ PyOpenGL_accelerate soundfile matplotlib imblearn +toml diff --git a/requirements.txt b/requirements.txt index 86ea6f34..b926a504 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pyserial pylsl scikit-learn scipy~=1.11.0 -pyqtgraph +git+https://github.com/wutwasthat/pyqtgraph.git@physio pyxdf pyscreeze opencv-python @@ -18,3 +18,4 @@ PyOpenGL_accelerate soundfile matplotlib imblearn +toml diff --git a/tests/RenaVisualizationTest.py b/tests/VisualizationTest.py similarity index 100% rename from tests/RenaVisualizationTest.py rename to tests/VisualizationTest.py