From ff95e94fd91e5ab6885b6c663a408c27fac3b064 Mon Sep 17 00:00:00 2001 From: Gilbert Montague Date: Thu, 17 Jul 2025 18:34:25 -0700 Subject: [PATCH 1/4] feature: make live plotting better for gpio --- synapse/cli/offline_hdf5_plotter.py | 78 ++++++++++++++++++++++++++++- synapse/cli/synapse_plotter.py | 61 +++++++++++++++++++++- 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/synapse/cli/offline_hdf5_plotter.py b/synapse/cli/offline_hdf5_plotter.py index ca6a552..939e0ed 100644 --- a/synapse/cli/offline_hdf5_plotter.py +++ b/synapse/cli/offline_hdf5_plotter.py @@ -48,6 +48,28 @@ def filter_channels(self, channel_ids: List[int]) -> "PlotData": channel_ids=channel_ids, ) + def save_channel_to_csv(self, channel_id: int, filename: str) -> bool: + """Save a specific channel's data (timestamp, sample) to CSV""" + try: + # Get the index of the channel_id in channel_ids list + channel_index = self.channel_ids.index(channel_id) + + # Get timestamp and sample data + timestamps = self.time_array + samples = self.data.iloc[:, channel_index].to_numpy() + + # Create DataFrame for export + export_df = pd.DataFrame( + {"timestamp_s": timestamps, "sample_value": samples} + ) + + # Save to CSV + export_df.to_csv(filename, index=False) + return True + + except (ValueError, Exception): + return False + def compute_fft(data, sample_rate): # Apply window function to reduce spectral leakage @@ -369,9 +391,63 @@ def update_single_channel(channel_id): ) combo.setFixedWidth(100) + # Function to save current channel to CSV + def save_channel_csv(): + current_channel_id = int(combo.currentText()) + + # Open file dialog + options = QtWidgets.QFileDialog.Options() + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + main_widget, + f"Save Channel {current_channel_id} Data", + f"channel_{current_channel_id}_data.csv", + "CSV Files (*.csv);;All Files (*)", + options=options, + ) + + if filename: + success = plot_data.save_channel_to_csv(current_channel_id, filename) + if success: + console.print( + f"[green]✓ Channel {current_channel_id} data saved to {filename}[/green]" + ) + # Show success message in GUI + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Information) + msg.setWindowTitle("Export Successful") + msg.setText( + f"Channel {current_channel_id} data successfully saved to:\n{filename}" + ) + msg.exec_() + else: + console.print( + f"[red]✗ Failed to save channel {current_channel_id} data[/red]" + ) + # Show error message in GUI + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Critical) + msg.setWindowTitle("Export Failed") + msg.setText(f"Failed to save channel {current_channel_id} data") + msg.exec_() + + # Create save button + save_button = QtWidgets.QPushButton("Save Channel to CSV") + save_button.clicked.connect(save_channel_csv) + save_button.setFixedWidth(150) + + # Create a horizontal layout for controls + controls_layout = QtWidgets.QHBoxLayout() + controls_layout.addWidget(QtWidgets.QLabel("Channel:")) + controls_layout.addWidget(combo) + controls_layout.addWidget(save_button) + controls_layout.addStretch() # Add stretch to push everything to the left + + controls_widget = QtWidgets.QWidget() + controls_widget.setLayout(controls_layout) + # Create a layout for our plot, fft, and controls main_layout = QtWidgets.QVBoxLayout() - main_layout.addWidget(combo) + main_layout.addWidget(controls_widget) main_layout.addWidget(main_splitter) # And finally our main widget to show diff --git a/synapse/cli/synapse_plotter.py b/synapse/cli/synapse_plotter.py index 263db73..e99a6d5 100644 --- a/synapse/cli/synapse_plotter.py +++ b/synapse/cli/synapse_plotter.py @@ -48,6 +48,14 @@ def __init__(self, sample_rate: int, window_size: int, channel_ids): self.zoom_y_min = -1000 self.zoom_y_max = 1000 + # DC offset removal toggle for zoomed channel + self.remove_dc_offset = True + + # Binary range toggle state and stored previous range + self.is_binary_range = False + self.stored_y_min = -1000 + self.stored_y_max = 1000 + self.signal_separation = 1000 # Dictionary to store line series for plotted channels @@ -135,6 +143,23 @@ def setup_gui(self): tag="zoom_y_max_input", ) + dpg.add_separator() + + # DC Offset Removal Toggle + dpg.add_checkbox( + label="Remove DC Offset", + default_value=self.remove_dc_offset, + callback=self.toggle_remove_dc_offset, + tag="remove_dc_offset_checkbox", + ) + + # Quick Y-axis range button for binary signals + dpg.add_button( + label="Binary Range (-1 to 3)", + callback=self.set_binary_range, + tag="set_binary_range_button", + ) + # ----------------------------- # Main Data Window (plots) # ----------------------------- @@ -236,13 +261,45 @@ def remove_line_series(self, ch_id): def set_zoom_y_min(self, sender, app_data): self.zoom_y_min = app_data + # If not in binary range mode, update stored range + if not self.is_binary_range: + self.stored_y_min = app_data def set_zoom_y_max(self, sender, app_data): self.zoom_y_max = app_data + # If not in binary range mode, update stored range + if not self.is_binary_range: + self.stored_y_max = app_data def set_signal_separation(self, sender, app_data): self.signal_separation = app_data + def toggle_remove_dc_offset(self, sender, app_data): + self.remove_dc_offset = app_data + + def set_binary_range(self, sender, app_data): + if self.is_binary_range: + # Currently in binary range, switch back to stored range + self.zoom_y_min = self.stored_y_min + self.zoom_y_max = self.stored_y_max + self.is_binary_range = False + button_text = "Binary Range (-1 to 3)" + else: + # Currently in normal range, switch to binary range + self.stored_y_min = self.zoom_y_min + self.stored_y_max = self.zoom_y_max + self.zoom_y_min = -1 + self.zoom_y_max = 3 + self.is_binary_range = True + button_text = ( + f"Restore Range ({self.stored_y_min:.0f} to {self.stored_y_max:.0f})" + ) + + # Update the GUI input fields and button text + dpg.set_value("zoom_y_min_input", self.zoom_y_min) + dpg.set_value("zoom_y_max_input", self.zoom_y_max) + dpg.set_item_label("set_binary_range_button", button_text) + def put(self, frame: BroadbandFrame): """Add a BroadbandFrame to the processing queue""" try: @@ -394,8 +451,8 @@ def update_plot(self): ds_x_ch = rolled_x_ch[::ds_factor] ds_y_ch = rolled_y_ch[::ds_factor] - # Remove DC offset by subtracting the mean - if len(ds_y_ch) > 0: + # Remove DC offset by subtracting the mean if enabled + if self.remove_dc_offset and len(ds_y_ch) > 0: ds_y_ch = ds_y_ch - np.mean(ds_y_ch) # Update the single "zoomed_line" series From 3e3dba95ef7eefa500a1f8b77b57ac2fe424f6f5 Mon Sep 17 00:00:00 2001 From: Gilbert Montague Date: Thu, 17 Jul 2025 18:36:19 -0700 Subject: [PATCH 2/4] Revert save csv --- synapse/cli/offline_hdf5_plotter.py | 67 ----------------------------- 1 file changed, 67 deletions(-) diff --git a/synapse/cli/offline_hdf5_plotter.py b/synapse/cli/offline_hdf5_plotter.py index 939e0ed..428158d 100644 --- a/synapse/cli/offline_hdf5_plotter.py +++ b/synapse/cli/offline_hdf5_plotter.py @@ -48,28 +48,6 @@ def filter_channels(self, channel_ids: List[int]) -> "PlotData": channel_ids=channel_ids, ) - def save_channel_to_csv(self, channel_id: int, filename: str) -> bool: - """Save a specific channel's data (timestamp, sample) to CSV""" - try: - # Get the index of the channel_id in channel_ids list - channel_index = self.channel_ids.index(channel_id) - - # Get timestamp and sample data - timestamps = self.time_array - samples = self.data.iloc[:, channel_index].to_numpy() - - # Create DataFrame for export - export_df = pd.DataFrame( - {"timestamp_s": timestamps, "sample_value": samples} - ) - - # Save to CSV - export_df.to_csv(filename, index=False) - return True - - except (ValueError, Exception): - return False - def compute_fft(data, sample_rate): # Apply window function to reduce spectral leakage @@ -391,55 +369,10 @@ def update_single_channel(channel_id): ) combo.setFixedWidth(100) - # Function to save current channel to CSV - def save_channel_csv(): - current_channel_id = int(combo.currentText()) - - # Open file dialog - options = QtWidgets.QFileDialog.Options() - filename, _ = QtWidgets.QFileDialog.getSaveFileName( - main_widget, - f"Save Channel {current_channel_id} Data", - f"channel_{current_channel_id}_data.csv", - "CSV Files (*.csv);;All Files (*)", - options=options, - ) - - if filename: - success = plot_data.save_channel_to_csv(current_channel_id, filename) - if success: - console.print( - f"[green]✓ Channel {current_channel_id} data saved to {filename}[/green]" - ) - # Show success message in GUI - msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Information) - msg.setWindowTitle("Export Successful") - msg.setText( - f"Channel {current_channel_id} data successfully saved to:\n{filename}" - ) - msg.exec_() - else: - console.print( - f"[red]✗ Failed to save channel {current_channel_id} data[/red]" - ) - # Show error message in GUI - msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Critical) - msg.setWindowTitle("Export Failed") - msg.setText(f"Failed to save channel {current_channel_id} data") - msg.exec_() - - # Create save button - save_button = QtWidgets.QPushButton("Save Channel to CSV") - save_button.clicked.connect(save_channel_csv) - save_button.setFixedWidth(150) - # Create a horizontal layout for controls controls_layout = QtWidgets.QHBoxLayout() controls_layout.addWidget(QtWidgets.QLabel("Channel:")) controls_layout.addWidget(combo) - controls_layout.addWidget(save_button) controls_layout.addStretch() # Add stretch to push everything to the left controls_widget = QtWidgets.QWidget() From c52994620f1e94d44da36f33e142adace93e8bb7 Mon Sep 17 00:00:00 2001 From: Gilbert Montague Date: Thu, 17 Jul 2025 18:38:00 -0700 Subject: [PATCH 3/4] Revert save csv --- synapse/cli/offline_hdf5_plotter.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/synapse/cli/offline_hdf5_plotter.py b/synapse/cli/offline_hdf5_plotter.py index 428158d..ca6a552 100644 --- a/synapse/cli/offline_hdf5_plotter.py +++ b/synapse/cli/offline_hdf5_plotter.py @@ -369,18 +369,9 @@ def update_single_channel(channel_id): ) combo.setFixedWidth(100) - # Create a horizontal layout for controls - controls_layout = QtWidgets.QHBoxLayout() - controls_layout.addWidget(QtWidgets.QLabel("Channel:")) - controls_layout.addWidget(combo) - controls_layout.addStretch() # Add stretch to push everything to the left - - controls_widget = QtWidgets.QWidget() - controls_widget.setLayout(controls_layout) - # Create a layout for our plot, fft, and controls main_layout = QtWidgets.QVBoxLayout() - main_layout.addWidget(controls_widget) + main_layout.addWidget(combo) main_layout.addWidget(main_splitter) # And finally our main widget to show From 00abdb0d6c03792b580792d1fc6eecb27c2f9ac4 Mon Sep 17 00:00:00 2001 From: Gilbert Montague Date: Thu, 17 Jul 2025 18:54:45 -0700 Subject: [PATCH 4/4] add better info --- synapse/cli/device_info_display.py | 15 +++++++++++++++ synapse/cli/offline_hdf5_plotter.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/synapse/cli/device_info_display.py b/synapse/cli/device_info_display.py index ff848c5..be0dfad 100644 --- a/synapse/cli/device_info_display.py +++ b/synapse/cli/device_info_display.py @@ -27,6 +27,21 @@ def visualize_configuration(info_dict, status): node_tree.add(f"Error Logs:\n{error_logs}") elif node_type == "BroadbandSource": source = node.get("broadband_source", {}) + # Get the peripheral id and name + peripheral_id = source.get("peripheral_id", "Unknown") + peripherals = info_dict.get("peripherals", []) + peripheral_name = next( + ( + p.get("name", "Unknown") + for p in peripherals + if p.get("peripheral_id") == peripheral_id + ), + "Unknown", + ) + node_tree.add(f"Connected to: {peripheral_name} (id: {peripheral_id})") + # Get the sample rate and bit width + node_tree.add(f"Sample Rate: {source.get('sample_rate_hz', 'Unknown')}") + node_tree.add(f"Bit Width: {source.get('bit_width', 'Unknown')}") if "signal" in source and "electrode" in source["signal"]: channels = source["signal"]["electrode"].get("channels", []) electrode_ids = [ diff --git a/synapse/cli/offline_hdf5_plotter.py b/synapse/cli/offline_hdf5_plotter.py index ca6a552..a4709f8 100644 --- a/synapse/cli/offline_hdf5_plotter.py +++ b/synapse/cli/offline_hdf5_plotter.py @@ -363,7 +363,7 @@ def update_single_channel(channel_id): # Create a dropdown for channel selection combo = QtWidgets.QComboBox() - combo.addItems([str(ch) for ch in plot_data.channel_ids]) + combo.addItems([str(ch) for ch in sorted(plot_data.channel_ids)]) combo.currentIndexChanged.connect( lambda: update_single_channel(int(combo.currentText())) )