diff options
| author | Sam Scholten | 2025-10-15 17:08:17 +1000 |
|---|---|---|
| committer | Sam Scholten | 2025-10-15 17:42:47 +1000 |
| commit | 0797f0c5639afca0c6745d223624df1903ec4ef4 (patch) | |
| tree | 2be6584e530a9eec7ac3f23d8ddc77279d3f4d53 | |
| parent | 55001da9a7add8ae57c66f7c921e4f70309df511 (diff) | |
| download | picostream-0797f0c5639afca0c6745d223624df1903ec4ef4.tar.gz picostream-0797f0c5639afca0c6745d223624df1903ec4ef4.zip | |
ensure plotter state is reset and metadata cleared before new acq
| -rw-r--r-- | picostream/dfplot.py | 93 | ||||
| -rw-r--r-- | picostream/main.py | 18 | ||||
| -rw-r--r-- | picostream/pico.py | 7 |
3 files changed, 99 insertions, 19 deletions
diff --git a/picostream/dfplot.py b/picostream/dfplot.py index ebf6f03..e338d58 100644 --- a/picostream/dfplot.py +++ b/picostream/dfplot.py @@ -78,6 +78,7 @@ class HDF5LivePlotter(QWidget): self.max_adc_val: Optional[int] = None self.downsample_mode: Optional[str] = None self.analog_offset_v: float = 0.0 + self.metadata_read: bool = False # --- Debug Counters --- self.update_count: int = 0 @@ -116,21 +117,64 @@ class HDF5LivePlotter(QWidget): # Initial file check self.check_file_exists() - def set_hdf5_path(self, hdf5_path: str) -> None: - """Sets the HDF5 file path to monitor and resets the plot.""" - self.hdf5_path = hdf5_path - logger.info(f"Plotter path updated to: {hdf5_path}") - # Reset state + def reset_for_new_file(self) -> None: + """Reset all state in preparation for a new file. + + This should be called BEFORE set_hdf5_path() when starting a new acquisition + to ensure no stale metadata or state is carried over. + """ + logger.debug("Resetting plotter state for new file") + + # Clear display + self.curve.setData([], []) + + # Reset all data buffers self.display_data = np.array([]) self.time_data = np.array([]) self.data_start_sample = 0 + + # Reset buffer tracking self.last_file_size = 0 self.buffer_reset_count = 0 self.total_samples_processed = 0 + + # Clear cached metadata - this is critical! + self.voltage_range_v = None + self.max_adc_val = None + self.downsample_mode = None + self.analog_offset_v = 0.0 + self.sample_interval_ns = 16.0 + self.hardware_downsample_ratio = 1 + self.metadata_read = False + + # Reset rate checking self.rate_check_start_time = None + self.rate_check_start_samples = 0 self.last_displayed_size = 0 - self.curve.setData([], []) # Clear plot - self.check_file_exists() + + # Reset counters + self.update_count = 0 + self.file_read_count = 0 + self.display_update_count = 0 + self.data_change_count = 0 + self.stale_update_count = 0 + + # Reset status + self.is_saturated = False + self.last_data_timestamp = None + self.display_latency_ms = 0.0 + + def set_hdf5_path(self, hdf5_path: str) -> None: + """Sets the HDF5 file path to monitor. + + Note: Call reset_for_new_file() BEFORE this method when starting a new + acquisition to ensure clean state. + """ + self.hdf5_path = hdf5_path + logger.info(f"Plotter path updated to: {hdf5_path}") + + # Don't check file or read metadata here - wait for update_from_file() + # to be called, which will only read metadata once the file exists with valid data def start_updates(self) -> None: """Starts the plot update timer.""" @@ -204,7 +248,13 @@ class HDF5LivePlotter(QWidget): self.plot_widget.setClipToView(True) def check_file_exists(self) -> None: - """Checks if the HDF5 file exists and attempts to read metadata.""" + """Checks if the HDF5 file exists and attempts to read metadata. + + Only reads metadata if it hasn't been read yet for this file. + """ + if self.metadata_read: + return + try: with h5py.File(self.hdf5_path, "r") as f: if "adc_counts" in f: @@ -227,6 +277,10 @@ class HDF5LivePlotter(QWidget): Args: hdf5_file: An open h5py.File object. """ + if self.metadata_read: + logger.debug("Metadata already read, skipping") + return + try: # Metadata is stored as root-level attributes base_sample_interval_ns = hdf5_file.attrs["sample_interval_ns"] @@ -241,14 +295,22 @@ class HDF5LivePlotter(QWidget): self.max_adc_val = hdf5_file.attrs["max_adc"] self.downsample_mode = hdf5_file.attrs.get("downsample_mode", "average") self.analog_offset_v = hdf5_file.attrs.get("analog_offset_v", 0.0) + + self.metadata_read = True + + logger.info( + f"Plotter read metadata: voltage_range_v={self.voltage_range_v}V, " + f"max_adc={self.max_adc_val}, downsample_mode={self.downsample_mode}, " + f"analog_offset_v={self.analog_offset_v}V" + ) # Update rate label with configured sample rate configured_rate_sps = 1e9 / self.sample_interval_ns self.rate_label.setText( f"Rate: ... : {self._format_rate_sps(configured_rate_sps)}" ) - except KeyError: - logger.debug("Metadata not fully available yet. Will retry.") + except KeyError as e: + logger.debug(f"Metadata not fully available yet: {e}. Will retry.") def _update_heartbeat(self) -> None: """Update UI heartbeat to show the UI thread is alive.""" @@ -386,15 +448,18 @@ class HDF5LivePlotter(QWidget): if current_size == 0: return + # Read metadata if not already done + if not self.metadata_read: + self.read_metadata(f) + # If metadata still not available, wait for next update + if not self.metadata_read: + return + # Start the rate check timer on the first data point if self.rate_check_start_time is None: self.rate_check_start_time = time.perf_counter() self.rate_check_start_samples = current_size - # Read metadata if not already done - if self.voltage_range_v is None: - self.read_metadata(f) - # Dynamically calculate the number of timesteps for the display window display_window_timesteps = int( self.display_window_seconds / (self.sample_interval_ns * 1e-9) diff --git a/picostream/main.py b/picostream/main.py index 26a9b06..81d027f 100644 --- a/picostream/main.py +++ b/picostream/main.py @@ -148,7 +148,8 @@ class PicoStreamMainWindow(QMainWindow): # Connect signals self.start_button.clicked.connect(self.start_acquisition) self.stop_button.clicked.connect(self.stop_acquisition) - self.output_file_input.textChanged.connect(self.plotter.set_hdf5_path) + # Disconnect the automatic path update - we'll handle it manually in start_acquisition + # self.output_file_input.textChanged.connect(self.plotter.set_hdf5_path) self.load_settings() @@ -168,16 +169,25 @@ class PicoStreamMainWindow(QMainWindow): output_file = self.output_file_input.text() - # Reset plotter state and ensure a clean file for the new acquisition. - self.plotter.set_hdf5_path(output_file) + # Stop plotter updates and clear all state BEFORE changing file path + self.plotter.stop_updates() + + # Clear the plotter's display and reset all internal state + self.plotter.reset_for_new_file() + + # Remove old file if it exists if os.path.exists(output_file): try: os.remove(output_file) + logger.info(f"Removed existing file: {output_file}") except OSError as e: self.on_acquisition_error(f"Failed to remove old file: {e}") self.on_acquisition_finished() # Reset UI state return + # Now set the new file path (plotter is stopped and cleared) + self.plotter.set_hdf5_path(output_file) + settings = { "sample_rate_msps": self.sample_rate_input.value(), "resolution_bits": int(self.resolution_input.currentText()), @@ -206,6 +216,8 @@ class PicoStreamMainWindow(QMainWindow): self.worker.error.connect(self.on_acquisition_error) self.thread.start() + + # Start plotter updates - it will wait for the file to exist with valid data self.plotter.start_updates() def stop_acquisition(self) -> None: diff --git a/picostream/pico.py b/picostream/pico.py index eca2877..cd1b13b 100644 --- a/picostream/pico.py +++ b/picostream/pico.py @@ -153,6 +153,8 @@ class PicoDevice: status = ps.ps5000aMaximumValue(self.handle, ctypes.byref(self.max_adc)) check_status(status, "ps5000aMaximumValue") + + logger.info(f"Device opened - Resolution: {self.resolution}, max_adc queried: {self.max_adc.value}") def set_channel( self, chan: str, en: int, coup: str, voltage_range_str: str, offset: float @@ -196,8 +198,9 @@ class PicoDevice: self.handle, channel_enum, en, coupling_enum, channel_range_enum, offset ) check_status(status, f"ps5000aSetChannel ({chan})") - logger.debug( - f"Range '{voltage_range_str}' maps to enum value: {channel_range_enum}, voltage range: {self.voltage_range_v}V" + logger.info( + f"Channel configured - Range: '{voltage_range_str}' (enum: {channel_range_enum}), " + f"voltage_range_v: {self.voltage_range_v}V, max_adc: {self.max_adc.value}" ) def set_data_buffer(self, chan: str, segment: int, rat: str) -> None: |
