From 1a6eb42bb822168f063e3a5ac73733738c1c4dfa Mon Sep 17 00:00:00 2001 From: JamesPicoTech <118216728+JamesPicoTech@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:18:34 +0100 Subject: [PATCH 01/11] Updated get_time_axis() in base.py - Removed int() cast to stop incorrect offsets --- pypicosdk/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypicosdk/base.py b/pypicosdk/base.py index f55bf78..6d169b2 100644 --- a/pypicosdk/base.py +++ b/pypicosdk/base.py @@ -490,7 +490,7 @@ def get_time_axis( if pre_trig_percent is None: return time_axis else: - offset = int(time_axis.max() * (pre_trig_percent / 100)) + offset = time_axis.max() * (pre_trig_percent / 100) return time_axis - offset def realign_downsampled_data( From 54d81441f04759e8d7d337ec7eb4464e6f5345b1 Mon Sep 17 00:00:00 2001 From: Manuel Soeiro Date: Fri, 10 Oct 2025 15:54:39 +0100 Subject: [PATCH 02/11] Fix for linux - lib name and paths (#151) * fix for linux * fix for linux, lib paths * fix for linux, name convention --- pypicosdk/base.py | 14 +++++++++++++- pypicosdk/common.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pypicosdk/base.py b/pypicosdk/base.py index f55bf78..fd8e7a4 100644 --- a/pypicosdk/base.py +++ b/pypicosdk/base.py @@ -4,6 +4,7 @@ import ctypes import os +import platform import warnings import numpy as np @@ -38,7 +39,18 @@ def __init__(self, dll_name, *args, **kwargs): if self._pytest: self.dll = None else: - self.dll = ctypes.CDLL(os.path.join(_get_lib_path(), dll_name + ".dll")) + # Determine file extension and naming convention based on OS + system = platform.system() + if system == "Windows": + lib_name = dll_name + ".dll" + elif system == "Linux": + lib_name = "lib" + dll_name + ".so" + elif system == "Darwin": + lib_name = "lib" + dll_name + ".dylib" + else: + lib_name = "lib" + dll_name + ".so" # Default to Unix-like naming + + self.dll = ctypes.CDLL(os.path.join(_get_lib_path(), lib_name)) self._unit_prefix_n = dll_name # Setup class variables diff --git a/pypicosdk/common.py b/pypicosdk/common.py index 7c5524f..ca24a21 100644 --- a/pypicosdk/common.py +++ b/pypicosdk/common.py @@ -79,7 +79,7 @@ def _get_lib_path() -> str: ] return _check_path(program_files, checklist) elif system == "Linux": - return _check_path('opt', 'picoscope') + return _check_path('/opt', ['picoscope/lib']) elif system == "Darwin": raise PicoSDKException("macOS is not yet tested and supported") else: From 048109dbcc1c185ede5d605acc84613d99639fcb Mon Sep 17 00:00:00 2001 From: James <118216728+JamesPicoTech@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:21:15 +0100 Subject: [PATCH 03/11] Updated Channel Dictionary to Channel Class (#148) * Added channel dataclass to store channel data. * Added noqa to legacy files. * Applied channel class to psospa and ps6000a * Removed set_ylim from protocol class * Removed set_ylim from protocol class * Removed set_ylim from protocol class * Added channel dataclass to store channel data. * Added noqa to legacy files. * Applied channel class to psospa and ps6000a * Fixed merged duplications * Fixed docstring return space * Removed scaled_range in ChannelClass dataclass * Removed duplicated lines * Updated conversion_test.py to new channel class - Updated tests to use channel classes. - Updated mv_to_adc to just include channel * Updated conversions.md to reflect mv_to_adc() changes. * Added function to remember last used voltage for get_ylim. --- docs/docs/ref/ps6000a/conversions.md | 2 +- docs/docs/ref/psospa/conversions.md | 2 +- pypicosdk/_classes/__init__.py | 0 pypicosdk/_classes/_channel_class.py | 33 ++++++++++ pypicosdk/base.py | 96 +++++++++++++++------------- pypicosdk/psospa.py | 33 ++++++++-- pypicosdk/shared/_protocol.py | 1 - pypicosdk/shared/ps6000a_psospa.py | 52 +++++++++++---- tests/conversion_test.py | 16 ++--- 9 files changed, 160 insertions(+), 75 deletions(-) create mode 100644 pypicosdk/_classes/__init__.py create mode 100644 pypicosdk/_classes/_channel_class.py diff --git a/docs/docs/ref/ps6000a/conversions.md b/docs/docs/ref/ps6000a/conversions.md index 9a8f2c6..d8768bf 100644 --- a/docs/docs/ref/ps6000a/conversions.md +++ b/docs/docs/ref/ps6000a/conversions.md @@ -12,7 +12,7 @@ the PicoScope needs to be initialized using `scope.open_unit()` followed by the >>> import pypicosdk as psdk >>> scope = psdk.ps6000a() >>> scope.open_unit(resolution=psdk.RESOLUTION._8BIT) ->>> scope.mv_to_adc(100, channel_range=psdk.RANGE.V1) +>>> scope.mv_to_adc(100, channel=psdk.CHANNEL.A) 3251 >>> scope.close_unit() ``` diff --git a/docs/docs/ref/psospa/conversions.md b/docs/docs/ref/psospa/conversions.md index 96481d6..076a935 100644 --- a/docs/docs/ref/psospa/conversions.md +++ b/docs/docs/ref/psospa/conversions.md @@ -12,7 +12,7 @@ the PicoScope needs to be initialized using `scope.open_unit()` followed by the >>> import pypicosdk as psdk >>> scope = psdk.psospa() >>> scope.open_unit(resolution=psdk.RESOLUTION._8BIT) ->>> scope.mv_to_adc(100, channel_range=psdk.RANGE.V1) +>>> scope.mv_to_adc(100, channel=psdk.CHANNEL.A) 3251 >>> scope.close_unit() ``` diff --git a/pypicosdk/_classes/__init__.py b/pypicosdk/_classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pypicosdk/_classes/_channel_class.py b/pypicosdk/_classes/_channel_class.py new file mode 100644 index 0000000..7ed0995 --- /dev/null +++ b/pypicosdk/_classes/_channel_class.py @@ -0,0 +1,33 @@ +""" +This file contains a class to hold the channel data +i.e. range information and probe scale information +""" +from dataclasses import dataclass +import numpy as np +if __name__ != '__main__': + from .. import constants as cst +else: + from pypicosdk import constants as cst + + +@dataclass +class ChannelClass: + "Dataclass containing channel information" + range: cst.RANGE + range_mv: int + probe_scale: float + ylim_mv: int + ylim_v: float + + def __init__(self, ch_range: cst.RANGE, probe_scale: float): + self.range = ch_range + self.probe_scale = probe_scale + self.range_mv = cst.RANGE_LIST[ch_range] + self.range_v = self.range_mv / 1000 + self.ylim_mv = np.array([-self.range_mv, self.range_mv]) * probe_scale + self.ylim_v = self.ylim_mv / 1000 + + +if __name__ == '__main__': + test = ChannelClass(ch_range=cst.RANGE.V1, probe_scale=10) + print(test) diff --git a/pypicosdk/base.py b/pypicosdk/base.py index fd8e7a4..e41e3a7 100644 --- a/pypicosdk/base.py +++ b/pypicosdk/base.py @@ -2,6 +2,8 @@ Copyright (C) 2025-2025 Pico Technology Ltd. See LICENSE file for terms. """ +# flake8: noqa +# pylint: skip-file import ctypes import os import platform @@ -10,6 +12,7 @@ import numpy as np import numpy.ctypeslib as npc +from ._classes._channel_class import ChannelClass from .error_list import ERROR_STRING from .constants import * from . import constants as cst @@ -55,15 +58,13 @@ def __init__(self, dll_name, *args, **kwargs): # Setup class variables self.handle = ctypes.c_short() - self.range = {} - self.probe_scale = {} + self.channel_db: dict[int, ChannelClass] = {} self.resolution = None self.max_adc_value = None self.min_adc_value = None self.over_range = 0 self._actual_interval = 0 - - self.ylim = (0, 0) + self.last_used_volt_unit: str = 'mv' def __exit__(self): self.close_unit() @@ -312,7 +313,7 @@ def _get_enabled_channel_flags(self) -> int: int: Decimal of enabled channels """ enabled_channel_byte = 0 - for channel in self.range: + for channel in self.channel_db: enabled_channel_byte += 2**channel return enabled_channel_byte @@ -831,22 +832,21 @@ def get_minimum_timebase_stateless(self) -> dict: } # Data conversion ADC/mV & ctypes/int - def mv_to_adc(self, mv: float, channel_range: int, channel: CHANNEL = None) -> int: + def mv_to_adc(self, mv: float, channel: CHANNEL = None) -> int: """ Converts a millivolt (mV) value to an ADC value based on the device's maximum ADC range. Args: mv (float): Voltage in millivolts to be converted. - channel_range (int): Range of channel in millivolts i.e. 500 mV. channel (CHANNEL, optional): Channel associated with ``mv``. The probe scaling for the channel will be applied if provided. Returns: int: ADC value corresponding to the input millivolt value. """ - scale = self.probe_scale.get(channel, 1) - channel_range_mv = RANGE_LIST[channel_range] + scale = self.channel_db[channel].probe_scale + channel_range_mv = self.channel_db[channel].range_mv return int(((mv / scale) / channel_range_mv) * self.max_adc_value) def _adc_conversion( @@ -857,8 +857,8 @@ def _adc_conversion( ) -> float | np.ndarray: """Converts ADC value or array to mV or V using the stored probe scaling.""" unit_scale = _get_literal(output_unit, OutputUnitV_M) - channel_range_mv = RANGE_LIST[self.range[channel]] - channel_scale = self.probe_scale[channel] + channel_range_mv = self.channel_db[channel].range_mv + channel_scale = self.channel_db[channel].probe_scale return (((adc / self.max_adc_value) * channel_range_mv) * channel_scale) / unit_scale def _adc_to_( @@ -882,6 +882,10 @@ def _adc_to_( Returns: dict | float | np.ndarray: _description_ """ + + # Update last used + self.last_used_volt_unit = unit + if isinstance(data, dict): for channel, adc in data.items(): data[channel] = self._adc_conversion(adc, channel, output_unit=unit) @@ -910,6 +914,7 @@ def adc_to_mv( Returns: dict, int, float, np.ndarray: Data converted into millivolts (mV) """ + self.last_used_volt_unit = 'mv' # Update last used return self._adc_to_(data, channel, unit='mv') def adc_to_volts( @@ -931,6 +936,7 @@ def adc_to_volts( Returns: dict, int, float, np.ndarray: Data converted into volts (V) """ + self.last_used_volt_unit = 'v' # Update last used return self._adc_to_(data, channel, unit='v') def _thr_hyst_mv_to_adc( @@ -941,11 +947,12 @@ def _thr_hyst_mv_to_adc( hysteresis_upper_mv, hysteresis_lower_mv ) -> tuple[int, int, int, int]: - if channel in self.range: - upper_adc = self.mv_to_adc(threshold_upper_mv, self.range[channel], channel) - lower_adc = self.mv_to_adc(threshold_lower_mv, self.range[channel], channel) - hyst_upper_adc = self.mv_to_adc(hysteresis_upper_mv, self.range[channel], channel) - hyst_lower_adc = self.mv_to_adc(hysteresis_lower_mv, self.range[channel], channel) + if channel in self.channel_db: + ch_range = self.channel_db[channel].range + upper_adc = self.mv_to_adc(threshold_upper_mv, channel) + lower_adc = self.mv_to_adc(threshold_lower_mv, channel) + hyst_upper_adc = self.mv_to_adc(hysteresis_upper_mv, channel) + hyst_lower_adc = self.mv_to_adc(hysteresis_lower_mv, channel) else: upper_adc = int(threshold_upper_mv) lower_adc = int(threshold_lower_mv) @@ -968,43 +975,39 @@ def _change_power_source(self, state: POWER_SOURCE) -> 0: state ) - def _set_ylim(self, ch_range: RANGE | range_literal) -> None: - """ - Update the scope self.ylim with the largest channel range - - Args: - ch_range (RANGE | range_literal): Range of current channel - """ - # Convert to mv - ch_range = RANGE_LIST[ch_range] - - # Compare largest value - max_ylim = max(self.ylim[1], ch_range) - min_ylim = -max_ylim - self.ylim = (min_ylim, max_ylim) - - def get_ylim(self, unit: OutputUnitV_L = 'mv') -> tuple[float, float]: + def get_ylim(self, unit: str | None | OutputUnitV_L = None) -> tuple[float, float]: """ - Returns the ylim of the widest channel range as a tuple. + Returns the ylim of the widest channel range as a tuple. The unit is taken from + the last used adc to voltage conversion, but can be overwritten by declaring a + `unit` variable. Ideal for pyplot ylim function. Args: - unit (str): 'mv' or 'v'. Depending on whether your data is in mV - or Volts. + unit (str | None, optional): Overwrite the ylim unit using `'mv'` or `'v'`. + If None, The unit will be taken from the last voltage unit conversion. Returns: - tuple[float, float]: Minium and maximum range values + tuple[float,float]: Minium and maximum range values Examples: >>> from matplotlib import pyplot as plt >>> ... >>> plt.ylim(scope.get_ylim()) """ + if unit is None: + # Collect last used voltage unit + unit = self.last_used_volt_unit + + # Get largest channel range + largest_range_index = max( + self.channel_db, + key=lambda ch: self.channel_db[ch].range_mv + ) unit = unit.lower() - if unit.lower() == 'mv': - return self.ylim - elif unit.lower(): - return self.ylim[0] / 1000, self.ylim[1] / 1000 + if unit == 'mv': + return self.channel_db[largest_range_index].ylim_mv + elif unit == 'v': + return self.channel_db[largest_range_index].ylim_v def set_device_resolution(self, resolution: RESOLUTION) -> None: """Configure the ADC resolution using ``ps6000aSetDeviceResolution``. @@ -1054,8 +1057,8 @@ def set_simple_trigger( channel = _get_literal(channel, channel_map) direction = _get_literal(direction, trigger_dir_m) - if channel in self.range: - threshold_adc = self.mv_to_adc(threshold_mv, self.range[channel], channel) + if channel in self.channel_db: + threshold_adc = self.mv_to_adc(threshold_mv, channel) else: threshold_adc = int(threshold_mv) @@ -1454,12 +1457,12 @@ def set_data_buffer_for_enabled_channels( channels_buffer = {} # Rapid if captures > 0: - for channel in self.range: + for channel in self.channel_db: np_buffer = self.set_data_buffer_rapid_capture(channel, samples, captures, segment, datatype, ratio_mode, action=ACTION.ADD) channels_buffer[channel] = np_buffer # Single else: - for channel in self.range: + for channel in self.channel_db: channels_buffer[channel] = self.set_data_buffer(channel, samples, segment, datatype, ratio_mode, action=ACTION.ADD) return channels_buffer @@ -1763,6 +1766,9 @@ def run_simple_block_capture( >>> buffers = scope.run_simple_block_capture(timebase=3, samples=1000) """ + # Update last used + self.last_used_volt_unit = output_unit + # Create data buffers channel_buffer = \ self.set_data_buffer_for_enabled_channels(samples, segment, datatype, ratio_mode) @@ -1821,6 +1827,8 @@ def run_simple_rapid_block_capture( tuple[dict,np.ndarray]: Dictionary of channel buffers and the time axis (numpy array). """ + # Update last used + self.last_used_volt_unit = output_unit # Segment set to 0 segment = 0 diff --git a/pypicosdk/psospa.py b/pypicosdk/psospa.py index 24a10fb..dacc6ee 100644 --- a/pypicosdk/psospa.py +++ b/pypicosdk/psospa.py @@ -1,13 +1,19 @@ """Copyright (C) 2025-2025 Pico Technology Ltd. See LICENSE file for terms.""" +# flake8: noqa +# pylint: skip-file import ctypes from typing import override, Literal import json +from warnings import warn +from ._classes._channel_class import ChannelClass +from . import constants as cst from .constants import * from .constants import ( TIME_UNIT ) +from . import common as cmn from .common import PicoSDKException from .base import PicoScopeBase from .shared.ps6000a_psospa import shared_ps6000a_psospa @@ -61,12 +67,13 @@ def open_unit(self, serial_number:str=None, resolution:RESOLUTION | resolution_l @override def set_channel_on( self, - channel:CHANNEL, - range:RANGE, - coupling:COUPLING=COUPLING.DC, - offset:float=0, - bandwidth:BANDWIDTH_CH=BANDWIDTH_CH.FULL, - range_type:PICO_PROBE_RANGE_INFO=PICO_PROBE_RANGE_INFO.X1_PROBE_NV + channel: CHANNEL, + range: RANGE, + coupling: COUPLING = COUPLING.DC, + offset: float = 0, + bandwidth: BANDWIDTH_CH = BANDWIDTH_CH.FULL, + range_type: PICO_PROBE_RANGE_INFO = PICO_PROBE_RANGE_INFO.X1_PROBE_NV, + probe_scale: float = 1.0, ) -> int: """ Enable and configure a specific channel on the device with given parameters. @@ -84,8 +91,16 @@ def set_channel_on( Bandwidth limit setting for the channel. Defaults to full bandwidth. range_type (PICO_PROBE_RANGE_INFO, optional): Specifies the probe range type. Defaults to X1 probe (no attenuation). + probe_scale (float, optional): Probe attenuation factor e.g. 10 for x10 probe. + Default value of 1.0 (x1). """ - self.range[channel] = range + if probe_scale != 1: + warn( + f'Ensure selected channel range of {cst.range_literal.__args__[range]} ' + + f'accounts for attenuation of x{probe_scale} at scope input', + cmn.ProbeScaleWarning) + + self.channel_db[channel] = ChannelClass(ch_range=range, probe_scale=probe_scale) range_max = ctypes.c_int64(RANGE_LIST[range] * 1_000_000) range_min = ctypes.c_int64(-range_max.value) @@ -103,6 +118,7 @@ def set_channel_on( ) return status + @override def get_nearest_sampling_interval(self, interval_s:float, round_faster:int=True) -> dict: """ @@ -267,6 +283,7 @@ def set_led_colours( hue = [hue] saturation = [saturation] + if isinstance(hue[0], str): hue = [led_colours_m[i] for i in hue] @@ -287,6 +304,7 @@ def set_led_colours( array_len, ) + def set_all_led_states(self,state:str|led_state_l): """ Sets the state of all LED's on the PicoScope. @@ -303,6 +321,7 @@ def set_led_states(self, led:str|led_channel_l|list[led_channel_l], state:str|le Sets the state for a selected LED. Between default behaviour (auto), on or off. + Args: led (str): The selected LED. Must be one of these values: `'A'`, `'B'`, `'C'`, `'D'`, `'E'`, `'F'`, `'G'`, `'H'`, `'AWG'`, `'AUX'`. diff --git a/pypicosdk/shared/_protocol.py b/pypicosdk/shared/_protocol.py index 478b7cb..59faddf 100644 --- a/pypicosdk/shared/_protocol.py +++ b/pypicosdk/shared/_protocol.py @@ -8,4 +8,3 @@ class _ProtocolBase(Protocol): """Protocol placeholder class for shared methods""" - def _set_ylim(self, *args, **kwargs): ... diff --git a/pypicosdk/shared/ps6000a_psospa.py b/pypicosdk/shared/ps6000a_psospa.py index 791c91e..8cde495 100644 --- a/pypicosdk/shared/ps6000a_psospa.py +++ b/pypicosdk/shared/ps6000a_psospa.py @@ -2,10 +2,13 @@ Copyright (C) 2025-2025 Pico Technology Ltd. See LICENSE file for terms. """ +# flake8: noqa +# pylint: skip-file import ctypes from warnings import warn import numpy as np +from .._classes._channel_class import ChannelClass from .. import constants as cst from ..constants import * from ..constants import ( @@ -33,6 +36,7 @@ class shared_ps6000a_psospa(_ProtocolBase): """Shared functions between ps6000a and psospa""" probe_scale: dict[float] + channel_db: dict[int, ChannelClass] def get_adc_limits(self) -> tuple: """ @@ -730,25 +734,47 @@ def set_channel( channel = _get_literal(channel, channel_map) range = _get_literal(range, range_map) - # Add probe scaling - self.probe_scale[channel] = probe_scale - if probe_scale != 1.0: + if enabled: + self.set_channel_on(channel, range, coupling, offset, bandwidth, + probe_scale=probe_scale) + else: + self.set_channel_off(channel) + + def set_channel_on( + self, + channel, + range, + coupling=COUPLING.DC, + offset=0.0, + bandwidth=BANDWIDTH_CH.FULL, + probe_scale: float = 1.0 + ) -> int: + """ + Enable and configure a specific channel on the device with given parameters. + + Args: + channel (CHANNEL): + The channel to enable (e.g., CHANNEL.A, CHANNEL.B). + range (RANGE): + The input voltage range to set for the channel. + coupling (COUPLING, optional): + The coupling mode to use (e.g., DC, AC). Defaults to DC. + offset (float, optional): + DC offset to apply to the channel input, in volts. Defaults to 0. + bandwidth (BANDWIDTH_CH, optional): + Bandwidth limit setting for the channel. Defaults to full bandwidth. + probe_scale (float, optional): Probe attenuation factor e.g. 10 for x10 probe. + Default value of 1.0 (x1). + + """ + if probe_scale != 1: warn( f'Ensure selected channel range of {cst.range_literal.__args__[range]} ' + f'accounts for attenuation of x{probe_scale} at scope input', cmn.ProbeScaleWarning) - # Update ylims - self._set_ylim(range) - - if enabled: - self.set_channel_on(channel, range, coupling, offset, bandwidth) - else: - self.set_channel_off(channel) + self.channel_db[channel] = ChannelClass(ch_range=range, probe_scale=probe_scale) - def set_channel_on(self, channel, range, coupling=COUPLING.DC, offset=0.0, bandwidth=BANDWIDTH_CH.FULL): - """Sets a channel to ON at a specified range (6000E)""" - self.range[channel] = range status = self._call_attr_function( 'SetChannelOn', self.handle, diff --git a/tests/conversion_test.py b/tests/conversion_test.py index 50ebf6f..0d11006 100644 --- a/tests/conversion_test.py +++ b/tests/conversion_test.py @@ -3,22 +3,22 @@ pytest file for checking the mv/adc conversions """ -from pypicosdk import ps6000a, RANGE +from pypicosdk import ps6000a, RANGE, CHANNEL +from pypicosdk._classes._channel_class import ChannelClass +channel = CHANNEL.A -def test_mv_to_adc(): +def test_ps6000a_mv_to_adc(): """Test mv_to_adc function""" scope = ps6000a('pytest') - scope.range = {0: 6} # Channel A set to 1 V - scope.probe_scale = {0: 1.0} # Channel A at 1.0 probe scale scope.max_adc_value = 32000 - assert scope.mv_to_adc(5.0, RANGE.V1) == 160 + scope.channel_db[channel] = ChannelClass(RANGE.V1, 1) + assert scope.mv_to_adc(5.0, channel) == 160 -def test_adc_to_mv(): +def test_ps6000a_adc_to_mv(): """Test adc_to_mv function""" scope = ps6000a('pytest') - scope.range = {0: 6} # Channel A set to 1 V - scope.probe_scale = {0: 1.0} # Channel A at 1.0 probe scale scope.max_adc_value = 32000 + scope.channel_db[channel] = ChannelClass(RANGE.V1, 1) assert scope.adc_to_mv(160, 0) == 5.0 From 503ee6d0ef594925a61cb9c1230af22bed1a8773 Mon Sep 17 00:00:00 2001 From: James <118216728+JamesPicoTech@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:47:37 +0100 Subject: [PATCH 04/11] Added simple trigger threshold unit configuration (#152) * Added volts_to_adc to base.py * Updated volts_to_adc in base.py. - Updated channel docstring arg to remove "cst." * Updated mv_to_adc in base.py - Removed channel as optional - Added "(mV)" to mv docstring * Updated set_simple_trigger() in base.py to include unit declaration for threshold. * Added pytest for volts_to_adc() * Updated examples from threshold_mv to threshold in set_simple_trigger() --- examples/advanced_block_capture.py | 2 +- .../advanced_block_capture_downsampled.py | 3 +- examples/dead_time.py | 2 +- examples/eres_res_enhance.py | 2 +- examples/fft.py | 2 +- .../amplitude_pk2pk_rms.py | 2 +- examples/measurement_examples/frequency.py | 2 +- examples/measurement_examples/overshoot.py | 2 +- examples/pk2pk_histogram.py | 2 +- examples/rapid_block.py | 2 +- examples/saving_files/save_csv.py | 2 +- examples/saving_files/save_numpy.py | 2 +- examples/simple_block_capture.py | 8 ++-- pypicosdk/base.py | 45 ++++++++++++++----- tests/conversion_test.py | 8 ++++ 15 files changed, 57 insertions(+), 29 deletions(-) diff --git a/examples/advanced_block_capture.py b/examples/advanced_block_capture.py index 72d52cc..a259f8d 100644 --- a/examples/advanced_block_capture.py +++ b/examples/advanced_block_capture.py @@ -38,7 +38,7 @@ scope.set_channel(channel=psdk.CHANNEL.A, range=psdk.RANGE.V1) # Configure a simple rising edge trigger for channel A, wait indefinitely (do not auto trigger) -scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold_mv=0, auto_trigger=0) +scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold=0, auto_trigger=0) # Helper function to set timebase of scope via requested sample rate TIMEBASE = scope.sample_rate_to_timebase(sample_rate=50, unit=psdk.SAMPLE_RATE.MSPS) diff --git a/examples/advanced_block_capture_downsampled.py b/examples/advanced_block_capture_downsampled.py index 29d3fc1..9c80863 100644 --- a/examples/advanced_block_capture_downsampled.py +++ b/examples/advanced_block_capture_downsampled.py @@ -18,7 +18,6 @@ """ from matplotlib import pyplot as plt -import numpy as np import pypicosdk as psdk # Number of raw samples to request from the driver before downsampling @@ -43,7 +42,7 @@ scope.set_channel(channel=psdk.CHANNEL.A, range=psdk.RANGE.V1) # Configure a simple rising edge trigger for channel A, wait indefinitely (do not auto trigger) -scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold_mv=0, auto_trigger=0) +scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold=0, auto_trigger=0) # Helper function to set timebase of scope via requested sample rate TIMEBASE = scope.sample_rate_to_timebase(sample_rate=50, unit=psdk.SAMPLE_RATE.MSPS) diff --git a/examples/dead_time.py b/examples/dead_time.py index 0750fca..83a650e 100644 --- a/examples/dead_time.py +++ b/examples/dead_time.py @@ -35,7 +35,7 @@ scope.set_channel(channel=psdk.CHANNEL.A, range=psdk.RANGE.V2) # Configure a simple rising edge trigger for channel A, wait indefinitely (do not auto trigger) -scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold_mv=0, auto_trigger=0) +scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold=0, auto_trigger=0) # Helper function to set timebase of scope via requested sample rate TIMEBASE = scope.sample_rate_to_timebase(sample_rate=50, unit=psdk.SAMPLE_RATE.GSPS) diff --git a/examples/eres_res_enhance.py b/examples/eres_res_enhance.py index 8ae72c5..ceda409 100644 --- a/examples/eres_res_enhance.py +++ b/examples/eres_res_enhance.py @@ -35,7 +35,7 @@ scope.set_channel(channel=psdk.CHANNEL.A, range=psdk.RANGE.V2) # Configure a simple rising edge trigger for channel A, wait indefinitely (do not auto trigger) -scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold_mv=0, auto_trigger=0) +scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold=0, auto_trigger=0) # Helper function to set timebase of scope via requested sample rate TIMEBASE = scope.sample_rate_to_timebase(sample_rate=1.25, unit=psdk.SAMPLE_RATE.GSPS) diff --git a/examples/fft.py b/examples/fft.py index 173f7d4..60b1d3a 100644 --- a/examples/fft.py +++ b/examples/fft.py @@ -38,7 +38,7 @@ scope.set_channel(channel=psdk.CHANNEL.A, range=psdk.RANGE.V1) # Configure a simple rising edge trigger for channel A, wait indefinitely (do not auto trigger) -scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold_mv=0, auto_trigger=0) +scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold=0, auto_trigger=0) # Helper function to set timebase of scope via requested sample rate TIMEBASE = scope.sample_rate_to_timebase(50, psdk.SAMPLE_RATE.MSPS) diff --git a/examples/measurement_examples/amplitude_pk2pk_rms.py b/examples/measurement_examples/amplitude_pk2pk_rms.py index 1bcdcda..260dd48 100644 --- a/examples/measurement_examples/amplitude_pk2pk_rms.py +++ b/examples/measurement_examples/amplitude_pk2pk_rms.py @@ -26,7 +26,7 @@ # Setup channel and trigger scope.set_channel(channel=CHANNEL, range=RANGE) -scope.set_simple_trigger(channel=CHANNEL, threshold_mv=THRESHOLD) +scope.set_simple_trigger(channel=CHANNEL, threshold=THRESHOLD) scope.set_siggen(1E6, 1.6, psdk.WAVEFORM.SINE, offset=0.1) diff --git a/examples/measurement_examples/frequency.py b/examples/measurement_examples/frequency.py index 508e76c..fe97226 100644 --- a/examples/measurement_examples/frequency.py +++ b/examples/measurement_examples/frequency.py @@ -23,7 +23,7 @@ # Setup channel and trigger scope.set_channel(channel=CHANNEL, range=RANGE) -scope.set_simple_trigger(channel=CHANNEL, threshold_mv=THRESHOLD) +scope.set_simple_trigger(channel=CHANNEL, threshold=THRESHOLD) scope.set_siggen(0.05E6, 1.0, psdk.WAVEFORM.SINE) diff --git a/examples/measurement_examples/overshoot.py b/examples/measurement_examples/overshoot.py index f46fefd..8939092 100644 --- a/examples/measurement_examples/overshoot.py +++ b/examples/measurement_examples/overshoot.py @@ -25,7 +25,7 @@ # Setup channel and trigger scope.set_channel(channel=CHANNEL, range=RANGE) -scope.set_simple_trigger(channel=CHANNEL, threshold_mv=THRESHOLD) +scope.set_simple_trigger(channel=CHANNEL, threshold=THRESHOLD) scope.set_siggen(1E6, 1.6, psdk.WAVEFORM.SQUARE) diff --git a/examples/pk2pk_histogram.py b/examples/pk2pk_histogram.py index a0c4d30..715c320 100644 --- a/examples/pk2pk_histogram.py +++ b/examples/pk2pk_histogram.py @@ -35,7 +35,7 @@ scope.set_channel(channel=psdk.CHANNEL.A, coupling=psdk.COUPLING.DC, range=psdk.RANGE.mV500) # Configure a simple rising edge trigger for channel A, wait indefinitely (do not auto trigger) -scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold_mv=200, +scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold=200, direction=psdk.TRIGGER_DIR.RISING, auto_trigger=0) # Set siggen to 10MHz & 0.9Vpkpk output sine wave diff --git a/examples/rapid_block.py b/examples/rapid_block.py index ece89b6..380b17f 100644 --- a/examples/rapid_block.py +++ b/examples/rapid_block.py @@ -34,7 +34,7 @@ scope.set_channel(channel=psdk.CHANNEL.A, range=psdk.RANGE.V1) # Configure a simple rising edge trigger for channel A, wait indefinitely (do not auto trigger) -scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold_mv=0, auto_trigger=0) +scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold=0, auto_trigger=0) # Set siggen to 100kHz & 0.8Vpkpk output sine wave scope.set_siggen(frequency=100_000, pk2pk=0.8, wave_type=psdk.WAVEFORM.SINE) diff --git a/examples/saving_files/save_csv.py b/examples/saving_files/save_csv.py index 4e107f5..e74fa33 100644 --- a/examples/saving_files/save_csv.py +++ b/examples/saving_files/save_csv.py @@ -29,7 +29,7 @@ scope.open_unit() scope.set_siggen(frequency=50_000, pk2pk=1.8, wave_type=psdk.WAVEFORM.SINE) scope.set_channel(channel=psdk.CHANNEL.A, range=psdk.RANGE.V1) -scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold_mv=0, auto_trigger=0) +scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold=0, auto_trigger=0) TIMEBASE = scope.sample_rate_to_timebase(sample_rate=50, unit=psdk.SAMPLE_RATE.MSPS) # Get data to save as channel buffer and time axis diff --git a/examples/saving_files/save_numpy.py b/examples/saving_files/save_numpy.py index 43c6c36..26a2c73 100644 --- a/examples/saving_files/save_numpy.py +++ b/examples/saving_files/save_numpy.py @@ -28,7 +28,7 @@ scope.open_unit() scope.set_siggen(frequency=50_000, pk2pk=1.8, wave_type=psdk.WAVEFORM.SINE) scope.set_channel(channel=psdk.CHANNEL.A, range=psdk.RANGE.V1) -scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold_mv=0, auto_trigger=0) +scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold=0, auto_trigger=0) TIMEBASE = scope.sample_rate_to_timebase(sample_rate=50, unit=psdk.SAMPLE_RATE.MSPS) # Get data to save as channel buffer and time axis diff --git a/examples/simple_block_capture.py b/examples/simple_block_capture.py index ba7be9e..9b4a948 100644 --- a/examples/simple_block_capture.py +++ b/examples/simple_block_capture.py @@ -21,20 +21,20 @@ SAMPLES = 5_000 # Create "scope" class and initialize PicoScope -scope = psdk.psospa() +scope = psdk.ps6000a() scope.open_unit() # Print the returned serial number of the initialized instrument print(scope.get_unit_serial()) # Set siggen to 1MHz & 0.8Vpkpk output sine wave -scope.set_siggen(frequency=1_000_000, pk2pk=1.8, wave_type=psdk.WAVEFORM.SINE) +scope.set_siggen(frequency=10_000, pk2pk=1.8, wave_type=psdk.WAVEFORM.SINE) # Enable channel A with +/- 1V range (2V total dynamic range) scope.set_channel(channel=psdk.CHANNEL.A, range=psdk.RANGE.V1) # Configure a simple rising edge trigger for channel A, wait indefinitely (do not auto trigger) -scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold_mv=0, auto_trigger=0) +scope.set_simple_trigger(channel=psdk.CHANNEL.A, threshold=0, auto_trigger=0) # Helper function to set timebase of scope via requested sample rate TIMEBASE = scope.sample_rate_to_timebase(sample_rate=50, unit=psdk.SAMPLE_RATE.MSPS) @@ -61,7 +61,7 @@ plt.grid(True) # Set the Y axis of the graph to the largest voltage range selected for enabled channels, in mV -plt.ylim(scope.get_ylim(unit='mv')) +plt.ylim(scope.get_ylim()) # Display the completed plot plt.show() diff --git a/pypicosdk/base.py b/pypicosdk/base.py index e41e3a7..2063f06 100644 --- a/pypicosdk/base.py +++ b/pypicosdk/base.py @@ -831,15 +831,32 @@ def get_minimum_timebase_stateless(self) -> dict: "time_interval": time_interval.value, } + def volts_to_adc(self, volts: float, channel: cst.CHANNEL) -> int: + """ + Coverts a volt (V) value to an ADC value based on the channel range and device's maximum + ADC value. + + Args: + volts (float): Voltage in volts (V) to be converted + channel (CHANNEL): Channel associated with `volts`. The probe scaling will + be applied if provided. + + Returns: + int: ADC value corresponding to the input voltage. + """ + scale = self.channel_db[channel].probe_scale + channel_range_v = self.channel_db[channel].range_v + return int(((volts / scale) / channel_range_v) * self.max_adc_value) + # Data conversion ADC/mV & ctypes/int - def mv_to_adc(self, mv: float, channel: CHANNEL = None) -> int: + def mv_to_adc(self, mv: float, channel: cst.CHANNEL) -> int: """ Converts a millivolt (mV) value to an ADC value based on the device's maximum ADC range. Args: - mv (float): Voltage in millivolts to be converted. - channel (CHANNEL, optional): Channel associated with ``mv``. The + mv (float): Voltage in millivolts (mV) to be converted. + channel (CHANNEL): Channel associated with ``mv``. The probe scaling for the channel will be applied if provided. Returns: @@ -1032,18 +1049,20 @@ def set_all_channels_off(self): def set_simple_trigger( self, channel: CHANNEL | channel_literal, - threshold_mv:int=0, - enable:bool=True, - direction:TRIGGER_DIR | trigger_dir_l = TRIGGER_DIR.RISING, - delay:int=0, - auto_trigger:int=0 + threshold: int = 0, + threshold_unit: cst.output_unit_l = 'mv', + enable: bool = True, + direction: TRIGGER_DIR | trigger_dir_l = TRIGGER_DIR.RISING, + delay: int = 0, + auto_trigger: int = 0, ) -> None: """ Sets up a simple trigger from a specified channel and threshold in mV. Args: channel (CHANNEL | str): The input channel to apply the trigger to. - threshold_mv (int, optional): Trigger threshold level in millivolts. + threshold (int, optional): Trigger threshold level. + threshold_unit (str, optional): Trigger threshold unit. Default is 'mv'. enable (bool, optional): Enables or disables the trigger. direction (TRIGGER_DIR | str, optional): Trigger direction (e.g., ``TRIGGER_DIR.RISING``). delay (int, optional): Delay in samples after the trigger condition is met before starting capture. @@ -1057,10 +1076,12 @@ def set_simple_trigger( channel = _get_literal(channel, channel_map) direction = _get_literal(direction, trigger_dir_m) - if channel in self.channel_db: - threshold_adc = self.mv_to_adc(threshold_mv, channel) + if threshold_unit == 'mv': + threshold_adc = self.mv_to_adc(threshold, channel) + elif threshold_unit == 'v': + threshold_adc = self.volts_to_adc(threshold, channel) else: - threshold_adc = int(threshold_mv) + threshold_adc = int(threshold) self._call_attr_function( 'SetSimpleTrigger', diff --git a/tests/conversion_test.py b/tests/conversion_test.py index 0d11006..233073f 100644 --- a/tests/conversion_test.py +++ b/tests/conversion_test.py @@ -16,6 +16,14 @@ def test_ps6000a_mv_to_adc(): assert scope.mv_to_adc(5.0, channel) == 160 +def test_ps6000a_volts_to_adc(): + """Test mv_to_adc function""" + scope = ps6000a('pytest') + scope.max_adc_value = 32000 + scope.channel_db[channel] = ChannelClass(RANGE.V1, 1) + assert scope.volts_to_adc(5.0, channel) == 160000 + + def test_ps6000a_adc_to_mv(): """Test adc_to_mv function""" scope = ps6000a('pytest') From b31f5e388652fee560631fca0cc3faef1e1aaff9 Mon Sep 17 00:00:00 2001 From: JamesPicoTech <118216728+JamesPicoTech@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:18:34 +0100 Subject: [PATCH 05/11] Updated get_time_axis() in base.py - Removed int() cast to stop incorrect offsets --- pypicosdk/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypicosdk/base.py b/pypicosdk/base.py index 2063f06..1b6434d 100644 --- a/pypicosdk/base.py +++ b/pypicosdk/base.py @@ -503,7 +503,7 @@ def get_time_axis( if pre_trig_percent is None: return time_axis else: - offset = int(time_axis.max() * (pre_trig_percent / 100)) + offset = time_axis.max() * (pre_trig_percent / 100) return time_axis - offset def realign_downsampled_data( From fef4b49ad1f129e630a8ba270c3e0a2c028b2d4a Mon Sep 17 00:00:00 2001 From: JamesPicoTech <118216728+JamesPicoTech@users.noreply.github.com> Date: Wed, 15 Oct 2025 10:25:22 +0100 Subject: [PATCH 06/11] Added base dataclass storage for PicoScopeBase (base.py) --- pypicosdk/_classes/_general.py | 9 +++++++++ pypicosdk/base.py | 3 +++ 2 files changed, 12 insertions(+) create mode 100644 pypicosdk/_classes/_general.py diff --git a/pypicosdk/_classes/_general.py b/pypicosdk/_classes/_general.py new file mode 100644 index 0000000..cd0801a --- /dev/null +++ b/pypicosdk/_classes/_general.py @@ -0,0 +1,9 @@ +"This file contains general classes for pyPicoSDK" + +from dataclasses import dataclass + + +@dataclass +class BaseDataClass: + "Class containing data for PicoScopeBase" + last_pre_trig: float = 50 diff --git a/pypicosdk/base.py b/pypicosdk/base.py index 1b6434d..6c25980 100644 --- a/pypicosdk/base.py +++ b/pypicosdk/base.py @@ -29,6 +29,7 @@ from .common import ( _get_literal, ) +from ._classes import _general class PicoScopeBase: @@ -66,6 +67,8 @@ def __init__(self, dll_name, *args, **kwargs): self._actual_interval = 0 self.last_used_volt_unit: str = 'mv' + self.base_dataclass = _general.BaseDataClass() + def __exit__(self): self.close_unit() From c1805d097862b0e42a5955f7c48c3ad789131198 Mon Sep 17 00:00:00 2001 From: JamesPicoTech <118216728+JamesPicoTech@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:04:11 +0100 Subject: [PATCH 07/11] Updated get_time_axis and run_block_capture --- pypicosdk/base.py | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/pypicosdk/base.py b/pypicosdk/base.py index 6c25980..7d83b09 100644 --- a/pypicosdk/base.py +++ b/pypicosdk/base.py @@ -490,7 +490,8 @@ def get_time_axis( Args: timebase (int): PicoScope timebase samples (int): Number of samples captured - pre_trig_percent (int): Percent to offset the 0 point by. If None, default is 0. + pre_trig_percent (int): Percent to offset the 0 point by. If None, defaults to last + used pre_trig_percent or 50. unit (str): Unit of seconds the time axis is returned in. Default is 'ns' (nanoseconds). ratio (int): If using a downsampling ratio, this will scale the time interval @@ -499,15 +500,24 @@ def get_time_axis( Returns: np.ndarray: Array of time values in nano-seconds """ + # Check and save pre_trig to base dataclass + if pre_trig_percent == None: + pre_trig_percent = self.base_dataclass.last_pre_trig + self.base_dataclass.last_pre_trig = pre_trig_percent + + # Default to 1 when using ratio ratio = max(1, ratio) + + # Get unit scalar value scalar = cst.TimeUnitStd_M['ns'] / cst.TimeUnitStd_M[unit] + + # Get the interval for the specified timebase/samples interval = self.get_timebase(timebase, samples)['Interval(ns)'] * ratio / scalar + + # Maths time_axis = np.arange(samples) * interval - if pre_trig_percent is None: - return time_axis - else: - offset = time_axis.max() * (pre_trig_percent / 100) - return time_axis - offset + offset = time_axis.max() * (pre_trig_percent / 100) + return time_axis - offset def realign_downsampled_data( self, @@ -1888,7 +1898,13 @@ def run_simple_rapid_block_capture( # Return data return channel_buffer, time_axis - def run_block_capture(self, timebase, samples, pre_trig_percent=50, segment=0) -> int: + def run_block_capture( + self, + timebase: int, + samples: int, + pre_trig_percent: float | None = None, + segment: int = 0, + ) -> int: """ Runs a block capture using the specified timebase and number of samples. @@ -1896,14 +1912,20 @@ def run_block_capture(self, timebase, samples, pre_trig_percent=50, segment=0) - pre-trigger and post-trigger samples. It uses the PicoSDK `RunBlock` function. Args: - timebase (int): Timebase value determining sample interval (refer to PicoSDK guide). - samples (int): Total number of samples to capture. - pre_trig_percent (int, optional): Percentage of samples to capture before the trigger. - segment (int, optional): Memory segment index to use. + timebase (int): Timebase value determining sample interval (refer to PicoSDK guide). + samples (int): Total number of samples to capture. + pre_trig_percent (int | None, optional): + Percentage of samples to capture before the trigger. If None, defaults to + last called pre_trig_percent or 50. + segment (int, optional): Memory segment index to use. Returns: - int: Estimated time (in milliseconds) the device will be busy capturing data. + int: Estimated time (in milliseconds) the device will be busy capturing data. """ + # Check and add pre-trig to base dataclass + if pre_trig_percent is None: + pre_trig_percent = self.base_dataclass.last_pre_trig + self.base_dataclass.last_pre_trig = pre_trig_percent pre_samples = int((samples * pre_trig_percent) / 100) post_samples = int(samples - pre_samples) From 98b74b4a19d35f2b6b4d12e8280c6c19144117ff Mon Sep 17 00:00:00 2001 From: James <118216728+JamesPicoTech@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:26:03 +0100 Subject: [PATCH 08/11] Updated set_channel in ps6000a_psospa.py (#153) - Added constrain to probe_scale (above 0.0) --- pypicosdk/shared/ps6000a_psospa.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pypicosdk/shared/ps6000a_psospa.py b/pypicosdk/shared/ps6000a_psospa.py index 8cde495..6f20e0d 100644 --- a/pypicosdk/shared/ps6000a_psospa.py +++ b/pypicosdk/shared/ps6000a_psospa.py @@ -26,10 +26,12 @@ ) from .. import common as cmn from ..common import ( - PicoSDKException, _struct_to_dict, _get_literal, ) +from .._exceptions import ( + PicoSDKException +) from ._protocol import _ProtocolBase @@ -730,6 +732,11 @@ def set_channel( probe_scale (float, optional): Probe attenuation factor e.g. 10 for x10 probe. Default value of 1.0 (x1). """ + # Constrain probe scale + if probe_scale <= 0.0: + raise PicoSDKException( + f'Invalid probe scale: {probe_scale}. Value must be greater than 0.0.') + # Check if typing Literals channel = _get_literal(channel, channel_map) range = _get_literal(range, range_map) From f5e40f669622031978f9a0ff7b804542e7367906 Mon Sep 17 00:00:00 2001 From: JamesPicoTech <118216728+JamesPicoTech@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:18:34 +0100 Subject: [PATCH 09/11] Updated get_time_axis() in base.py - Removed int() cast to stop incorrect offsets --- pypicosdk/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypicosdk/base.py b/pypicosdk/base.py index 2063f06..1b6434d 100644 --- a/pypicosdk/base.py +++ b/pypicosdk/base.py @@ -503,7 +503,7 @@ def get_time_axis( if pre_trig_percent is None: return time_axis else: - offset = int(time_axis.max() * (pre_trig_percent / 100)) + offset = time_axis.max() * (pre_trig_percent / 100) return time_axis - offset def realign_downsampled_data( From 9caafb3b26494896a86f797aed90ccd33c7bda14 Mon Sep 17 00:00:00 2001 From: JamesPicoTech <118216728+JamesPicoTech@users.noreply.github.com> Date: Wed, 15 Oct 2025 10:25:22 +0100 Subject: [PATCH 10/11] Added base dataclass storage for PicoScopeBase (base.py) --- pypicosdk/_classes/_general.py | 9 +++++++++ pypicosdk/base.py | 3 +++ 2 files changed, 12 insertions(+) create mode 100644 pypicosdk/_classes/_general.py diff --git a/pypicosdk/_classes/_general.py b/pypicosdk/_classes/_general.py new file mode 100644 index 0000000..cd0801a --- /dev/null +++ b/pypicosdk/_classes/_general.py @@ -0,0 +1,9 @@ +"This file contains general classes for pyPicoSDK" + +from dataclasses import dataclass + + +@dataclass +class BaseDataClass: + "Class containing data for PicoScopeBase" + last_pre_trig: float = 50 diff --git a/pypicosdk/base.py b/pypicosdk/base.py index 1b6434d..6c25980 100644 --- a/pypicosdk/base.py +++ b/pypicosdk/base.py @@ -29,6 +29,7 @@ from .common import ( _get_literal, ) +from ._classes import _general class PicoScopeBase: @@ -66,6 +67,8 @@ def __init__(self, dll_name, *args, **kwargs): self._actual_interval = 0 self.last_used_volt_unit: str = 'mv' + self.base_dataclass = _general.BaseDataClass() + def __exit__(self): self.close_unit() From 4381e837fed0324860105ac90876bf3d4618462e Mon Sep 17 00:00:00 2001 From: JamesPicoTech <118216728+JamesPicoTech@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:04:11 +0100 Subject: [PATCH 11/11] Updated get_time_axis and run_block_capture --- pypicosdk/base.py | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/pypicosdk/base.py b/pypicosdk/base.py index 6c25980..7d83b09 100644 --- a/pypicosdk/base.py +++ b/pypicosdk/base.py @@ -490,7 +490,8 @@ def get_time_axis( Args: timebase (int): PicoScope timebase samples (int): Number of samples captured - pre_trig_percent (int): Percent to offset the 0 point by. If None, default is 0. + pre_trig_percent (int): Percent to offset the 0 point by. If None, defaults to last + used pre_trig_percent or 50. unit (str): Unit of seconds the time axis is returned in. Default is 'ns' (nanoseconds). ratio (int): If using a downsampling ratio, this will scale the time interval @@ -499,15 +500,24 @@ def get_time_axis( Returns: np.ndarray: Array of time values in nano-seconds """ + # Check and save pre_trig to base dataclass + if pre_trig_percent == None: + pre_trig_percent = self.base_dataclass.last_pre_trig + self.base_dataclass.last_pre_trig = pre_trig_percent + + # Default to 1 when using ratio ratio = max(1, ratio) + + # Get unit scalar value scalar = cst.TimeUnitStd_M['ns'] / cst.TimeUnitStd_M[unit] + + # Get the interval for the specified timebase/samples interval = self.get_timebase(timebase, samples)['Interval(ns)'] * ratio / scalar + + # Maths time_axis = np.arange(samples) * interval - if pre_trig_percent is None: - return time_axis - else: - offset = time_axis.max() * (pre_trig_percent / 100) - return time_axis - offset + offset = time_axis.max() * (pre_trig_percent / 100) + return time_axis - offset def realign_downsampled_data( self, @@ -1888,7 +1898,13 @@ def run_simple_rapid_block_capture( # Return data return channel_buffer, time_axis - def run_block_capture(self, timebase, samples, pre_trig_percent=50, segment=0) -> int: + def run_block_capture( + self, + timebase: int, + samples: int, + pre_trig_percent: float | None = None, + segment: int = 0, + ) -> int: """ Runs a block capture using the specified timebase and number of samples. @@ -1896,14 +1912,20 @@ def run_block_capture(self, timebase, samples, pre_trig_percent=50, segment=0) - pre-trigger and post-trigger samples. It uses the PicoSDK `RunBlock` function. Args: - timebase (int): Timebase value determining sample interval (refer to PicoSDK guide). - samples (int): Total number of samples to capture. - pre_trig_percent (int, optional): Percentage of samples to capture before the trigger. - segment (int, optional): Memory segment index to use. + timebase (int): Timebase value determining sample interval (refer to PicoSDK guide). + samples (int): Total number of samples to capture. + pre_trig_percent (int | None, optional): + Percentage of samples to capture before the trigger. If None, defaults to + last called pre_trig_percent or 50. + segment (int, optional): Memory segment index to use. Returns: - int: Estimated time (in milliseconds) the device will be busy capturing data. + int: Estimated time (in milliseconds) the device will be busy capturing data. """ + # Check and add pre-trig to base dataclass + if pre_trig_percent is None: + pre_trig_percent = self.base_dataclass.last_pre_trig + self.base_dataclass.last_pre_trig = pre_trig_percent pre_samples = int((samples * pre_trig_percent) / 100) post_samples = int(samples - pre_samples)