From c91c2fc38c9a34061512060f122b250a098293f7 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 28 May 2025 14:29:31 +0200 Subject: [PATCH 001/129] Fix missing _ when making Python internal/protected functions from non snake_case names --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index 289b0c4..8800287 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -103,7 +103,7 @@ def make_symbol(self, python_name, c_name, return_type, argument_types, docstrin # AND if the function is camel case, add an "underscore-ized" version: if python_name.lower() != python_name: acc = [] - for c in python_name[1:]: + for c in python_name: # Be careful to exclude both digits (lower index) and lower case (higher index). if ord('A') <= ord(c) <= ord('Z'): c = "_" + c.lower() From a697cb004938c3b3f8708a4ea36ed9a7198cb4e5 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 28 May 2025 15:45:44 +0200 Subject: [PATCH 002/129] Fix bug when character has _ as prefix eg ['_', '_i'] is not ['_', '_'] --- picosdk/library.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 8800287..0115211 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -103,13 +103,16 @@ def make_symbol(self, python_name, c_name, return_type, argument_types, docstrin # AND if the function is camel case, add an "underscore-ized" version: if python_name.lower() != python_name: acc = [] + if python_name.startswith('_'): + python_name = python_name[1:] for c in python_name: # Be careful to exclude both digits (lower index) and lower case (higher index). if ord('A') <= ord(c) <= ord('Z'): c = "_" + c.lower() acc.append(c) - if acc[:2] == ['_', '_']: - acc = acc[1:] + new_python_name = "".join(acc) + if not new_python_name.startswith('_'): + new_python_name = "_" + new_python_name setattr(self, "".join(acc), c_function) def list_units(self): From 84c532c4554f030f28f470e27ef0a3d5bbfc3601 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 28 May 2025 15:57:30 +0200 Subject: [PATCH 003/129] Use lstrip to remove if statement --- picosdk/library.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 0115211..97220bf 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -103,8 +103,7 @@ def make_symbol(self, python_name, c_name, return_type, argument_types, docstrin # AND if the function is camel case, add an "underscore-ized" version: if python_name.lower() != python_name: acc = [] - if python_name.startswith('_'): - python_name = python_name[1:] + python_name = python_name.lstrip('_') for c in python_name: # Be careful to exclude both digits (lower index) and lower case (higher index). if ord('A') <= ord(c) <= ord('Z'): From 76068f9e3514ab705721c5dc1872c9765a8dc68b Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 30 May 2025 15:55:09 +0200 Subject: [PATCH 004/129] Use snake case function names for ps4000a --- picosdk/ps4000a.py | 156 ++++++++++++++++++++++----------------------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/picosdk/ps4000a.py b/picosdk/ps4000a.py index b5a287c..eed693c 100644 --- a/picosdk/ps4000a.py +++ b/picosdk/ps4000a.py @@ -190,14 +190,14 @@ def process_enum(enum): class PS4000A_USER_PROBE_INTERACTIONS(Structure): _pack_ = 1 _fields_ = [ ("connected", c_uint16), - + ("channel", c_int32), ("enabled", c_uint16), ("probeName", c_uint32), ("requiresPower_", c_uint8), - ("isPowered_", c_uint8), + ("isPowered_", c_uint8), ("status", c_uint32), @@ -289,9 +289,9 @@ class PS4000A_USER_PROBE_INTERACTIONS(Structure): def _define_conditions_info(): PICO_CLEAR = 0x00000001 PICO_ADD = 0x00000002 - + return {k.upper(): v for k, v in locals().items() if k.startswith("PICO")} - + ps4000a.PS4000A_CONDITIONS_INFO = _define_conditions_info() ps4000a.PS4000A_THRESHOLD_DIRECTION = make_enum([ @@ -331,14 +331,14 @@ class PS4000A_CONDITION (Structure): _pack_ = 1 _fields_ = [("source", c_int32), ("condition", c_int32)] - + ps4000a.PS4000A_CONDITION = PS4000A_CONDITION class PS4000A_DIRECTION(Structure): _pack_ = 1 _fields_ = [("channel", c_int32), ("direction", c_int32)] - + ps4000a.PS4000A_DIRECTION = PS4000A_DIRECTION class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): @@ -349,7 +349,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): ("thresholdLowerHysteresis", c_uint16), ("channel", c_int32), ("thresholdMode", c_int32)] - + ps4000a.PS4000A_TRIGGER_CHANNEL_PROPERTIES = PS4000A_TRIGGER_CHANNEL_PROPERTIES doc = """ PICO_STATUS ps4000aOpenUnit @@ -357,14 +357,14 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t *handle, int8_t *serial ); """ -ps4000a.make_symbol("_OpenUnit", "ps4000aOpenUnit", c_uint32, [c_void_p, c_char_p], doc) +ps4000a.make_symbol("_open_unit", "ps4000aOpenUnit", c_uint32, [c_void_p, c_char_p], doc) doc = """ PICO_STATUS ps4000aOpenUnitAsync ( int16_t *status, int8_t *serial ); """ -ps4000a.make_symbol("_OpenUnitAsync", "ps4000aOpenUnitAsync", c_uint32, [c_void_p, c_char_p], doc) +ps4000a.make_symbol("_open_unit_async", "ps4000aOpenUnitAsync", c_uint32, [c_void_p, c_char_p], doc) doc = """ PICO_STATUS ps4000aOpenUnitProgress ( @@ -372,7 +372,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t *progressPercent, int16_t *complete ); """ -ps4000a.make_symbol("_OpenUnitProgress", "ps4000aOpenUnitProgress", c_uint32, [c_void_p, c_void_p, c_void_p], doc) +ps4000a.make_symbol("_open_unit_progress", "ps4000aOpenUnitProgress", c_uint32, [c_void_p, c_void_p, c_void_p], doc) doc = """ PICO_STATUS ps4000aGetUnitInfo ( @@ -382,7 +382,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t *requiredSize, PICO_INFO info ); """ -ps4000a.make_symbol("_GetUnitInfo", "ps4000aGetUnitInfo", c_uint32, [c_int16, c_char_p, c_int16, c_void_p, c_uint32], +ps4000a.make_symbol("_get_unit_info", "ps4000aGetUnitInfo", c_uint32, [c_int16, c_char_p, c_int16, c_void_p, c_uint32], doc) doc = """ PICO_STATUS ps4000aFlashLed @@ -390,7 +390,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t handle, int16_t start ); """ -ps4000a.make_symbol("_FlashLed", "ps4000aFlashLed", c_uint32, [c_int16, c_int16], doc) +ps4000a.make_symbol("_flash_led", "ps4000aFlashLed", c_uint32, [c_int16, c_int16], doc) doc = """ PICO_STATUS ps4000aSetChannelLed ( @@ -398,20 +398,20 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_CHANNEL_LED_SETTING *ledStates, uint16_t nLedStates ); """ -ps4000a.make_symbol("_SetChannelLed", "ps4000aSetChannelLed", c_uint32, [c_int16, c_void_p, c_uint16], doc) +ps4000a.make_symbol("_set_channel_led", "ps4000aSetChannelLed", c_uint32, [c_int16, c_void_p, c_uint16], doc) doc = """ PICO_STATUS ps4000aIsLedFlashing ( int16_t handle, int16_t *status ); """ -ps4000a.make_symbol("_IsLedFlashing", "ps4000aIsLedFlashing", c_uint32, [c_int16, c_void_p], doc) +ps4000a.make_symbol("_is_led_flashing", "ps4000aIsLedFlashing", c_uint32, [c_int16, c_void_p], doc) doc = """ PICO_STATUS ps4000aCloseUnit ( int16_t handle ); """ -ps4000a.make_symbol("_CloseUnit", "ps4000aCloseUnit", c_uint32, [c_int16, ], doc) +ps4000a.make_symbol("_close_unit", "ps4000aCloseUnit", c_uint32, [c_int16, ], doc) doc = """ PICO_STATUS ps4000aMemorySegments ( @@ -419,7 +419,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t nSegments, int32_t *nMaxSamples ); """ -ps4000a.make_symbol("_MemorySegments", "ps4000aMemorySegments", c_uint32, [c_int16, c_uint32, c_void_p], doc) +ps4000a.make_symbol("_memory_segments", "ps4000aMemorySegments", c_uint32, [c_int16, c_uint32, c_void_p], doc) doc = """ PICO_STATUS ps4000aSetChannel ( @@ -430,7 +430,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_RANGE range, float analogOffset ); """ -ps4000a.make_symbol("_SetChannel", "ps4000aSetChannel", c_uint32, +ps4000a.make_symbol("_set_channel", "ps4000aSetChannel", c_uint32, [c_int16, c_int32, c_int16, c_int32, c_int32, c_float], doc) doc = """ PICO_STATUS ps4000aSetBandwidthFilter @@ -439,7 +439,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_CHANNEL channel, PS4000A_BANDWIDTH_LIMITER bandwidth ); """ -ps4000a.make_symbol("_SetBandwidthFilter", "ps4000aSetBandwidthFilter", c_uint32, [c_int16, c_int32, c_int32], doc) +ps4000a.make_symbol("_set_bandwidth_filter", "ps4000aSetBandwidthFilter", c_uint32, [c_int16, c_int32, c_int32], doc) doc = """ PICO_STATUS ps4000aApplyResistanceScaling ( @@ -451,7 +451,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t buffertLth, int16_t *overflow ); """ -ps4000a.make_symbol("_ApplyResistanceScaling", "ps4000aApplyResistanceScaling", c_uint32, +ps4000a.make_symbol("_apply_resistance_scaling", "ps4000aApplyResistanceScaling", c_uint32, [c_int16, c_int32, c_int32, c_int16, c_int16, c_uint32, c_int16], doc) doc = """ PICO_STATUS ps4000aGetTimebase @@ -463,7 +463,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int32_t *maxSamples, uint32_t segmentIndex ); """ -ps4000a.make_symbol('_GetTimebase', 'ps4000aGetTimebase', c_uint32, +ps4000a.make_symbol('_get_timebase', 'ps4000aGetTimebase', c_uint32, [c_int16, c_uint32, c_int32, c_void_p, c_void_p, c_uint32], doc) doc = """ PICO_STATUS ps4000aGetTimebase2 @@ -475,7 +475,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int32_t *maxSamples, uint32_t segmentIndex ); """ -ps4000a.make_symbol("_GetTimebase2", "ps4000aGetTimebase2", c_uint32, +ps4000a.make_symbol("_get_timebase2", "ps4000aGetTimebase2", c_uint32, [c_int16, c_uint32, c_int32, c_void_p, c_void_p, c_uint32], doc) doc = """ PICO_STATUS ps4000aSetSigGenArbitrary @@ -498,7 +498,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_SIGGEN_TRIG_SOURCE triggerSource, int16_t extInThreshold ); """ -ps4000a.make_symbol("_SetSigGenArbitrary", "ps4000aSetSigGenArbitrary", c_uint32, +ps4000a.make_symbol("_set_sig_gen_arbitrary", "ps4000aSetSigGenArbitrary", c_uint32, [c_int16, c_int32, c_uint32, c_uint32, c_uint32, c_uint32, c_uint32, c_void_p, c_int32, c_int32, c_int32, c_int32, c_uint32, c_uint32, c_int32, c_int32, c_int16], doc) @@ -520,7 +520,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_SIGGEN_TRIG_SOURCE triggerSource, int16_t extInThreshold ); """ -ps4000a.make_symbol("_SetSigGenBuiltIn", "ps4000aSetSigGenBuiltIn", c_uint32, +ps4000a.make_symbol("_set_sig_gen_built_in", "ps4000aSetSigGenBuiltIn", c_uint32, [c_int16, c_int32, c_uint32, c_int32, c_double, c_double, c_double, c_double, c_int32, c_int32, c_uint32, c_uint32, c_int32, c_int32, c_int16], doc) @@ -538,7 +538,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_SIGGEN_TRIG_SOURCE triggerSource, int16_t extInThreshold ); """ -ps4000a.make_symbol("_SetSigGenPropertiesArbitrary", "ps4000aSetSigGenPropertiesArbitrary", c_uint32, +ps4000a.make_symbol("_set_sig_gen_properties_arbitrary", "ps4000aSetSigGenPropertiesArbitrary", c_uint32, [c_int16, c_uint32, c_uint32, c_uint32, c_uint32, c_int32, c_uint32, c_uint32, c_int32, c_int32, c_int16], doc) @@ -556,7 +556,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_SIGGEN_TRIG_SOURCE triggerSource, int16_t extInThreshold ); """ -ps4000a.make_symbol("_SetSigGenPropertiesBuiltIn", "ps4000aSetSigGenPropertiesBuiltIn", c_uint32, +ps4000a.make_symbol("_set_sig_gen_properties_built_in", "ps4000aSetSigGenPropertiesBuiltIn", c_uint32, [c_int16, c_double, c_double, c_double, c_double, c_int32, c_uint32, c_uint32, c_int32, c_int32, c_int16], doc) @@ -568,7 +568,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t bufferLength, uint32_t *phase ); """ -ps4000a.make_symbol("_SigGenFrequencyToPhase", "ps4000aSigGenFrequencyToPhase", c_uint32, +ps4000a.make_symbol("_sig_gen_frequency_to_phase", "ps4000aSigGenFrequencyToPhase", c_uint32, [c_int16, c_double, c_int32, c_uint32, c_void_p], doc) doc = """ PICO_STATUS ps4000aSigGenArbitraryMinMaxValues @@ -579,7 +579,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t *minArbitraryWaveformSize, uint32_t *maxArbitraryWaveformSize ); """ -ps4000a.make_symbol("_SigGenArbitraryMinMaxValues", "ps4000aSigGenArbitraryMinMaxValues", c_uint32, +ps4000a.make_symbol("_sig_gen_arbitrary_min_max_values", "ps4000aSigGenArbitraryMinMaxValues", c_uint32, [c_int16, c_void_p, c_void_p, c_void_p, c_void_p], doc) doc = """ PICO_STATUS ps4000aSigGenSoftwareControl @@ -587,7 +587,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t handle, int16_t state );""" -ps4000a.make_symbol("_SigGenSoftwareControl", "ps4000aSigGenSoftwareControl", c_uint32, [c_int16, c_int16], doc) +ps4000a.make_symbol("_sig_gen_software_control", "ps4000aSigGenSoftwareControl", c_uint32, [c_int16, c_int16], doc) doc = """ PICO_STATUS ps4000aSetEts ( @@ -597,7 +597,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t etsInterleave, int32_t *sampleTimePicoseconds ); """ -ps4000a.make_symbol("_SetEts", "ps4000aSetEts", c_uint32, [c_int16, c_int32, c_int16, c_int16, c_void_p], doc) +ps4000a.make_symbol("_set_ets", "ps4000aSetEts", c_uint32, [c_int16, c_int32, c_int16, c_int16, c_void_p], doc) doc = """ PICO_STATUS ps4000aSetTriggerChannelProperties ( @@ -607,7 +607,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t auxOutputEnable, int32_t autoTriggerMilliseconds ); """ -ps4000a.make_symbol("_SetTriggerChannelProperties", "ps4000aSetTriggerChannelProperties", c_uint32, +ps4000a.make_symbol("_set_trigger_channel_properties", "ps4000aSetTriggerChannelProperties", c_uint32, [c_int16, c_void_p, c_int16, c_int16, c_int32], doc) doc = """ PICO_STATUS ps4000aSetTriggerChannelConditions @@ -617,7 +617,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t nConditions, PS4000A_CONDITIONS_INFO info ); """ -ps4000a.make_symbol("_SetTriggerChannelConditions", "ps4000aSetTriggerChannelConditions", c_uint32, +ps4000a.make_symbol("_set_trigger_channel_conditions", "ps4000aSetTriggerChannelConditions", c_uint32, [c_int16, c_void_p, c_int16, c_int32], doc) doc = """ PICO_STATUS ps4000aSetTriggerChannelDirections @@ -626,7 +626,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_DIRECTION *directions, int16_t nDirections ); """ -ps4000a.make_symbol("_SetTriggerChannelDirections", "ps4000aSetTriggerChannelDirections", c_uint32, +ps4000a.make_symbol("_set_trigger_channel_directions", "ps4000aSetTriggerChannelDirections", c_uint32, [c_int16, c_void_p, c_int16], doc) doc = """ PICO_STATUS ps4000aSetSimpleTrigger @@ -639,7 +639,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t delay, int16_t autoTrigger_ms ); """ -ps4000a.make_symbol("_SetSimpleTrigger", "ps4000aSetSimpleTrigger", c_uint32, +ps4000a.make_symbol("_set_simple_trigger", "ps4000aSetSimpleTrigger", c_uint32, [c_int16, c_int16, c_int32, c_int16, c_int32, c_uint32, c_int16], doc) doc = """ PICO_STATUS ps4000aSetTriggerDelay @@ -647,7 +647,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t handle, uint32_t delay ); """ -ps4000a.make_symbol("_SetTriggerDelay", "ps4000aSetTriggerDelay", c_uint32, [c_int16, c_uint32], doc) +ps4000a.make_symbol("_set_trigger_delay", "ps4000aSetTriggerDelay", c_uint32, [c_int16, c_uint32], doc) doc = """ PICO_STATUS ps4000aSetPulseWidthQualifierProperties ( @@ -657,7 +657,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t upper, PS4000A_PULSE_WIDTH_TYPE type ); """ -ps4000a.make_symbol("_SetPulseWidthQualifierProperties", "ps4000aSetPulseWidthQualifierProperties", c_uint32, +ps4000a.make_symbol("_set_pulse_width_qualifier_properties", "ps4000aSetPulseWidthQualifierProperties", c_uint32, [c_int16, c_int32, c_uint32, c_uint32, c_int32], doc) doc = """ PICO_STATUS ps4000aSetPulseWidthQualifierConditions @@ -667,7 +667,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t nConditions, PS4000A_CONDITIONS_INFO info ); """ -ps4000a.make_symbol("_SetPulseWidthQualifierConditions", "ps4000aSetPulseWidthQualifierConditions", c_uint32, +ps4000a.make_symbol("_set_pulse_width_qualifier_conditions", "ps4000aSetPulseWidthQualifierConditions", c_uint32, [c_int16, c_void_p, c_int16, c_int32], doc) doc = """ PICO_STATUS ps4000aIsTriggerOrPulseWidthQualifierEnabled @@ -676,7 +676,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t *triggerEnabled, int16_t *pulseWidthQualifierEnabled ); """ -ps4000a.make_symbol("_IsTriggerOrPulseWidthQualifierEnabled", "ps4000aIsTriggerOrPulseWidthQualifierEnabled", c_uint32, +ps4000a.make_symbol("_is_trigger_or_pulse_width_qualifier_enabled", "ps4000aIsTriggerOrPulseWidthQualifierEnabled", c_uint32, [c_int16, c_void_p, c_void_p], doc) doc = """ PICO_STATUS ps4000aGetTriggerTimeOffset @@ -687,7 +687,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_TIME_UNITS *timeUnits, uint32_t segmentIndex ); """ -ps4000a.make_symbol("_GetTriggerTimeOffset", "ps4000aGetTriggerTimeOffset", c_uint32, +ps4000a.make_symbol("_get_trigger_time_offset", "ps4000aGetTriggerTimeOffset", c_uint32, [c_int16, c_void_p, c_void_p, c_void_p, c_uint32], doc) doc = """ PICO_STATUS ps4000aGetTriggerTimeOffset64 @@ -697,7 +697,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_TIME_UNITS *timeUnits, uint32_t segmentIndex ); """ -ps4000a.make_symbol("_GetTriggerTimeOffset64", "ps4000aGetTriggerTimeOffset64", c_uint32, +ps4000a.make_symbol("_get_trigger_time_offset64", "ps4000aGetTriggerTimeOffset64", c_uint32, [c_int16, c_void_p, c_void_p, c_uint32], doc) doc = """ PICO_STATUS ps4000aGetValuesTriggerTimeOffsetBulk @@ -709,7 +709,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t fromSegmentIndex, uint32_t toSegmentIndex ); """ -ps4000a.make_symbol("_GetValuesTriggerTimeOffsetBulk", "ps4000aGetValuesTriggerTimeOffsetBulk", c_uint32, +ps4000a.make_symbol("_get_values_trigger_time_offset_bulk", "ps4000aGetValuesTriggerTimeOffsetBulk", c_uint32, [c_int16, c_void_p, c_void_p, c_void_p, c_uint32, c_uint32], doc) doc = """ PICO_STATUS ps4000aGetValuesTriggerTimeOffsetBulk64 @@ -720,7 +720,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t fromSegmentIndex, uint32_t toSegmentIndex ); """ -ps4000a.make_symbol("_GetValuesTriggerTimeOffsetBulk64", "ps4000aGetValuesTriggerTimeOffsetBulk64", c_uint32, +ps4000a.make_symbol("_get_values_trigger_time_offset_bulk64", "ps4000aGetValuesTriggerTimeOffsetBulk64", c_uint32, [c_int16, c_void_p, c_void_p, c_uint32, c_uint32], doc) doc = """ PICO_STATUS ps4000aSetDataBuffers @@ -733,7 +733,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t segmentIndex, PS4000A_RATIO_MODE mode ); """ -ps4000a.make_symbol("_SetDataBuffers", "ps4000aSetDataBuffers", c_uint32, +ps4000a.make_symbol("_set_data_buffers", "ps4000aSetDataBuffers", c_uint32, [c_int16, c_int32, c_void_p, c_void_p, c_int32, c_uint32, c_int32], doc) doc = """ PICO_STATUS ps4000aSetDataBuffer @@ -745,7 +745,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t segmentIndex, PS4000A_RATIO_MODE mode ); """ -ps4000a.make_symbol("_SetDataBuffer", "ps4000aSetDataBuffer", c_uint32, +ps4000a.make_symbol("_set_data_buffer", "ps4000aSetDataBuffer", c_uint32, [c_int16, c_int32, c_void_p, c_int32, c_uint32, c_int32], doc) doc = """ PICO_STATUS ps4000aSetEtsTimeBuffer @@ -754,7 +754,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int64_t *buffer, int32_t bufferLth ); """ -ps4000a.make_symbol("_SetEtsTimeBuffer", "ps4000aSetEtsTimeBuffer", c_uint32, [c_int16, c_void_p, c_int32], doc) +ps4000a.make_symbol("_set_ets_time_buffer", "ps4000aSetEtsTimeBuffer", c_uint32, [c_int16, c_void_p, c_int32], doc) doc = """ PICO_STATUS ps4000aSetEtsTimeBuffers ( @@ -763,7 +763,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t *timeLower, int32_t bufferLth ); """ -ps4000a.make_symbol("_SetEtsTimeBuffers", "ps4000aSetEtsTimeBuffers", c_uint32, [c_int16, c_void_p, c_void_p, c_int32], +ps4000a.make_symbol("_set_ets_time_buffers", "ps4000aSetEtsTimeBuffers", c_uint32, [c_int16, c_void_p, c_void_p, c_int32], doc) doc = """ PICO_STATUS ps4000aIsReady @@ -771,7 +771,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t handle, int16_t *ready ); """ -ps4000a.make_symbol("_IsReady", "ps4000aIsReady", c_uint32, [c_int16, c_void_p], doc) +ps4000a.make_symbol("_is_ready", "ps4000aIsReady", c_uint32, [c_int16, c_void_p], doc) doc = """ PICO_STATUS ps4000aRunBlock ( @@ -784,7 +784,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): ps4000aBlockReady lpReady, void *pParameter ); """ -ps4000a.make_symbol("_RunBlock", "ps4000aRunBlock", c_uint32, +ps4000a.make_symbol("_run_block", "ps4000aRunBlock", c_uint32, [c_int16, c_int32, c_int32, c_uint32, c_void_p, c_uint32, c_void_p, c_void_p], doc) doc = """ PICO_STATUS ps4000aRunStreaming @@ -799,7 +799,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_RATIO_MODE downSampleRatioMode, uint32_t overviewBufferSize ); """ -ps4000a.make_symbol("_RunStreaming", "ps4000aRunStreaming", c_uint32, +ps4000a.make_symbol("_run_streaming", "ps4000aRunStreaming", c_uint32, [c_int16, c_void_p, c_int32, c_uint32, c_uint32, c_int16, c_uint32, c_int32, c_uint32], doc) doc = """ PICO_STATUS ps4000aGetStreamingLatestValues @@ -808,7 +808,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): ps4000aStreamingReady lpPs4000aReady, void *pParameter ); """ -ps4000a.make_symbol("_GetStreamingLatestValues", "ps4000aGetStreamingLatestValues", c_uint32, +ps4000a.make_symbol("_get_streaming_latest_values", "ps4000aGetStreamingLatestValues", c_uint32, [c_int16, c_void_p, c_void_p], doc) doc = """ void *ps4000aStreamingReady @@ -843,7 +843,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t handle, uint32_t *noOfValues ); """ -ps4000a.make_symbol("_NoOfStreamingValues", "ps4000aNoOfStreamingValues", c_uint32, [c_int16, c_void_p], doc) +ps4000a.make_symbol("_no_of_streaming_values", "ps4000aNoOfStreamingValues", c_uint32, [c_int16, c_void_p], doc) doc = """ PICO_STATUS ps4000aGetMaxDownSampleRatio ( @@ -853,7 +853,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_RATIO_MODE downSampleRatioMode, uint32_t segmentIndex ); """ -ps4000a.make_symbol("_GetMaxDownSampleRatio", "ps4000aGetMaxDownSampleRatio", c_uint32, +ps4000a.make_symbol("_get_max_down_sample_ratio", "ps4000aGetMaxDownSampleRatio", c_uint32, [c_int16, c_uint32, c_void_p, c_int32, c_uint32], doc) doc = """ PICO_STATUS ps4000aGetValues @@ -866,7 +866,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t segmentIndex, int16_t *overflow ); """ -ps4000a.make_symbol("_GetValues", "ps4000aGetValues", c_uint32, +ps4000a.make_symbol("_get_values", "ps4000aGetValues", c_uint32, [c_int16, c_uint32, c_void_p, c_uint32, c_int32, c_uint32, c_void_p], doc) doc = """ PICO_STATUS ps4000aGetValuesAsync @@ -880,7 +880,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): void *lpDataReady, void *pParameter ); """ -ps4000a.make_symbol("_GetValuesAsync", "ps4000aGetValuesAsync", c_uint32, +ps4000a.make_symbol("_get_values_async", "ps4000aGetValuesAsync", c_uint32, [c_int16, c_uint32, c_uint32, c_uint32, c_int32, c_uint32, c_void_p, c_void_p], doc) doc = """ PICO_STATUS ps4000aGetValuesBulk @@ -893,7 +893,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_RATIO_MODE downSampleRatioMode, int16_t *overflow ); """ -ps4000a.make_symbol("_GetValuesBulk", "ps4000aGetValuesBulk", c_uint32, +ps4000a.make_symbol("_get_values_bulk", "ps4000aGetValuesBulk", c_uint32, [c_int16, c_void_p, c_uint32, c_uint32, c_uint32, c_int32, c_void_p], doc) doc = """ PICO_STATUS ps4000aGetValuesOverlapped @@ -906,7 +906,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t segmentIndex, int16_t *overflow ); """ -ps4000a.make_symbol("_GetValuesOverlapped", "ps4000aGetValuesOverlapped", c_uint32, +ps4000a.make_symbol("_get_values_overlapped", "ps4000aGetValuesOverlapped", c_uint32, [c_int16, c_uint32, c_void_p, c_uint32, c_int32, c_uint32, c_void_p], doc) doc = """ PICO_STATUS ps4000aGetValuesOverlappedBulk @@ -920,7 +920,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): uint32_t toSegmentIndex, int16_t *overflow ); """ -ps4000a.make_symbol("_GetValuesOverlappedBulk", "ps4000aGetValuesOverlappedBulk", c_uint32, +ps4000a.make_symbol("_get_values_overlapped_bulk", "ps4000aGetValuesOverlappedBulk", c_uint32, [c_int16, c_uint32, c_void_p, c_uint32, c_int32, c_uint32, c_uint32, c_void_p], doc) doc = """ PICO_STATUS ps4000aEnumerateUnits @@ -929,7 +929,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int8_t *serials, int16_t *serialLth ); """ -ps4000a.make_symbol("_EnumerateUnits", "ps4000aEnumerateUnits", c_uint32, [c_void_p, c_void_p, c_void_p], doc) +ps4000a.make_symbol("_enumerate_units", "ps4000aEnumerateUnits", c_uint32, [c_void_p, c_void_p, c_void_p], doc) doc = """ PICO_STATUS ps4000aGetChannelInformation ( @@ -940,7 +940,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int32_t *length, int32_t channels ); """ -ps4000a.make_symbol("_GetChannelInformation", "ps4000aGetChannelInformation", c_uint32, +ps4000a.make_symbol("_get_channel_information", "ps4000aGetChannelInformation", c_uint32, [c_int16, c_int32, c_int32, c_void_p, c_void_p, c_int32], doc) doc = """ PICO_STATUS ps4000aConnectDetect @@ -949,21 +949,21 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_CONNECT_DETECT *sensor, int16_t nSensors ); """ -ps4000a.make_symbol("_ConnectDetect", "ps4000aConnectDetect", c_uint32, [c_int16, c_void_p, c_int16], doc) +ps4000a.make_symbol("_connect_detect", "ps4000aConnectDetect", c_uint32, [c_int16, c_void_p, c_int16], doc) doc = """ PICO_STATUS ps4000aMaximumValue ( int16_t handle, int16_t *value ); """ -ps4000a.make_symbol("_MaximumValue", "ps4000aMaximumValue", c_uint32, [c_int16, c_void_p], doc) +ps4000a.make_symbol("_maximum_value", "ps4000aMaximumValue", c_uint32, [c_int16, c_void_p], doc) doc = """ PICO_STATUS ps4000aMinimumValue ( int16_t handle, int16_t * value ); """ -ps4000a.make_symbol("_MinimumValue", "ps4000aMinimumValue", c_uint32, [c_int16, c_void_p], doc) +ps4000a.make_symbol("_minimum_value", "ps4000aMinimumValue", c_uint32, [c_int16, c_void_p], doc) doc = """ PICO_STATUS ps4000aGetAnalogueOffset ( @@ -973,7 +973,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): float *maximumVoltage, float *minimumVoltage ); """ -ps4000a.make_symbol("_GetAnalogueOffset", "ps4000aGetAnalogueOffset", c_uint32, +ps4000a.make_symbol("_get_analogue_offset", "ps4000aGetAnalogueOffset", c_uint32, [c_int16, c_int32, c_int32, c_void_p, c_void_p], doc) doc = """ PICO_STATUS ps4000aGetMaxSegments @@ -981,53 +981,53 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t handle, uint32_t *maxSegments ); """ -ps4000a.make_symbol("_GetMaxSegments", "ps4000aGetMaxSegments", c_uint32, [c_int16, c_void_p], doc) +ps4000a.make_symbol("_get_max_segments", "ps4000aGetMaxSegments", c_uint32, [c_int16, c_void_p], doc) doc = """ PICO_STATUS ps4000aChangePowerSource ( int16_t handle, PICO_STATUS powerState ); """ -ps4000a.make_symbol("_ChangePowerSource", "ps4000aChangePowerSource", c_uint32, [c_int16, c_uint32], doc) +ps4000a.make_symbol("_change_power_source", "ps4000aChangePowerSource", c_uint32, [c_int16, c_uint32], doc) doc = """ PICO_STATUS ps4000aCurrentPowerSource ( int16_t handle ); """ -ps4000a.make_symbol("_CurrentPowerSource", "ps4000aCurrentPowerSource", c_uint32, [c_int16, ], doc) +ps4000a.make_symbol("_current_power_source", "ps4000aCurrentPowerSource", c_uint32, [c_int16, ], doc) doc = """ PICO_STATUS ps4000aStop ( int16_t handle ); """ -ps4000a.make_symbol("_Stop", "ps4000aStop", c_uint32, [c_int16, ], doc) +ps4000a.make_symbol("_stop", "ps4000aStop", c_uint32, [c_int16, ], doc) doc = """ PICO_STATUS ps4000aPingUnit ( int16_t handle ); """ -ps4000a.make_symbol("_PingUnit", "ps4000aPingUnit", c_uint32, [c_int16, ], doc) +ps4000a.make_symbol("_ping_unit", "ps4000aPingUnit", c_uint32, [c_int16, ], doc) doc = """ PICO_STATUS ps4000aSetNoOfCaptures ( int16_t handle, uint32_t nCaptures ); """ -ps4000a.make_symbol("_SetNoOfCaptures", "ps4000aSetNoOfCaptures", c_uint32, [c_int16, c_uint32], doc) +ps4000a.make_symbol("_set_no_of_captures", "ps4000aSetNoOfCaptures", c_uint32, [c_int16, c_uint32], doc) doc = """ PICO_STATUS ps4000aGetNoOfCaptures ( int16_t handle, uint32_t *nCaptures ); """ -ps4000a.make_symbol("_GetNoOfCaptures", "ps4000aGetNoOfCaptures", c_uint32, [c_int16, c_void_p], doc) +ps4000a.make_symbol("_get_no_of_captures", "ps4000aGetNoOfCaptures", c_uint32, [c_int16, c_void_p], doc) doc = """ PICO_STATUS ps4000aGetNoOfProcessedCaptures ( int16_t handle, uint32_t *nProcessedCaptures ); """ -ps4000a.make_symbol("_GetNoOfProcessedCaptures", "ps4000aGetNoOfProcessedCaptures", c_uint32, [c_int16, c_void_p], doc) +ps4000a.make_symbol("_get_no_of_processed_captures", "ps4000aGetNoOfProcessedCaptures", c_uint32, [c_int16, c_void_p], doc) doc = """ PICO_STATUS ps4000aDeviceMetaData ( @@ -1038,7 +1038,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): PS4000A_META_OPERATION operation, PS4000A_META_FORMAT format ); """ -ps4000a.make_symbol("_DeviceMetaData", "ps4000aDeviceMetaData", c_uint32, +ps4000a.make_symbol("_device_meta_data", "ps4000aDeviceMetaData", c_uint32, [c_int16, c_void_p, c_void_p, c_int32, c_int32, c_int32], doc) doc = """ PICO_STATUS ps4000aGetString @@ -1048,14 +1048,14 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int8_t *string, int32_t *stringLength ); """ -ps4000a.make_symbol("_GetString", "ps4000aGetString", c_uint32, [c_int16, c_int32, c_void_p, c_void_p], doc) +ps4000a.make_symbol("_get_string", "ps4000aGetString", c_uint32, [c_int16, c_int32, c_void_p, c_void_p], doc) doc = """ PICO_STATUS ps4000aGetCommonModeOverflow ( int16_t handle, uint16_t *overflow ); """ -ps4000a.make_symbol("_GetCommonModeOverflow", "ps4000aGetCommonModeOverflow", c_uint32, [c_int16, c_void_p], doc) +ps4000a.make_symbol("_get_common_mode_overflow", "ps4000aGetCommonModeOverflow", c_uint32, [c_int16, c_void_p], doc) doc = """ PICO_STATUS ps4000aSetFrequencyCounter ( @@ -1066,7 +1066,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int16_t thresholdMajor, int16_t thresholdMinor ); """ -ps4000a.make_symbol("_SetFrequencyCounter", "ps4000aSetFrequencyCounter", c_uint32, +ps4000a.make_symbol("_set_frequency_counter", "ps4000aSetFrequencyCounter", c_uint32, [c_int16, c_int32, c_int16, c_int32, c_int16, c_int16], doc) doc = """ PICO_STATUS ps4000aOpenUnitWithResolution @@ -1075,28 +1075,28 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): int8_t *serial, PS4000A_DEVICE_RESOLUTION resolution ); """ -ps4000a.make_symbol("_OpenUnitWithResolution", "ps4000aOpenUnitWithResolution", c_uint32, [c_void_p, c_void_p, c_int32], doc) +ps4000a.make_symbol("_open_unit_with_resolution", "ps4000aOpenUnitWithResolution", c_uint32, [c_void_p, c_void_p, c_int32], doc) doc = """ PICO_STATUS ps4000aGetDeviceResolution ( int16_t handle, PS4000A_DEVICE_RESOLUTION *resolution ); """ -ps4000a.make_symbol("_GetResolution", "ps4000aGetDeviceResolution", c_uint32, [c_int16, c_void_p], doc) +ps4000a.make_symbol("_get_resolution", "ps4000aGetDeviceResolution", c_uint32, [c_int16, c_void_p], doc) doc = """ PICO_STATUS ps4000aSetDeviceResolution ( int16_t handle, PS4000A_DEVICE_RESOLUTION resolution ); """ -ps4000a.make_symbol("_SetResolution", "ps4000aSetDeviceResolution", c_uint32, [c_int16, c_int32], doc) +ps4000a.make_symbol("_set_resolution", "ps4000aSetDeviceResolution", c_uint32, [c_int16, c_int32], doc) doc = """ PICO_STATUS ps4000aSetProbeInteractionCallback ( int16_t handle, ps4000aProbeInteractions callback ); """ -ps4000a.make_symbol("_SetProbeInteractionCallback", "ps4000aSetProbeInteractionCallback", c_uint32, [c_int16, c_void_p], doc) +ps4000a.make_symbol("_set_probe_interaction_callback", "ps4000aSetProbeInteractionCallback", c_uint32, [c_int16, c_void_p], doc) doc = """ void *ps4000aProbeInteractions ( From b52c3ef7bd0b3b9231b5dd1fdabb9b7a94e45737 Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 30 May 2025 15:56:17 +0200 Subject: [PATCH 005/129] Add correct channels for ps4000a to the PICO_CHANNEL variable --- picosdk/ps4000a.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/picosdk/ps4000a.py b/picosdk/ps4000a.py index eed693c..ff2626a 100644 --- a/picosdk/ps4000a.py +++ b/picosdk/ps4000a.py @@ -43,9 +43,14 @@ def __init__(self): "PS4000A_PULSE_WIDTH_SOURCE" : 0x10000000 } -ps4000a.PICO_CHANNEL = {k[19:]: v for k, v in ps4000a.PS4000A_CHANNEL.items()} - - +ps4000a.PICO_CHANNEL = {} +prefix = "PS4000A_CHANNEL_" +for key, value in ps4000a.PS4000A_CHANNEL.items(): + if isinstance(key, tuple): + key = key[0] # Use the first name in the tuple. + if key.startswith(prefix): + key = key[len(prefix):] + ps4000a.PICO_CHANNEL[key] = value # Only channels are included (A, B, C, D, E, F, G, H). # The voltage ranges for this driver are so oddly defined, that it is easier to describe them as a literal than trying # to use make_enum: From 7c96dca4780ad92eeb46e252c713e86b7674c8d1 Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 30 May 2025 18:20:07 +0200 Subject: [PATCH 006/129] Add missing enums for ps3000a --- picosdk/ps3000a.py | 56 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/picosdk/ps3000a.py b/picosdk/ps3000a.py index c9f4768..bea7569 100644 --- a/picosdk/ps3000a.py +++ b/picosdk/ps3000a.py @@ -63,13 +63,6 @@ def __init__(self): for k, v in ps3000a.PS3000A_RANGE.items() if k != "PS3000A_MAX_RANGES" } -ps3000a.PS3000A_RATIO_MODE = { - 'PS3000A_RATIO_MODE_NONE': 0, - 'PS3000A_RATIO_MODE_AGGREGATE': 1, - 'PS3000A_RATIO_MODE_DECIMATE': 2, - 'PS3000A_RATIO_MODE_AVERAGE': 4, -} - ps3000a.PS3000A_TIME_UNITS = make_enum([ 'PS3000A_FS', 'PS3000A_PS', @@ -80,6 +73,55 @@ def __init__(self): 'PS3000A_MAX_TIME_UNITS', ]) +ps3000a.PS3000A_WAVE_TYPE = make_enum([ + "PS3000A_SINE", + "PS3000A_SQUARE", + "PS3000A_TRIANGLE", + "PS3000A_DC_VOLTAGE", + "PS3000A_RAMP_UP", + "PS3000A_RAMP_DOWN", + "PS3000A_SINC", + "PS3000A_GAUSSIAN", + "PS3000A_HALF_SINE", +]) + +ps3000a.PS3000A_SWEEP_TYPE = make_enum([ + "PS3000A_UP", + "PS3000A_DOWN", + "PS3000A_UPDOWN", + "PS3000A_DOWNUP", +]) + +ps3000a.PS3000A_EXTRA_OPERATIONS = make_enum([ + 'PS3000A_ES_OFF', + 'PS3000A_WHITENOISE', + 'PS3000A_PRBS', +]) + +ps3000a.PS3000A_SIGGEN_TRIG_TYPE = make_enum([ + 'PS3000A_SIGGEN_RISING', + 'PS3000A_SIGGEN_FALLING', + 'PS3000A_SIGGEN_GATE_HIGH', + 'PS3000A_SIGGEN_GATE_LOW', +]) + +ps3000a.PS3000A_SIGGEN_TRIG_SOURCE = make_enum([ + 'PS3000A_SIGGEN_NONE', + 'PS3000A_SIGGEN_SCOPE_TRIG', + 'PS3000A_SIGGEN_EXT_IN', + 'PS3000A_SIGGEN_SOFT_TRIG', + 'PS3000A_SIGGEN_TRIGGER_RAW', +]) + +ps3000a.PS3000A_RATIO_MODE = make_enum([ + 'PS3000A_RATIO_MODE_NONE', + 'PS3000A_RATIO_MODE_AGGREGATE', + 'PS3000A_RATIO_MODE_AVERAGE', + 'PS3000A_RATIO_MODE_DECIMATE', +]) + +ps3000a.PICO_RATIO_MODE = {k[19:]: v for k, v in ps3000a.PS3000A_RATIO_MODE.items()} + ps3000a.PS3000A_DIGITAL_CHANNEL = make_enum([ "PS3000A_DIGITAL_CHANNEL_0", "PS3000A_DIGITAL_CHANNEL_1", From 9aa0c66210343cbe02cba8e46a61c46141314ff5 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 2 Jun 2025 15:19:28 +0200 Subject: [PATCH 007/129] Add set_digital_port function --- picosdk/library.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index 97220bf..414b379 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -16,7 +16,7 @@ import picosdk.constants as constants import numpy -from picosdk.errors import CannotFindPicoSDKError, CannotOpenPicoSDKError, DeviceNotFoundError, \ +from picosdk.errors import PicoError, CannotFindPicoSDKError, CannotOpenPicoSDKError, DeviceNotFoundError, \ ArgumentOutOfRangeError, ValidRangeEnumValueNotValidForThisDevice, DeviceCannotSegmentMemoryError, \ InvalidMemorySegmentsError, InvalidTimebaseError, InvalidTriggerParameters, InvalidCaptureParameters @@ -309,6 +309,33 @@ def set_channel(self, device, channel_name='A', enabled=True, coupling='DC', ran return max_voltage + @requires_device("set_digital_port requires a picosdk.device.Device instance, passed to the correct owning driver.") + def set_digital_port(self, device, port_number=0, enabled=True, logic_level=0): + """Set the digital port + + Args: + port_number (int): identifies the port for digital data. (e.g. 0 for digital channels 0-7) + enabled (bool): whether or not to enable the channel (boolean) + logic_level (int): the voltage at which the state transitions between 0 and 1. + Range: –32767 (–5 V) to 32767 (+5 V). + Raises: + NotImplementedError: This device doesn't support digital ports. + PicoError: set_digital_port failed + """ + if hasattr(self, '_set_digital_port') and len(self._set_digital_port.argtypes) == 4: + digital_ports = getattr(self, self.name.upper() + '_DIGITAL_PORT', None) + if not digital_ports: + raise NotImplementedError("This device doesn't support digital ports") + port_id = digital_ports[self.name.upper() + "_DIGITAL_PORT" + str(port_number)] + args = (device.handle, port_id, enabled, logic_level) + converted_args = self._convert_args(self._set_digital_port, args) + status = self._set_digital_port(*converted_args) + if status != self.PICO_STATUS['PICO_OK']: + raise PicoError( + f"set_digital_port failed ({constants.pico_tag(status)})") + else: + raise NotImplementedError("This device doesn't support digital ports or is not implemented yet") + def _resolve_range(self, signal_peak, exclude=()): # we use >= so that someone can specify the range they want precisely. # we allow exclude so that if the smallest range in the header file isn't available on this device (or in this From 078d949b51394c9938c9c224717ac2cd7650c716 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 2 Jun 2025 15:57:44 +0200 Subject: [PATCH 008/129] Use f-string formatting for error handling --- picosdk/library.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 414b379..9a6c29f 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -359,8 +359,8 @@ def _python_set_channel(self, handle, channel_id, enabled, coupling_id, range_id c_int16(coupling_id), c_int16(range_id)) if return_code == 0: - raise ValidRangeEnumValueNotValidForThisDevice("%sV is out of range for this device." % ( - self.PICO_VOLTAGE_RANGE[range_id])) + raise ValidRangeEnumValueNotValidForThisDevice( + f"{self.PICO_VOLTAGE_RANGE[range_id]}V is out of range for this device.") elif len(self._set_channel.argtypes) == 5 and self._set_channel.argtypes[1] == c_int32 or ( len(self._set_channel.argtypes) == 6): status = self.PICO_STATUS['PICO_OK'] @@ -383,13 +383,12 @@ def _python_set_channel(self, handle, channel_id, enabled, coupling_id, range_id c_int32(range_id)) if status != self.PICO_STATUS['PICO_OK']: if status == self.PICO_STATUS['PICO_INVALID_VOLTAGE_RANGE']: - raise ValidRangeEnumValueNotValidForThisDevice("%sV is out of range for this device." % ( - self.PICO_VOLTAGE_RANGE[range_id])) + raise ValidRangeEnumValueNotValidForThisDevice( + f"{self.PICO_VOLTAGE_RANGE[range_id]}V is out of range for this device.") if status == self.PICO_STATUS['PICO_INVALID_CHANNEL'] and not enabled: # don't throw errors if the user tried to disable a missing channel. return - raise ArgumentOutOfRangeError("problem configuring channel (%s)" % constants.pico_tag(status)) - + raise ArgumentOutOfRangeError(f"problem configuring channel ({constants.pico_tag(status)})") else: raise NotImplementedError("not done other driver types yet") @@ -450,7 +449,7 @@ def _python_get_timebase(self, handle, timebase_id, no_of_samples, oversample, s byref(max_samples), c_uint32(segment_index)) if status != self.PICO_STATUS['PICO_OK']: - raise InvalidTimebaseError("get_timebase2 failed (%s)" % constants.pico_tag(status)) + raise InvalidTimebaseError(f"get_timebase2 failed ({constants.pico_tag(status)})") return TimebaseInfo(timebase_id, time_interval.value, None, max_samples.value, segment_index) else: @@ -479,7 +478,7 @@ def set_null_trigger(self, device): c_uint32(0), c_int16(auto_trigger_after_millis)) if status != self.PICO_STATUS['PICO_OK']: - raise InvalidTriggerParameters("set_simple_trigger failed (%s)" % constants.pico_tag(status)) + raise InvalidTriggerParameters(f"set_simple_trigger failed ({constants.pico_tag(status)})") else: raise NotImplementedError("not done other driver types yet") @@ -515,7 +514,7 @@ def _python_run_block(self, handle, pre_samples, post_samples, timebase_id, over None, None) if status != self.PICO_STATUS['PICO_OK']: - raise InvalidCaptureParameters("run_block failed (%s)" % constants.pico_tag(status)) + raise InvalidCaptureParameters(f"run_block failed ({constants.pico_tag(status)})") else: raise NotImplementedError("not done other driver types yet") @@ -576,7 +575,7 @@ def get_values(self, device, active_channels, num_samples, segment_index=0): c_uint32(segment_index), c_int32(self.PICO_RATIO_MODE['NONE'])) if status != self.PICO_STATUS['PICO_OK']: - raise InvalidCaptureParameters("set_data_buffer failed (%s)" % constants.pico_tag(status)) + raise InvalidCaptureParameters(f"set_data_buffer failed ({constants.pico_tag(status)})") samples_collected = c_uint32(num_samples) status = self._get_values(c_int16(device.handle), @@ -587,7 +586,7 @@ def get_values(self, device, active_channels, num_samples, segment_index=0): c_uint32(segment_index), byref(overflow)) if status != self.PICO_STATUS['PICO_OK']: - raise InvalidCaptureParameters("get_values failed (%s)" % constants.pico_tag(status)) + raise InvalidCaptureParameters(f"get_values failed ({constants.pico_tag(status)})") overflow_warning = {} if overflow.value: @@ -607,4 +606,4 @@ def stop(self, device): else: status = self._stop(c_int16(device.handle)) if status != self.PICO_STATUS['PICO_OK']: - raise InvalidCaptureParameters("stop failed (%s)" % constants.pico_tag(status)) + raise InvalidCaptureParameters(f"stop failed ({constants.pico_tag(status)})") From 344bf1c4c2e7bb40535da27bdf78bd86cb9512ca Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 2 Jun 2025 15:58:20 +0200 Subject: [PATCH 009/129] Add function to convert arguments to the correct c types --- picosdk/library.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/picosdk/library.py b/picosdk/library.py index 9a6c29f..da71d96 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -607,3 +607,29 @@ def stop(self, device): status = self._stop(c_int16(device.handle)) if status != self.PICO_STATUS['PICO_OK']: raise InvalidCaptureParameters(f"stop failed ({constants.pico_tag(status)})") + + def _convert_args(self, func, args): + """Convert arguments to match function argtypes. + + Args: + func: The C function with argtypes defined + args: Tuple of arguments to convert + + Returns: + Tuple of converted arguments matching argtypes + """ + if not hasattr(func, 'argtypes'): + return args + + converted = [] + for arg, argtype in zip(args, func.argtypes): + # Handle byref parameters + if argtype == c_void_p and isinstance(arg, (c_int16, c_int32, c_uint32, c_float, c_double)): + converted.append(byref(arg)) + # Handle normal parameters + elif arg is not None: + converted.append(argtype(arg)) + else: + converted.append(None) + return tuple(converted) + From ac88a7704057ac541b260f9eeb5145cc6307233d Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 2 Jun 2025 16:03:59 +0200 Subject: [PATCH 010/129] Use _convert_args to convert arguments to the correct type --- picosdk/library.py | 149 +++++++++++++++++++++------------------------ 1 file changed, 70 insertions(+), 79 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index da71d96..665f795 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -10,7 +10,7 @@ from __future__ import print_function import sys -from ctypes import c_int16, c_int32, c_uint32, c_float, create_string_buffer, byref +from ctypes import c_int16, c_int32, c_uint32, c_float, c_double, c_void_p, create_string_buffer, byref from ctypes.util import find_library import collections import picosdk.constants as constants @@ -353,11 +353,11 @@ def _python_set_channel(self, handle, channel_id, enabled, coupling_id, range_id if len(self._set_channel.argtypes) == 5 and self._set_channel.argtypes[1] == c_int16: if analog_offset is not None: raise ArgumentOutOfRangeError("This device doesn't support analog offset") - return_code = self._set_channel(c_int16(handle), - c_int16(channel_id), - c_int16(enabled), - c_int16(coupling_id), - c_int16(range_id)) + + args = (handle, channel_id, enabled, coupling_id, range_id) + converted_args = self._convert_args(self._set_channel, args) + return_code = self._set_channel(*converted_args) + if return_code == 0: raise ValidRangeEnumValueNotValidForThisDevice( f"{self.PICO_VOLTAGE_RANGE[range_id]}V is out of range for this device.") @@ -367,20 +367,17 @@ def _python_set_channel(self, handle, channel_id, enabled, coupling_id, range_id if len(self._set_channel.argtypes) == 6: if analog_offset is None: analog_offset = 0.0 - status = self._set_channel(c_int16(handle), - c_int32(channel_id), - c_int16(enabled), - c_int32(coupling_id), - c_int32(range_id), - c_float(analog_offset)) + args = (handle, channel_id, enabled, coupling_id, range_id, analog_offset) + converted_args = self._convert_args(self._set_channel, args) + status = self._set_channel(*converted_args) + elif len(self._set_channel.argtypes) == 5 and self._set_channel.argtypes[1] == c_int32: if analog_offset is not None: raise ArgumentOutOfRangeError("This device doesn't support analog offset") - status = self._set_channel(c_int16(handle), - c_int32(channel_id), - c_int16(enabled), - c_int16(coupling_id), - c_int32(range_id)) + args = (handle, channel_id, enabled, coupling_id, range_id) + converted_args = self._convert_args(self._set_channel, args) + status = self._set_channel(*converted_args) + if status != self.PICO_STATUS['PICO_OK']: if status == self.PICO_STATUS['PICO_INVALID_VOLTAGE_RANGE']: raise ValidRangeEnumValueNotValidForThisDevice( @@ -426,28 +423,26 @@ def _python_get_timebase(self, handle, timebase_id, no_of_samples, oversample, s time_interval = c_int32(0) time_units = c_int16(0) max_samples = c_int32(0) - return_code = self._get_timebase(c_int16(handle), - c_int16(timebase_id), - c_int32(no_of_samples), - byref(time_interval), - byref(time_units), - c_int16(oversample), - byref(max_samples)) + + args = (handle, timebase_id, no_of_samples, time_interval, + time_units, oversample, max_samples) + converted_args = self._convert_args(self._get_timebase, args) + return_code = self._get_timebase(*converted_args) + if return_code == 0: raise InvalidTimebaseError() return TimebaseInfo(timebase_id, float(time_interval.value), time_units.value, max_samples.value, None) elif hasattr(self, '_get_timebase2') and ( - len(self._get_timebase2.argtypes) == 7 and self._get_timebase2.argtypes[1] == c_uint32): + len(self._get_timebase2.argtypes) == 7 and self._get_timebase2.argtypes[1] == c_uint32): time_interval = c_float(0.0) max_samples = c_int32(0) - status = self._get_timebase2(c_int16(handle), - c_uint32(timebase_id), - c_int32(no_of_samples), - byref(time_interval), - c_int16(oversample), - byref(max_samples), - c_uint32(segment_index)) + + args = (handle, timebase_id, no_of_samples, time_interval, + oversample, max_samples, segment_index) + converted_args = self._convert_args(self._get_timebase2, args) + status = self._get_timebase2(*converted_args) + if status != self.PICO_STATUS['PICO_OK']: raise InvalidTimebaseError(f"get_timebase2 failed ({constants.pico_tag(status)})") @@ -460,12 +455,10 @@ def set_null_trigger(self, device): auto_trigger_after_millis = 1 if hasattr(self, '_set_trigger') and len(self._set_trigger.argtypes) == 6: PS2000_NONE = 5 - return_code = self._set_trigger(c_int16(device.handle), - c_int16(PS2000_NONE), - c_int16(0), - c_int16(0), - c_int16(0), - c_int16(auto_trigger_after_millis)) + args = (device.handle, PS2000_NONE, 0, 0, 0, auto_trigger_after_millis) + converted_args = self._convert_args(self._set_trigger, args) + return_code = self._set_trigger(*converted_args) + if return_code == 0: raise InvalidTriggerParameters() elif hasattr(self, '_set_simple_trigger') and len(self._set_simple_trigger.argtypes) == 7: @@ -496,23 +489,19 @@ def run_block(self, device, pre_trigger_samples, post_trigger_samples, timebase_ def _python_run_block(self, handle, pre_samples, post_samples, timebase_id, oversample, segment_index): time_indisposed = c_int32(0) if len(self._run_block.argtypes) == 5: - return_code = self._run_block(c_int16(handle), - c_int32(pre_samples + post_samples), - c_int16(timebase_id), - c_int16(oversample), - byref(time_indisposed)) + args = (handle, pre_samples + post_samples, timebase_id, + oversample, time_indisposed) + converted_args = self._convert_args(self._run_block, args) + return_code = self._run_block(*converted_args) + if return_code == 0: raise InvalidCaptureParameters() elif len(self._run_block.argtypes) == 9: - status = self._run_block(c_int16(handle), - c_int32(pre_samples), - c_int32(post_samples), - c_uint32(timebase_id), - c_int16(oversample), - byref(time_indisposed), - c_uint32(segment_index), - None, - None) + args = (handle, pre_samples, post_samples, timebase_id, + oversample, time_indisposed, segment_index, None, None) + converted_args = self._convert_args(self._run_block, args) + status = self._run_block(*converted_args) + if status != self.PICO_STATUS['PICO_OK']: raise InvalidCaptureParameters(f"run_block failed ({constants.pico_tag(status)})") else: @@ -538,10 +527,13 @@ def is_ready(self, device): @requires_device() def maximum_value(self, device): + """Get the maximum ADC value for this device.""" if not hasattr(self, '_maximum_value'): return (2**15)-1 max_adc = c_int16(0) - self._maximum_value(c_int16(device.handle), byref(max_adc)) + args = (device.handle, max_adc) + converted_args = self._convert_args(self._maximum_value, args) + self._maximum_value(*converted_args) return max_adc.value @requires_device() @@ -555,36 +547,32 @@ def get_values(self, device, active_channels, num_samples, segment_index=0): inputs = {k: None for k in 'ABCD'} for k, arr in results.items(): inputs[k] = arr.ctypes.data - return_code = self._get_values(c_int16(device.handle), - inputs['A'], - inputs['B'], - inputs['C'], - inputs['D'], - byref(overflow), - c_int32(num_samples)) + + args = (device.handle, inputs['A'], inputs['B'], inputs['C'], + inputs['D'], overflow, no_of_samples) + converted_args = self._convert_args(self._get_values, args) + return_code = self._get_values(*converted_args) + if return_code == 0: raise InvalidCaptureParameters() elif len(self._get_values.argtypes) == 7 and self._get_timebase.argtypes[1] == c_uint32: # For this function pattern, we first call a function (self._set_data_buffer) to register each buffer. Then, # we can call self._get_values to actually populate them. for channel, array in results.items(): - status = self._set_data_buffer(c_int16(device.handle), - c_int32(self.PICO_CHANNEL[channel]), - array.ctypes.data, - c_int32(num_samples), - c_uint32(segment_index), - c_int32(self.PICO_RATIO_MODE['NONE'])) + args = (device.handle, available_channels[channel], array.ctypes.data, + no_of_samples, segment_index, self.PICO_RATIO_MODE['NONE']) + converted_args = self._convert_args(self._set_data_buffer, args) + status = self._set_data_buffer(*converted_args) + if status != self.PICO_STATUS['PICO_OK']: raise InvalidCaptureParameters(f"set_data_buffer failed ({constants.pico_tag(status)})") - samples_collected = c_uint32(num_samples) - status = self._get_values(c_int16(device.handle), - c_uint32(0), - byref(samples_collected), - c_uint32(1), - c_int32(self.PICO_RATIO_MODE['NONE']), - c_uint32(segment_index), - byref(overflow)) + samples_collected = c_uint32(no_of_samples) + args = (device.handle, 0, samples_collected, 1, + self.PICO_RATIO_MODE['NONE'], segment_index, overflow) + converted_args = self._convert_args(self._get_values, args) + status = self._get_values(*converted_args) + if status != self.PICO_STATUS['PICO_OK']: raise InvalidCaptureParameters(f"get_values failed ({constants.pico_tag(status)})") @@ -598,13 +586,16 @@ def get_values(self, device, active_channels, num_samples, segment_index=0): @requires_device() def stop(self, device): + """Stop data capture.""" + args = (device.handle,) + converted_args = self._convert_args(self._stop, args) + if self._stop.restype == c_int16: - return_code = self._stop(c_int16(device.handle)) - if isinstance(return_code, c_int16): - if return_code == 0: - raise InvalidCaptureParameters() + return_code = self._stop(*converted_args) + if isinstance(return_code, c_int16) and return_code == 0: + raise InvalidCaptureParameters() else: - status = self._stop(c_int16(device.handle)) + status = self._stop(*converted_args) if status != self.PICO_STATUS['PICO_OK']: raise InvalidCaptureParameters(f"stop failed ({constants.pico_tag(status)})") From 3ac3f81e4dccdb3149fc2fbcd81a8893ad9f3917 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 2 Jun 2025 16:05:10 +0200 Subject: [PATCH 011/129] Update get_values to allow digital channels as well --- picosdk/library.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 665f795..c5fe1b1 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -537,10 +537,22 @@ def maximum_value(self, device): return max_adc.value @requires_device() - def get_values(self, device, active_channels, num_samples, segment_index=0): - # Initialise buffers to hold the data: - results = {channel: numpy.empty(num_samples, numpy.dtype('int16')) for channel in active_channels} + def get_values(self, device, active_channels, no_of_samples, segment_index=0): + """Get captured data from the device. + Args: + device: Device instance + active_channels: List of channels to get data from (port numbers included) + no_of_samples: Number of samples to get + segment_index: Memory segment to get data from + + Returns: + Tuple of (results dict, overflow warnings dict) + """ + if isinstance(active_channels, int): + active_channels = [active_channels] + results = {channel: numpy.empty(no_of_samples, numpy.dtype('int16')) + for channel in active_channels} overflow = c_int16(0) if len(self._get_values.argtypes) == 7 and self._get_timebase.argtypes[1] == c_int16: @@ -558,6 +570,13 @@ def get_values(self, device, active_channels, num_samples, segment_index=0): elif len(self._get_values.argtypes) == 7 and self._get_timebase.argtypes[1] == c_uint32: # For this function pattern, we first call a function (self._set_data_buffer) to register each buffer. Then, # we can call self._get_values to actually populate them. + available_channels = self.PICO_CHANNEL + digital_ports = getattr(self, self.name.upper() + '_DIGITAL_PORT', None) + if digital_ports: + for digital_port, value in digital_ports.items(): + if digital_port.startswith(self.name.upper() + '_DIGITAL_PORT'): + port_number = int(digital_port[-1]) + available_channels[port_number] = value for channel, array in results.items(): args = (device.handle, available_channels[channel], array.ctypes.data, no_of_samples, segment_index, self.PICO_RATIO_MODE['NONE']) From 00f0abaecf685118df3278b81ce8d38b7400b86b Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 2 Jun 2025 16:07:06 +0200 Subject: [PATCH 012/129] Fix set_null_trigger function when PICO_THRESHOLD_DIRECTION is not set --- picosdk/library.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index c5fe1b1..6b4bdf9 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -462,14 +462,19 @@ def set_null_trigger(self, device): if return_code == 0: raise InvalidTriggerParameters() elif hasattr(self, '_set_simple_trigger') and len(self._set_simple_trigger.argtypes) == 7: - enabled = False - status = self._set_simple_trigger(c_int16(device.handle), - c_int16(int(enabled)), - c_int32(self.PICO_CHANNEL['A']), - c_int16(0), - c_int32(self.PICO_THRESHOLD_DIRECTION['NONE']), - c_uint32(0), - c_int16(auto_trigger_after_millis)) + threshold_direction_id = None + if not self.PICO_THRESHOLD_DIRECTION: + threshold_directions = getattr(self, self.name.upper() + '_THRESHOLD_DIRECTION', None) + if not threshold_directions: + raise NotImplementedError("This device doesn't support threshold direction") + threshold_direction_id = threshold_directions[self.name.upper() + '_NONE'] + else: + threshold_direction_id = self.PICO_THRESHOLD_DIRECTION['NONE'] + args = (device.handle, False, self.PICO_CHANNEL['A'], 0, + threshold_direction_id, 0, auto_trigger_after_millis) + converted_args = self._convert_args(self._set_simple_trigger, args) + status = self._set_simple_trigger(*converted_args) + if status != self.PICO_STATUS['PICO_OK']: raise InvalidTriggerParameters(f"set_simple_trigger failed ({constants.pico_tag(status)})") else: From 27cab7312854c472f1906f1826a85c5307fdcc32 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 2 Jun 2025 16:07:55 +0200 Subject: [PATCH 013/129] Fix run_block when 8 arguments are passed --- picosdk/library.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/picosdk/library.py b/picosdk/library.py index 6b4bdf9..fada380 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -501,6 +501,14 @@ def _python_run_block(self, handle, pre_samples, post_samples, timebase_id, over if return_code == 0: raise InvalidCaptureParameters() + elif len(self._run_block.argtypes) == 8: + args = (handle, pre_samples, post_samples, timebase_id, + time_indisposed, segment_index, None, None) + converted_args = self._convert_args(self._run_block, args) + status = self._run_block(*converted_args) + + if status != self.PICO_STATUS['PICO_OK']: + raise InvalidCaptureParameters(f"run_block failed ({constants.pico_tag(status)})") elif len(self._run_block.argtypes) == 9: args = (handle, pre_samples, post_samples, timebase_id, oversample, time_indisposed, segment_index, None, None) From 01bf41d3b1afd1b91b8cda27eab3985dfba28ce1 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 2 Jun 2025 16:08:33 +0200 Subject: [PATCH 014/129] Add set_sig_gen_built_in function to use built-in signal generators --- picosdk/library.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/picosdk/library.py b/picosdk/library.py index fada380..b2c95bf 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -631,6 +631,74 @@ def stop(self, device): if status != self.PICO_STATUS['PICO_OK']: raise InvalidCaptureParameters(f"stop failed ({constants.pico_tag(status)})") + @requires_device() + def set_sig_gen_built_in(self, device, offset_voltage=0, pk_to_pk=2000000, wave_type="SINE", + start_frequency=1000.0, stop_frequency=1000.0, increment=0.0, + dwell_time=1.0, sweep_type="UP", operation='ES_OFF', shots=1, sweeps=1, + trigger_type="RISING", trigger_source="NONE", ext_in_threshold=0): + """Set up the signal generator to output a built-in waveform. + + Args: + device: Device instance + offset_voltage: Offset voltage in microvolts (default 0) + pk_to_pk: Peak-to-peak voltage in microvolts (default 2000000) + wave_type: Type of waveform (e.g. "SINE", "SQUARE", "TRIANGLE") + start_frequency: Start frequency in Hz (default 1000.0) + stop_frequency: Stop frequency in Hz (default 1000.0) + increment: Frequency increment in Hz (default 0.0) + dwell_time: Time at each frequency in seconds (default 1.0) + sweep_type: Sweep type (e.g. "UP", "DOWN", "UPDOWN") + operation: Configures the white noise/PRBS (e.g. "ES_OFF", "WHITENOISE", "PRBS") + shots: Number of shots per trigger (default 1) + sweeps: Number of sweeps (default 1) + trigger_type: Type of trigger (e.g. "RISING", "FALLING") + trigger_source: Source of trigger (e.g. "NONE", "SCOPE_TRIG") + ext_in_threshold: External trigger threshold in ADC counts + + Returns: + None + + Raises: + ArgumentOutOfRangeError: If parameters are invalid for device + """ + prefix = self.name.upper() + + # Convert string parameters to enum values + try: + wave_type_val = getattr(self, f"{prefix}_WAVE_TYPE")[f"{prefix}_{wave_type.upper()}"] + sweep_type_val = getattr(self, f"{prefix}_SWEEP_TYPE")[f"{prefix}_{sweep_type.upper()}"] + except (AttributeError, KeyError) as e: + raise ArgumentOutOfRangeError(f"Invalid wave_type or sweep_type for this device: {e}") + + # Check function signature and call appropriate version + if len(self._set_sig_gen_built_in.argtypes) == 10: + args = (device.handle, offset_voltage, pk_to_pk, wave_type_val, + start_frequency, stop_frequency, increment, dwell_time, + sweep_type_val, sweeps) + converted_args = self._convert_args(self._set_sig_gen_built_in, args) + status = self._set_sig_gen_built_in(*converted_args) + + elif len(self._set_sig_gen_built_in.argtypes) == 15: + try: + trigger_type_val = getattr(self, f"{prefix}_SIGGEN_TRIG_TYPE")[f"{prefix}_SIGGEN_{trigger_type.upper()}"] + trigger_source_val = getattr(self, f"{prefix}_SIGGEN_TRIG_SOURCE")[f"{prefix}_SIGGEN_{trigger_source.upper()}"] + extra_ops_val = getattr(self, f"{prefix}_EXTRA_OPERATIONS")[f"{prefix}_{operation.upper()}"] + except (AttributeError, KeyError) as e: + raise ArgumentOutOfRangeError(f"Invalid trigger parameters for this device: {e}") + + args = (device.handle, offset_voltage, pk_to_pk, wave_type_val, + start_frequency, stop_frequency, increment, dwell_time, + sweep_type_val, extra_ops_val, shots, sweeps, + trigger_type_val, trigger_source_val, ext_in_threshold) + converted_args = self._convert_args(self._set_sig_gen_built_in, args) + status = self._set_sig_gen_built_in(*converted_args) + + else: + raise NotImplementedError("Signal generator not supported on this device") + + if status != self.PICO_STATUS["PICO_OK"]: + raise PicoError(f"set_sig_gen_built_in failed: {constants.pico_tag(status)}") + def _convert_args(self, func, args): """Convert arguments to match function argtypes. From da5b35728c8bff83dfff2508759b283fcb76ecbe Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 5 Jun 2025 17:18:48 +0200 Subject: [PATCH 015/129] Add function voltage_to_logic_level to convert voltage to logic level --- picosdk/library.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index b2c95bf..e055c4e 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -43,6 +43,22 @@ def check_device_impl(self, device, *args, **kwargs): return check_device_decorator +def voltage_to_logic_level(voltage): + """Convert a voltage value into logic level for digital channels. + + Range: –32767 (–5 V) to 32767 (5 V). + + Args: + voltage (float): Voltage in volts. + + Returns: + int: The calculated logic level count. + """ + clamped_voltage = min(max(-5, voltage), 5) + logic_level = int((clamped_voltage) * (32767 / 5)) + return logic_level + + class Library(object): def __init__(self, name): self.name = name @@ -310,19 +326,19 @@ def set_channel(self, device, channel_name='A', enabled=True, coupling='DC', ran return max_voltage @requires_device("set_digital_port requires a picosdk.device.Device instance, passed to the correct owning driver.") - def set_digital_port(self, device, port_number=0, enabled=True, logic_level=0): + def set_digital_port(self, device, port_number=0, enabled=True, voltage_level=0): """Set the digital port Args: port_number (int): identifies the port for digital data. (e.g. 0 for digital channels 0-7) enabled (bool): whether or not to enable the channel (boolean) - logic_level (int): the voltage at which the state transitions between 0 and 1. - Range: –32767 (–5 V) to 32767 (+5 V). + voltage_level (float): the voltage at which the state transitions between 0 and 1. Range: –5.0 to 5.0 (V). Raises: NotImplementedError: This device doesn't support digital ports. PicoError: set_digital_port failed """ if hasattr(self, '_set_digital_port') and len(self._set_digital_port.argtypes) == 4: + logic_level = voltage_to_logic_level(voltage_level) digital_ports = getattr(self, self.name.upper() + '_DIGITAL_PORT', None) if not digital_ports: raise NotImplementedError("This device doesn't support digital ports") From 1801737f258983abcd753ef77db6e1b2a4da1d62 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 5 Jun 2025 17:19:57 +0200 Subject: [PATCH 016/129] Set default voltage level to 1.8V --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index e055c4e..852f64f 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -326,7 +326,7 @@ def set_channel(self, device, channel_name='A', enabled=True, coupling='DC', ran return max_voltage @requires_device("set_digital_port requires a picosdk.device.Device instance, passed to the correct owning driver.") - def set_digital_port(self, device, port_number=0, enabled=True, voltage_level=0): + def set_digital_port(self, device, port_number=0, enabled=True, voltage_level=1.8): """Set the digital port Args: From 475eded13c4163faca05533e34a8e5b2028a8ed7 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 12:09:41 +0200 Subject: [PATCH 017/129] Split get_values function into two functions: set_data_buffer and get_values (corresponding to the correct C function names) --- picosdk/library.py | 146 +++++++++++++++++++++++++++++---------------- 1 file changed, 94 insertions(+), 52 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 852f64f..e6a7b00 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -425,9 +425,9 @@ def get_timebase(self, device, timebase_id, no_of_samples, oversample=1, segment no_of_samples, oversample, segment_index) - + self.time_interval_sec = nanoseconds_result.time_interval * 1.e-9 return TimebaseInfo(nanoseconds_result.timebase_id, - nanoseconds_result.time_interval * 1.e-9, + self.time_interval_sec, nanoseconds_result.time_units, nanoseconds_result.max_samples, nanoseconds_result.segment_id) @@ -507,10 +507,12 @@ def run_block(self, device, pre_trigger_samples, post_trigger_samples, timebase_ oversample, segment_index) - def _python_run_block(self, handle, pre_samples, post_samples, timebase_id, oversample, segment_index): + def _python_run_block(self, handle, pre_trigger_samples, post_trigger_samples, timebase_id, oversample, + segment_index): + self.max_samples = pre_trigger_samples + post_trigger_samples time_indisposed = c_int32(0) if len(self._run_block.argtypes) == 5: - args = (handle, pre_samples + post_samples, timebase_id, + args = (handle, pre_trigger_samples + post_trigger_samples, timebase_id, oversample, time_indisposed) converted_args = self._convert_args(self._run_block, args) return_code = self._run_block(*converted_args) @@ -518,7 +520,7 @@ def _python_run_block(self, handle, pre_samples, post_samples, timebase_id, over if return_code == 0: raise InvalidCaptureParameters() elif len(self._run_block.argtypes) == 8: - args = (handle, pre_samples, post_samples, timebase_id, + args = (handle, pre_trigger_samples, post_trigger_samples, timebase_id, time_indisposed, segment_index, None, None) converted_args = self._convert_args(self._run_block, args) status = self._run_block(*converted_args) @@ -526,7 +528,7 @@ def _python_run_block(self, handle, pre_samples, post_samples, timebase_id, over if status != self.PICO_STATUS['PICO_OK']: raise InvalidCaptureParameters(f"run_block failed ({constants.pico_tag(status)})") elif len(self._run_block.argtypes) == 9: - args = (handle, pre_samples, post_samples, timebase_id, + args = (handle, pre_trigger_samples, post_trigger_samples, timebase_id, oversample, time_indisposed, segment_index, None, None) converted_args = self._convert_args(self._run_block, args) status = self._run_block(*converted_args) @@ -566,67 +568,107 @@ def maximum_value(self, device): return max_adc.value @requires_device() - def get_values(self, device, active_channels, no_of_samples, segment_index=0): - """Get captured data from the device. + def set_data_buffer(self, device, channel_or_port, buffer_length, segment_index=0, mode='NONE'): + """Set the data buffer for a specific channel. Args: device: Device instance - active_channels: List of channels to get data from (port numbers included) - no_of_samples: Number of samples to get - segment_index: Memory segment to get data from + channel_or_port: Channel (e.g. 'A', 'B') or digital port (e.g. 0, 1) to set data for + buffer_length: The size of the buffer array (equal to no_of_samples) + segment_index: The number of the memory segment to be used (default is 0) + mode: The ratio mode to be used (default is 'NONE') + + Raises: + ArgumentOutOfRangeError: If parameters are invalid for device + """ + if not hasattr(self, '_set_data_buffer'): + raise NotImplementedError("This device doesn't support setting data buffers") + if len(self._set_data_buffer.argtypes) != 6: + raise NotImplementedError("set_data_buffer is not implemented for this driver") + if isinstance(channel_or_port, str) and channel_or_port.upper() in self.PICO_CHANNEL: + id = self.PICO_CHANNEL[channel_or_port] + else: + try: + port_num = int(channel_or_port) + digital_ports = getattr(self, self.name.upper() + '_DIGITAL_PORT', None) + if digital_ports: + digital_port = self.name.upper() + '_DIGITAL_PORT' + str(port_num) + if digital_port not in digital_ports: + raise ArgumentOutOfRangeError(f"Invalid digital port number {port_num}") + id = digital_ports[digital_port] + except ValueError: + raise ArgumentOutOfRangeError(f"Invalid digital port number {port_num}") + + if mode not in self.PICO_RATIO_MODE: + raise ArgumentOutOfRangeError(f"Invalid ratio mode '{mode}' for {self.name} driver" + "or PICO_RATIO_MODE doesn't exist for this driver") + buffer = (c_int16 * buffer_length)() + args = (device.handle, id, buffer, buffer_length, segment_index, self.PICO_RATIO_MODE[mode]) + converted_args = self._convert_args(self._set_data_buffer, args) + status = self._set_data_buffer(*converted_args) + + if status != self.PICO_STATUS['PICO_OK']: + raise ArgumentOutOfRangeError(f"set_data_buffer failed ({constants.pico_tag(status)})") + + self.buffer[channel_or_port.upper()] = buffer + + @requires_device() + def get_values(self, device, start_index=0, downsample_ratio=0, downsample_ratio_mode="NONE", + segment_index=0, output_dir=".", filename="data", save_to_file=False): + """Get stored data values from the scope and store it in a clean SingletonScopeDataDict object. + + This function is used after data collection has stopped. It gets the stored data from the scope, with or + without downsampling, starting at the specified sample number. + + Args: + device (picosdk.device.Device): Device instance + start_index (int): A zero-based index that indicates the start point for data collection. It is measured in + sample intervals from the start of the buffer. + downsample_ratio (int): The downsampling factor that will be applied to the raw data. + downsample_ratio_mode (str): Which downsampling mode to use. + segment_index (int): Memory segment index + output_dir (str): The output directory where the json file will be saved. + filename (str): The name of the json file where the data will be stored + save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise Returns: Tuple of (results dict, overflow warnings dict) """ - if isinstance(active_channels, int): - active_channels = [active_channels] - results = {channel: numpy.empty(no_of_samples, numpy.dtype('int16')) - for channel in active_channels} - overflow = c_int16(0) - - if len(self._get_values.argtypes) == 7 and self._get_timebase.argtypes[1] == c_int16: - inputs = {k: None for k in 'ABCD'} - for k, arr in results.items(): - inputs[k] = arr.ctypes.data - - args = (device.handle, inputs['A'], inputs['B'], inputs['C'], - inputs['D'], overflow, no_of_samples) - converted_args = self._convert_args(self._get_values, args) - return_code = self._get_values(*converted_args) - - if return_code == 0: - raise InvalidCaptureParameters() - elif len(self._get_values.argtypes) == 7 and self._get_timebase.argtypes[1] == c_uint32: - # For this function pattern, we first call a function (self._set_data_buffer) to register each buffer. Then, - # we can call self._get_values to actually populate them. - available_channels = self.PICO_CHANNEL - digital_ports = getattr(self, self.name.upper() + '_DIGITAL_PORT', None) - if digital_ports: - for digital_port, value in digital_ports.items(): - if digital_port.startswith(self.name.upper() + '_DIGITAL_PORT'): - port_number = int(digital_port[-1]) - available_channels[port_number] = value - for channel, array in results.items(): - args = (device.handle, available_channels[channel], array.ctypes.data, - no_of_samples, segment_index, self.PICO_RATIO_MODE['NONE']) - converted_args = self._convert_args(self._set_data_buffer, args) - status = self._set_data_buffer(*converted_args) - - if status != self.PICO_STATUS['PICO_OK']: - raise InvalidCaptureParameters(f"set_data_buffer failed ({constants.pico_tag(status)})") - - samples_collected = c_uint32(no_of_samples) - args = (device.handle, 0, samples_collected, 1, - self.PICO_RATIO_MODE['NONE'], segment_index, overflow) + scope_data = SingletonScopeDataDict() + scope_data.clean_dict() + overflow = (c_int16 * 10)() + no_of_samples = c_uint32(self.max_samples) + + if len(self._get_values.argtypes) == 7: + args = (device.handle, start_index, no_of_samples, downsample_ratio, + self.PICO_RATIO_MODE[downsample_ratio_mode], segment_index, overflow) converted_args = self._convert_args(self._get_values, args) status = self._get_values(*converted_args) if status != self.PICO_STATUS['PICO_OK']: raise InvalidCaptureParameters(f"get_values failed ({constants.pico_tag(status)})") + else: + raise NotImplementedError("not done other driver types yet") + + for channel, buffer in self.buffer.items(): + if channel.isnumeric(): + scope_data[channel] = numpy.asarray(split_mso_data_fast(no_of_samples, buffer)) + else: + scope_data[channel] = numpy.array( + adc_to_mv(buffer, self.channel_range[channel], self.max_adc)) * self.probe_attenuation[channel] + + time_sec = numpy.linspace(0, + (self.max_samples - 1) * self.time_interval_sec, + self.max_samples) + scope_data["time"] = numpy.array(time_sec) + + if save_to_file: + with open(f"{output_dir}/{filename}.json", "w", encoding="utf-8") as json_file: + json.dump(scope_data, json_file, indent=4, cls=NumpyEncoder) overflow_warning = {} if overflow.value: - for channel in results.keys(): + for channel in self.buffer.keys(): if overflow.value & (1 >> self.PICO_CHANNEL[channel]): overflow_warning[channel] = True From 2a07cad686f55b9972c4136afdff4a28404f2703 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 12:11:12 +0200 Subject: [PATCH 018/129] Add SingletonScopeDataDict class to store the data of the buffers --- picosdk/library.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/picosdk/library.py b/picosdk/library.py index e6a7b00..98f2266 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -9,10 +9,13 @@ from __future__ import print_function +import json import sys from ctypes import c_int16, c_int32, c_uint32, c_float, c_double, c_void_p, create_string_buffer, byref from ctypes.util import find_library import collections +import time +import gc import picosdk.constants as constants import numpy @@ -59,6 +62,76 @@ def voltage_to_logic_level(voltage): return logic_level +class SingletonScopeDataDict(dict): + """SingletonScopeDataDict is a singleton dictionary object for sharing picoscope data between multiple classes. + + It handles both analog and digital data with uniform access patterns: + - Analog channels are accessed by their letter (e.g. 'A', 'B', 'C', 'D') + - Digital channels are accessed as 'D0'-'D15' (MSB channel D15 to LSB channel D0) + - Digital ports are accessed by their number (e.g. 0, 1) + """ + _instance = None + + def __new__(cls, *args, **kwargs): + """Create new singleton dictionary instance or return existing one.""" + if cls._instance is None: + cls._instance = super(SingletonScopeDataDict, cls).__new__(cls, *args, **kwargs) + return cls._instance + + def clean_dict(self): + """Remove all data from the singleton dictionary and run garbage collection.""" + self.clear() + gc.collect() + + def __getitem__(self, key: str): + """Get data with uniform access pattern for analog and digital channels. + + Args: + key: Channel identifier: + - 'A', 'B', 'C', 'D' for analog channels + - 'D0'-'D15' for digital channels + - 0-3 for digital ports + + Returns: + numpy array containing the channel data + + Raises: + KeyError: If channel doesn't exist + ValueError: If digital channel number is invalid + """ + # Handle digital channels (D0-D15) + if isinstance(key, str) and key.upper().startswith('D'): + try: + digital_number = int(key[1:]) + if not 0 <= digital_number <= 15: + raise ValueError(f"Digital channel number must be 0-15, got {digital_number}") + + # Calculate which port and bit + port_number = digital_number // 8 # Port 0 = D0-D7, Port 1 = D8-D15 + bit_number = 7 - (digital_number % 8) # Reverse bit order within port + + # Get port data + port_data = super().__getitem__(port_number) + + # Extract individual channel data + return (port_data >> bit_number) & 0x1 + + except (IndexError, ValueError) as e: + raise ValueError(f"Invalid digital channel {key}: {str(e)}") + + # Handle direct port access (0-3) or analog channels + return super().__getitem__(key) + + def set_port_data(self, port_number: int, data: np.ndarray): + """Set digital port data. + + Args: + port_number: Digital port number (0-3) + data: Numpy array containing port data + """ + self[port_number] = data + + class Library(object): def __init__(self, name): self.name = name From 43772579a28613f31e38adb5b7e1871c8f394141 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 12:12:03 +0200 Subject: [PATCH 019/129] Add NumpyEncoder class to use when storing the data in a file --- picosdk/library.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/picosdk/library.py b/picosdk/library.py index 98f2266..c8a018c 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -61,6 +61,21 @@ def voltage_to_logic_level(voltage): logic_level = int((clamped_voltage) * (32767 / 5)) return logic_level +class NumpyEncoder(json.JSONEncoder): + """Module specific json encoder class.""" + def default(self, o): + """Default json encoder override. + + Code inspired from https://github.com/Crimson-Crow/json-numpy/blob/main/json_numpy.py + """ + if isinstance(o, (numpy.ndarray, numpy.generic)): + return { + "__numpy__": b64encode(o.data if o.flags.c_contiguous else o.tobytes()).decode(), + "dtype": dtype_to_descr(o.dtype), + "shape": o.shape, + } + return super().default(o) + class SingletonScopeDataDict(dict): """SingletonScopeDataDict is a singleton dictionary object for sharing picoscope data between multiple classes. From 4a2294175532d88c774caaafc649f460dfd89033 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 12:15:16 +0200 Subject: [PATCH 020/129] Add function to handle data from digital ports --- picosdk/library.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/picosdk/library.py b/picosdk/library.py index c8a018c..a2ec522 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -61,6 +61,27 @@ def voltage_to_logic_level(voltage): logic_level = int((clamped_voltage) * (32767 / 5)) return logic_level + +def split_mso_data_fast(data_length, data): + """Return a tuple of 8 arrays, each of which is the values over time of a different digital channel. + + The tuple contains the channels in order (D7, D6, D5, ... D0) or equivalently (D15, D14, D13, ... D8). + + Args: + data_length (c_int32): The length of the data array. + data (c_int16 array): The data array containing the digital port values. + """ + num_samples = data_length.value + # Makes an array for each digital channel + buffer_binary_dj = tuple(numpy.empty(num_samples, dtype=numpy.uint8) for _ in range(8)) + # Splits out the individual bits from the port into the binary values for each digital channel/pin. + for i in range(num_samples): + for j in range(8): + buffer_binary_dj[j][i] = 1 if (data[i] & (1 << (7-j))) else 0 + + return buffer_binary_dj + + class NumpyEncoder(json.JSONEncoder): """Module specific json encoder class.""" def default(self, o): From bf532d582f1fded736d6368ba50b24421402cd0f Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 12:16:28 +0200 Subject: [PATCH 021/129] Add function to stop block capture allowing a timeout --- picosdk/library.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/picosdk/library.py b/picosdk/library.py index a2ec522..c65d078 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -665,6 +665,20 @@ def is_ready(self, device): else: raise NotImplementedError("not done other driver types yet") + @requires_device() + def stop_block_capture(self, device, timeout_minutes=0): + """Poll the driver to see if it has finished collecting the requested samples. + + Args: + timeout_minutes (int/float): The timeout in minutes. If the time exceeds the timeout, the poll stops. + """ + if timeout_minutes < 0: + raise ArgumentOutOfRangeError("timeout_minutes must be non-negative.") + timeout = time.time() + timeout_minutes*60 + while not self.is_ready(device): + if time.time() > timeout: + break + @requires_device() def maximum_value(self, device): """Get the maximum ADC value for this device.""" From 712d99cd9ff231d26a9e8404323888a2806c95e9 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 12:17:00 +0200 Subject: [PATCH 022/129] Add set_trigger_channel_properties function --- picosdk/library.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/picosdk/library.py b/picosdk/library.py index c65d078..2c40039 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -797,6 +797,41 @@ def get_values(self, device, start_index=0, downsample_ratio=0, downsample_ratio return results, overflow_warning + @requires_device("set_trigger_channel_properties requires a picosdk.device.Device instance, passed to the correct owning driver.") + def set_trigger_channel_properties(self, device, threshold_upper, threshold_upper_hysteresis, threshold_lower, threshold_lower_hysteresis, channel, threshold_mode, aux_output_enable, auto_trigger_milliseconds): + """Set the trigger channel properties for the device. + + Args: + device: Device instance + threshold_upper: Upper threshold in ADC counts + threshold_upper_hysteresis: Hysteresis for upper threshold in ADC counts + threshold_lower: Lower threshold in ADC counts + threshold_lower_hysteresis: Hysteresis for lower threshold in ADC counts + channel: Channel to set properties for (e.g. 'A', 'B', 'C', 'D') + threshold_mode: Threshold mode (e.g. "LEVEL", "WINDOW") + aux_output_enable: Enable auxiliary output (boolean) (Not used in eg. ps2000a, ps3000a, ps4000a) + auto_trigger_milliseconds: The number of milliseconds for which the scope device will wait for a trigger + before timing out. If set to zero, the scope device will wait indefinitely for a trigger + + Returns: + None + + Raises: + ArgumentOutOfRangeError: If parameters are invalid for device + """ + if hasattr(self, '_set_trigger_channel_properties'): + args = (device.handle, threshold_upper, threshold_upper_hysteresis, + threshold_lower, threshold_lower_hysteresis, + self.PICO_CHANNEL[channel], self.PICO_THRESHOLD_DIRECTION[threshold_mode], + aux_output_enable, auto_trigger_milliseconds) + converted_args = self._convert_args(self._set_trigger_channel_properties, args) + status = self._set_trigger_channel_properties(*converted_args) + + if status != self.PICO_STATUS['PICO_OK']: + raise PicoError(f"set_trigger_channel_properties failed ({constants.pico_tag(status)})") + else: + raise NotImplementedError("This device does not support setting trigger channel properties.") + @requires_device() def stop(self, device): """Stop data capture.""" From 62115de8930fdb724a13904bd9279bbb21e64afd Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 12:17:27 +0200 Subject: [PATCH 023/129] Modify the defaults of set_sig_gen_built_in --- picosdk/library.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 2c40039..a6a3547 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -849,9 +849,9 @@ def stop(self, device): @requires_device() def set_sig_gen_built_in(self, device, offset_voltage=0, pk_to_pk=2000000, wave_type="SINE", - start_frequency=1000.0, stop_frequency=1000.0, increment=0.0, - dwell_time=1.0, sweep_type="UP", operation='ES_OFF', shots=1, sweeps=1, - trigger_type="RISING", trigger_source="NONE", ext_in_threshold=0): + start_frequency=10000, stop_frequency=10000, increment=0, + dwell_time=1, sweep_type="UP", operation='ES_OFF', shots=0, sweeps=0, + trigger_type="RISING", trigger_source="NONE", ext_in_threshold=1): """Set up the signal generator to output a built-in waveform. Args: From b986ebd985301eebe50019d3fa920708d0ae2db9 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 14:28:08 +0200 Subject: [PATCH 024/129] Fix typo --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index a6a3547..5b65e3c 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -158,7 +158,7 @@ def __getitem__(self, key: str): # Handle direct port access (0-3) or analog channels return super().__getitem__(key) - def set_port_data(self, port_number: int, data: np.ndarray): + def set_port_data(self, port_number: int, data: numpy.ndarray): """Set digital port data. Args: From b95249ca202bb7325261f91eda1c758e2528d686 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 17:14:30 +0200 Subject: [PATCH 025/129] Set timeout to 5 minutes --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index 5b65e3c..df30bef 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -666,7 +666,7 @@ def is_ready(self, device): raise NotImplementedError("not done other driver types yet") @requires_device() - def stop_block_capture(self, device, timeout_minutes=0): + def stop_block_capture(self, device, timeout_minutes=5): """Poll the driver to see if it has finished collecting the requested samples. Args: From bc6dac21610d1c4e946fefba8a9212ec646676da Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 18:20:15 +0200 Subject: [PATCH 026/129] Update get timebase to allow ps4000a --- picosdk/library.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index df30bef..93fbd39 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -558,14 +558,18 @@ def _python_get_timebase(self, handle, timebase_id, no_of_samples, oversample, s raise InvalidTimebaseError() return TimebaseInfo(timebase_id, float(time_interval.value), time_units.value, max_samples.value, None) - elif hasattr(self, '_get_timebase2') and ( - len(self._get_timebase2.argtypes) == 7 and self._get_timebase2.argtypes[1] == c_uint32): + elif hasattr(self, '_get_timebase2') and self._get_timebase2.argtypes[1] == c_uint32: time_interval = c_float(0.0) max_samples = c_int32(0) - - args = (handle, timebase_id, no_of_samples, time_interval, - oversample, max_samples, segment_index) - converted_args = self._convert_args(self._get_timebase2, args) + if len(self._get_timebase2.argtypes) == 7: + args = (handle, timebase_id, no_of_samples, time_interval, + oversample, max_samples, segment_index) + converted_args = self._convert_args(self._get_timebase2, args) + elif len(self._get_timebase2.argtypes) == 6: + args = (handle, timebase_id, no_of_samples, time_interval, max_samples, segment_index) + converted_args = self._convert_args(self._get_timebase2, args) + else: + raise NotImplementedError("_get_timebase2 is not implemented for this driver yet") status = self._get_timebase2(*converted_args) if status != self.PICO_STATUS['PICO_OK']: @@ -573,7 +577,7 @@ def _python_get_timebase(self, handle, timebase_id, no_of_samples, oversample, s return TimebaseInfo(timebase_id, time_interval.value, None, max_samples.value, segment_index) else: - raise NotImplementedError("not done other driver types yet") + raise NotImplementedError("_get_timebase2 or _get_timebase is not implemented for this driver yet") @requires_device() def set_null_trigger(self, device): From fd958491d7640a68cc35b3bafe03fbbaef409f81 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 18:27:01 +0200 Subject: [PATCH 027/129] Use dictionaries to store channel data; update get_values with new function adc_to_mv --- picosdk/library.py | 63 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 93fbd39..e43e38d 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -27,6 +27,17 @@ from picosdk.device import Device +DEFAULT_PROBE_ATTENUATION = { + 'A': 10, + 'B': 10, + 'C': 10, + 'D': 10, + 'E': 10, + 'F': 10, + 'G': 10, + 'H': 10, +} + """TimebaseInfo: A type for holding the particulars of a timebase configuration. """ TimebaseInfo = collections.namedtuple('TimebaseInfo', ['timebase_id', @@ -82,6 +93,21 @@ def split_mso_data_fast(data_length, data): return buffer_binary_dj +def adc_to_mv(buffer_adc, channel_range, max_adc): + """Convert a buffer of raw adc count values into millivolts. + + Args: + buffer_adc (c_short_Array): The buffer of ADC count values. + channel_range (int): The channel range in V. + max_adc (int): The maximum ADC count. + + Returns: + list: The buffer in millivolts. + """ + buffer_mv = [(x * channel_range * 1000) / max_adc for x in buffer_adc] + return buffer_mv + + class NumpyEncoder(json.JSONEncoder): """Module specific json encoder class.""" def default(self, o): @@ -737,18 +763,25 @@ def set_data_buffer(self, device, channel_or_port, buffer_length, segment_index= if status != self.PICO_STATUS['PICO_OK']: raise ArgumentOutOfRangeError(f"set_data_buffer failed ({constants.pico_tag(status)})") - self.buffer[channel_or_port.upper()] = buffer + return {channel_or_port: buffer} @requires_device() - def get_values(self, device, start_index=0, downsample_ratio=0, downsample_ratio_mode="NONE", - segment_index=0, output_dir=".", filename="data", save_to_file=False): + def get_values(self, device, buffers, samples, max_voltage={}, start_index=0, downsample_ratio=0, + downsample_ratio_mode="NONE", segment_index=0, output_dir=".", filename="data", save_to_file=False, + probe_attenuation=DEFAULT_PROBE_ATTENUATION): """Get stored data values from the scope and store it in a clean SingletonScopeDataDict object. This function is used after data collection has stopped. It gets the stored data from the scope, with or without downsampling, starting at the specified sample number. + The returned captured data is converted to mV. + Args: device (picosdk.device.Device): Device instance + buffers (dict): Dictionary of buffers where the data will be stored. The keys are channel names or port numbers, + and the values are numpy arrays. + samples (int): The number of samples to retrieve from the scope. + max_voltage (dict): The maximum voltage of the range used per channel. start_index (int): A zero-based index that indicates the start point for data collection. It is measured in sample intervals from the start of the buffer. downsample_ratio (int): The downsampling factor that will be applied to the raw data. @@ -757,14 +790,15 @@ def get_values(self, device, start_index=0, downsample_ratio=0, downsample_ratio output_dir (str): The output directory where the json file will be saved. filename (str): The name of the json file where the data will be stored save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise + probe_attenuation (dict): The attenuation factor of the probe used per the channel (1 or 10). Returns: Tuple of (results dict, overflow warnings dict) """ scope_data = SingletonScopeDataDict() scope_data.clean_dict() - overflow = (c_int16 * 10)() - no_of_samples = c_uint32(self.max_samples) + overflow = c_int16(0) + no_of_samples = c_uint32(samples) if len(self._get_values.argtypes) == 7: args = (device.handle, start_index, no_of_samples, downsample_ratio, @@ -777,16 +811,18 @@ def get_values(self, device, start_index=0, downsample_ratio=0, downsample_ratio else: raise NotImplementedError("not done other driver types yet") - for channel, buffer in self.buffer.items(): + num_samples_retrieved = no_of_samples.value + for channel, arr in buffers.items(): + data = arr[:num_samples_retrieved] if channel.isnumeric(): - scope_data[channel] = numpy.asarray(split_mso_data_fast(no_of_samples, buffer)) + scope_data[channel] = numpy.asarray(split_mso_data_fast(no_of_samples, data)) else: - scope_data[channel] = numpy.array( - adc_to_mv(buffer, self.channel_range[channel], self.max_adc)) * self.probe_attenuation[channel] + scope_data[channel] = numpy.array(adc_to_mv(data, max_voltage[channel], + self.maximum_value(device))) * probe_attenuation[channel] time_sec = numpy.linspace(0, - (self.max_samples - 1) * self.time_interval_sec, - self.max_samples) + (samples - 1) * self.time_interval_sec, + samples) scope_data["time"] = numpy.array(time_sec) if save_to_file: @@ -795,11 +831,12 @@ def get_values(self, device, start_index=0, downsample_ratio=0, downsample_ratio overflow_warning = {} if overflow.value: - for channel in self.buffer.keys(): + for channel in buffers.keys(): if overflow.value & (1 >> self.PICO_CHANNEL[channel]): overflow_warning[channel] = True - return results, overflow_warning + return scope_data, overflow_warning + @requires_device("set_trigger_channel_properties requires a picosdk.device.Device instance, passed to the correct owning driver.") def set_trigger_channel_properties(self, device, threshold_upper, threshold_upper_hysteresis, threshold_lower, threshold_lower_hysteresis, channel, threshold_mode, aux_output_enable, auto_trigger_milliseconds): From 6dfbb671d4fcab8a5fd3422d8b2639f132f2f242 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 18:27:53 +0200 Subject: [PATCH 028/129] Check only if the arg is not None to fix issues with buffer data in _convert_args --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index e43e38d..e02b03f 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -972,7 +972,7 @@ def _convert_args(self, func, args): converted = [] for arg, argtype in zip(args, func.argtypes): # Handle byref parameters - if argtype == c_void_p and isinstance(arg, (c_int16, c_int32, c_uint32, c_float, c_double)): + if argtype == c_void_p and arg is not None: converted.append(byref(arg)) # Handle normal parameters elif arg is not None: From 889eea4751dcec91bceabc5dd8dcfd084f5be19d Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 18:28:30 +0200 Subject: [PATCH 029/129] Don't save max_samples in the library --- picosdk/library.py | 1 - 1 file changed, 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index e02b03f..73513bd 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -648,7 +648,6 @@ def run_block(self, device, pre_trigger_samples, post_trigger_samples, timebase_ def _python_run_block(self, handle, pre_trigger_samples, post_trigger_samples, timebase_id, oversample, segment_index): - self.max_samples = pre_trigger_samples + post_trigger_samples time_indisposed = c_int32(0) if len(self._run_block.argtypes) == 5: args = (handle, pre_trigger_samples + post_trigger_samples, timebase_id, From df3f26ce3d004b918b430a0222da0c3ee4d3f134 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 12 Jun 2025 18:30:22 +0200 Subject: [PATCH 030/129] Don't save time_interval_sec in the library but pass it as an argument --- picosdk/library.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 73513bd..0bbd5e0 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -560,9 +560,8 @@ def get_timebase(self, device, timebase_id, no_of_samples, oversample=1, segment no_of_samples, oversample, segment_index) - self.time_interval_sec = nanoseconds_result.time_interval * 1.e-9 return TimebaseInfo(nanoseconds_result.timebase_id, - self.time_interval_sec, + nanoseconds_result.time_interval * 1.e-9, nanoseconds_result.time_units, nanoseconds_result.max_samples, nanoseconds_result.segment_id) @@ -765,7 +764,7 @@ def set_data_buffer(self, device, channel_or_port, buffer_length, segment_index= return {channel_or_port: buffer} @requires_device() - def get_values(self, device, buffers, samples, max_voltage={}, start_index=0, downsample_ratio=0, + def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={}, start_index=0, downsample_ratio=0, downsample_ratio_mode="NONE", segment_index=0, output_dir=".", filename="data", save_to_file=False, probe_attenuation=DEFAULT_PROBE_ATTENUATION): """Get stored data values from the scope and store it in a clean SingletonScopeDataDict object. @@ -780,6 +779,7 @@ def get_values(self, device, buffers, samples, max_voltage={}, start_index=0, do buffers (dict): Dictionary of buffers where the data will be stored. The keys are channel names or port numbers, and the values are numpy arrays. samples (int): The number of samples to retrieve from the scope. + time_interval_sec (float): The time interval between samples in seconds. (obtained from get_timebase) max_voltage (dict): The maximum voltage of the range used per channel. start_index (int): A zero-based index that indicates the start point for data collection. It is measured in sample intervals from the start of the buffer. @@ -820,7 +820,7 @@ def get_values(self, device, buffers, samples, max_voltage={}, start_index=0, do self.maximum_value(device))) * probe_attenuation[channel] time_sec = numpy.linspace(0, - (samples - 1) * self.time_interval_sec, + (samples - 1) * time_interval_sec, samples) scope_data["time"] = numpy.array(time_sec) From 189ed6c3f376c491f01d2353a8cd00d23361fc46 Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 13 Jun 2025 13:42:59 +0200 Subject: [PATCH 031/129] Add mv_to_adc function --- picosdk/library.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/picosdk/library.py b/picosdk/library.py index 0bbd5e0..6c53208 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -108,6 +108,21 @@ def adc_to_mv(buffer_adc, channel_range, max_adc): return buffer_mv +def mv_to_adc(millivolts, channel_range, max_adc): + """Convert a voltage value into an ADC count. + + Args: + millivolts (float): Voltage in millivolts. + channel_range (int): The channel range in V. + max_adc (c_int16): The maximum ADC count. + + Returns: + int: The ADC count. + """ + adc_value = round((millivolts * max_adc) / (channel_range * 1000)) + return adc_value + + class NumpyEncoder(json.JSONEncoder): """Module specific json encoder class.""" def default(self, o): From a940dba4c061cd5e4aa95d7d346adb67c84db978 Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 13 Jun 2025 13:44:05 +0200 Subject: [PATCH 032/129] Add channel argument to set_null_trigger instead of using A --- picosdk/library.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 6c53208..5361c10 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -620,7 +620,7 @@ def _python_get_timebase(self, handle, timebase_id, no_of_samples, oversample, s raise NotImplementedError("_get_timebase2 or _get_timebase is not implemented for this driver yet") @requires_device() - def set_null_trigger(self, device): + def set_null_trigger(self, device, channel="A"): auto_trigger_after_millis = 1 if hasattr(self, '_set_trigger') and len(self._set_trigger.argtypes) == 6: PS2000_NONE = 5 @@ -632,14 +632,15 @@ def set_null_trigger(self, device): raise InvalidTriggerParameters() elif hasattr(self, '_set_simple_trigger') and len(self._set_simple_trigger.argtypes) == 7: threshold_direction_id = None - if not self.PICO_THRESHOLD_DIRECTION: + if self.PICO_THRESHOLD_DIRECTION: + threshold_direction_id = self.PICO_THRESHOLD_DIRECTION['NONE'] + else: threshold_directions = getattr(self, self.name.upper() + '_THRESHOLD_DIRECTION', None) - if not threshold_directions: + if threshold_directions: + threshold_direction_id = threshold_directions[self.name.upper() + '_NONE'] + else: raise NotImplementedError("This device doesn't support threshold direction") - threshold_direction_id = threshold_directions[self.name.upper() + '_NONE'] - else: - threshold_direction_id = self.PICO_THRESHOLD_DIRECTION['NONE'] - args = (device.handle, False, self.PICO_CHANNEL['A'], 0, + args = (device.handle, False, self.PICO_CHANNEL[channel], 0, threshold_direction_id, 0, auto_trigger_after_millis) converted_args = self._convert_args(self._set_simple_trigger, args) status = self._set_simple_trigger(*converted_args) From 0793c127e41ad6abe9daf8f148ce0fefa6fc7683 Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 13 Jun 2025 13:44:32 +0200 Subject: [PATCH 033/129] Add set_simple_trigger function --- picosdk/library.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index 5361c10..5ff17a3 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -648,7 +648,47 @@ def set_null_trigger(self, device, channel="A"): if status != self.PICO_STATUS['PICO_OK']: raise InvalidTriggerParameters(f"set_simple_trigger failed ({constants.pico_tag(status)})") else: - raise NotImplementedError("not done other driver types yet") + raise NotImplementedError("This device doesn't support set_null_trigger (yet)") + + @requires_device() + def set_simple_trigger(self, device, max_voltage, max_adc, enable=1, channel="A", threshold_mv=500, + direction="FALLING", delay=0, auto_trigger_ms=1000): + """Set a simple trigger for a channel + + Args: + device (picosdk.device.Device): The device instance + max_voltage (int/float): The maximum voltage of the range used by the channel. (obtained from `set_channel`) + max_adc (int): The maximum ADC count. (obtained from `maximum_value`) + enable (bool): False to disable the trigger, True to enable it + channel (str): The channel on which to trigger + threshold_mv (int/float): The threshold in millivolts at which the trigger will fire. + direction (str): The direction in which the signal must move to cause a trigger. + delay (int): The time (sample periods) between the trigger occurring and the first sample. + auto_trigger_ms (int): The number of milliseconds the device will wait if no trigger occurs. + If this is set to zero, the scope device will wait indefinitely for a trigger. + """ + if hasattr(self, '_set_simple_trigger') and len(self._set_simple_trigger.argtypes) == 7: + adc_threshold = mv_to_adc(threshold_mv, max_voltage, max_adc) + threshold_direction_id = None + if self.PICO_THRESHOLD_DIRECTION: + threshold_direction_id = self.PICO_THRESHOLD_DIRECTION[direction] + else: + threshold_directions = getattr(self, self.name.upper() + '_THRESHOLD_DIRECTION', None) + if threshold_directions: + threshold_direction_id = threshold_directions[self.name.upper() + f'_{direction.upper()}'] + else: + raise NotImplementedError("This device doesn't support threshold direction") + + args = (device.handle, enable, self.PICO_CHANNEL[channel], adc_threshold, + threshold_direction_id, delay, auto_trigger_ms) + converted_args = self._convert_args(self._set_simple_trigger, args) + status = self._set_simple_trigger(*converted_args) + + if status != self.PICO_STATUS['PICO_OK']: + raise InvalidTriggerParameters(f"set_simple_trigger failed ({constants.pico_tag(status)})") + else: + raise NotImplementedError("This device doesn't support set_simple_trigger (yet)") + @requires_device() def run_block(self, device, pre_trigger_samples, post_trigger_samples, timebase_id, oversample=1, segment_index=0): From d5d50cf76ae0a5a7145a0c45ad2ec35c96d4b684 Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 13 Jun 2025 13:45:01 +0200 Subject: [PATCH 034/129] Fix if statement when port is integer --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index 5ff17a3..c8b517b 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -869,7 +869,7 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} num_samples_retrieved = no_of_samples.value for channel, arr in buffers.items(): data = arr[:num_samples_retrieved] - if channel.isnumeric(): + if isinstance(channel, int) or channel.isnumeric(): scope_data[channel] = numpy.asarray(split_mso_data_fast(no_of_samples, data)) else: scope_data[channel] = numpy.array(adc_to_mv(data, max_voltage[channel], From e131611a5fdccf9767fc5282d8be21198cf6291f Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 13 Jun 2025 13:45:36 +0200 Subject: [PATCH 035/129] Add clarification to the documentation where the argument can be obtained --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index c8b517b..bc1d67b 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -836,7 +836,7 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} and the values are numpy arrays. samples (int): The number of samples to retrieve from the scope. time_interval_sec (float): The time interval between samples in seconds. (obtained from get_timebase) - max_voltage (dict): The maximum voltage of the range used per channel. + max_voltage (dict): The maximum voltage of the range used per channel. (obtained from set_channel) start_index (int): A zero-based index that indicates the start point for data collection. It is measured in sample intervals from the start of the buffer. downsample_ratio (int): The downsampling factor that will be applied to the raw data. From 4522e7d85d87dd725668ebb811ef15d11415fe61 Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 13 Jun 2025 13:46:00 +0200 Subject: [PATCH 036/129] Align arguments --- picosdk/library.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index bc1d67b..0d8fb3a 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -894,7 +894,9 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} @requires_device("set_trigger_channel_properties requires a picosdk.device.Device instance, passed to the correct owning driver.") - def set_trigger_channel_properties(self, device, threshold_upper, threshold_upper_hysteresis, threshold_lower, threshold_lower_hysteresis, channel, threshold_mode, aux_output_enable, auto_trigger_milliseconds): + def set_trigger_channel_properties(self, device, threshold_upper, threshold_upper_hysteresis, threshold_lower, + threshold_lower_hysteresis, channel, threshold_mode, aux_output_enable, + auto_trigger_milliseconds): """Set the trigger channel properties for the device. Args: From 25732720d51d76e2721d225dadc4d68368ddf41e Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 13 Jun 2025 14:21:49 +0200 Subject: [PATCH 037/129] Add missing imports --- picosdk/library.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/picosdk/library.py b/picosdk/library.py index 0d8fb3a..f4162aa 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -17,7 +17,9 @@ import time import gc import picosdk.constants as constants +from base64 import b64encode import numpy +from numpy.lib.format import dtype_to_descr from picosdk.errors import PicoError, CannotFindPicoSDKError, CannotOpenPicoSDKError, DeviceNotFoundError, \ ArgumentOutOfRangeError, ValidRangeEnumValueNotValidForThisDevice, DeviceCannotSegmentMemoryError, \ From 7d28cb8608e408ecaa80d9a55a54d1502b10e278 Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 13 Jun 2025 14:22:43 +0200 Subject: [PATCH 038/129] Add set_and_load_data function to combine set_data_buffer and get_values --- picosdk/library.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/picosdk/library.py b/picosdk/library.py index f4162aa..587b9c9 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -894,6 +894,43 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} return scope_data, overflow_warning + @requires_device() + def set_and_load_data(self, device, active_sources, buffer_length, time_interval_sec, max_voltage={}, + segment_index=0, ratio_mode='NONE', start_index=0, + downsample_ratio=0, downsample_ratio_mode="NONE", probe_attenuation=DEFAULT_PROBE_ATTENUATION, + output_dir=".", filename="data", save_to_file=False): + """Load values from the device. + + Combines set_data_buffer and get_values to load values from the device. + + Args: + device (picosdk.device.Device): Device instance + active_sources (lsit[str/int]): List of active channels and/or ports + buffer_length: The size of the buffer array (equal to the number of samples) + time_interval_sec (float): The time interval between samples in seconds. (obtained from get_timebase) + max_voltage (dict): The maximum voltage of the range used per channel. (obtained from set_channel) + segment_index (int): Memory segment index + ratio_mode: The ratio mode to be used (default is 'NONE') + start_index (int): A zero-based index that indicates the start point for data collection. It is measured in + sample intervals from the start of the buffer. + downsample_ratio (int): The downsampling factor that will be applied to the raw data. + downsample_ratio_mode (str): Which downsampling mode to use. + probe_attenuation (dict): The attenuation factor of the probe used per the channel (1 or 10). + output_dir (str): The output directory where the json file will be saved. + filename (str): The name of the json file where the data will be stored + save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise + + Returns: + Tuple of (captured data including time, overflow warnings) + """ + buffers = {} + for source in active_sources: + buffer = self.set_data_buffer(device, source, buffer_length, segment_index, ratio_mode) + buffers = buffers | buffer + + return self.get_values(device, buffers, buffer_length, time_interval_sec, max_voltage, start_index, + downsample_ratio, downsample_ratio_mode, segment_index, output_dir, filename, + save_to_file, probe_attenuation) @requires_device("set_trigger_channel_properties requires a picosdk.device.Device instance, passed to the correct owning driver.") def set_trigger_channel_properties(self, device, threshold_upper, threshold_upper_hysteresis, threshold_lower, From 272d06815143d09ca1b9044e66f1e9907467a1ab Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 13 Jun 2025 14:22:58 +0200 Subject: [PATCH 039/129] Update docstring --- picosdk/library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 587b9c9..0c1bf89 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -447,7 +447,7 @@ def set_channel(self, device, channel_name='A', enabled=True, coupling='DC', ran enabled: whether to enable the channel (boolean) coupling: string of the relevant enum member for your driver less the driver name prefix. e.g. 'DC' or 'AC'. range_peak: float which is the largest value you expect in the input signal. We will throw an exception if no - range on the device is large enough for that value. + range on the device is large enough for that value. (in Voltage) analog_offset: the meaning of 0 for this channel. return value: Max voltage of new range. Raises an exception in error cases.""" @@ -850,7 +850,7 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} probe_attenuation (dict): The attenuation factor of the probe used per the channel (1 or 10). Returns: - Tuple of (results dict, overflow warnings dict) + Tuple of (captured data including time, overflow warnings) """ scope_data = SingletonScopeDataDict() scope_data.clean_dict() From c5326552575db543186ecfb2fca86c2ea9ba81f6 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 12:10:26 +0200 Subject: [PATCH 040/129] Make set_channel of Device same as the one of Library class; Update namedtuple with default values --- picosdk/device.py | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index 21c35b1..fc6820e 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -31,8 +31,8 @@ def check_open_impl(self, *args, **kwargs): coupling (optional) = 'AC' or 'DC', default is 'DC'. range_peak (optional) = +/- max volts, the highest precision range which includes your value will be selected. analog_offset (optional) = the meaning of 0 for this channel.""" -ChannelConfig = collections.namedtuple('ChannelConfig', ['name', 'enabled', 'coupling', 'range_peak', 'analog_offset']) -ChannelConfig.__new__.__defaults__ = (None, None, None) +ChannelConfig = collections.namedtuple('ChannelConfig', 'name enabled coupling range_peak analog_offset', + defaults=['DC', float('inf'), None]) """TimebaseOptions: A type for specifying timebase constraints (pass to Device.find_timebase or Device.capture_*) @@ -84,27 +84,35 @@ def __exit__(self, *args): return False @requires_open() - def set_channel(self, channel_config): - name = channel_config.name - if not channel_config.enabled: + def set_channel(self, channel_name, enabled, coupling='DC', range_peak=float('inf'), analog_offset=None): + """Configures a single analog channel. + + channel_name: The channel name as a string (e.g., 'A'). + enabled: bool, True to enable the channel, False to disable. + coupling (optional): 'AC' or 'DC'. Defaults to 'DC'. + range_peak (optional): Desired +/- peak voltage. The driver selects the best range. + Required if enabling the channel. + analog_offset (optional): The analog offset for the channel in Volts. + """ + if not enabled: self.driver.set_channel(self, - channel_name=name, - enabled=channel_config.enabled) + channel_name=channel_name, + enabled=enabled) try: - del self._channel_ranges[name] - del self._channel_offsets[name] + del self._channel_ranges[channel_name] + del self._channel_offsets[channel_name] except KeyError: pass return - # if enabled, we pass through the values from the channel config: - self._channel_ranges[name] = self.driver.set_channel(self, - channel_name=name, - enabled=channel_config.enabled, - coupling=channel_config.coupling, - range_peak=channel_config.range_peak, - analog_offset=channel_config.analog_offset) - self._channel_offsets[name] = channel_config.analog_offset - return self._channel_ranges[name] + + self._channel_ranges[channel_name] = self.driver.set_channel(device=self, + channel_name=channel_name, + enabled=enabled, + coupling=coupling, + range_peak=range_peak, + analog_offset=analog_offset) + self._channel_offsets[channel_name] = analog_offset + return self._channel_ranges[channel_name] @requires_open() def set_channels(self, *channel_configs): @@ -121,7 +129,7 @@ def set_channels(self, *channel_configs): channel_configs.append(ChannelConfig(channel_name, False)) for channel_config in channel_configs: - self.set_channel(channel_config) + self.set_channel(*channel_config) def _timebase_options_are_impossible(self, options): device_max_samples_possible = self.driver.MAX_MEMORY From 4b751b8b8c95b9451607fb9e24c0f011bc588162 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 12:53:31 +0200 Subject: [PATCH 041/129] Add properties to Device to save various data --- picosdk/device.py | 75 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index fc6820e..bc48e28 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -15,6 +15,18 @@ NoChannelsEnabledError, NoValidTimebaseForOptionsError +DEFAULT_PROBE_ATTENUATION = { + 'A': 10, + 'B': 10, + 'C': 10, + 'D': 10, + 'E': 10, + 'F': 10, + 'G': 10, + 'H': 10, +} + + def requires_open(error_message="This operation requires a device to be connected."): def check_open_decorator(method): def check_open_impl(self, *args, **kwargs): @@ -55,19 +67,66 @@ class Device(object): unwanted behaviour (e.g. throwing an exception because no channels are enabled, when you enabled them yourself on the driver object.)""" def __init__(self, driver, handle): - self.driver = driver - self.handle = handle - self.is_open = handle > 0 + self._driver = driver + self._handle = handle # if a channel is missing from here, it is disabled (or in an undefined state). + self._max_adc = None + self._buffers = {} + self._max_samples = None self._channel_ranges = {} self._channel_offsets = {} + self._enabled_sources = [] + self._time_interval_ns = None + self._probe_attenuations = DEFAULT_PROBE_ATTENUATION + + @property + def driver(self): + return self._driver + + @property + def handle(self): + return self._handle + + @property + def is_open(self): + return self.handle is not None and self.handle > 0 + + @property + def max_adc(self): + return self._max_adc + + @property + def buffers(self): + return self._buffers + + @property + def max_samples(self): + return self._max_samples + + @property + def channel_ranges(self): + return self._channel_ranges + + @property + def channel_offsets(self): + return self._channel_offsets + + @property + def enabled_sources(self): + return self._enabled_sources + + @property + def time_interval_ns(self): + return self._time_interval_ns + + @property + def probe_attenuations(self): + return self._probe_attenuations - @requires_open("The device either did not initialise correctly or has already been closed.") - def close(self): - self.driver.close_unit(self) - self.handle = None - self.is_open = False + @probe_attenuations.setter + def probe_attenuations(self, value): + self._probe_attenuations = value @property @requires_open() From 23daf4fa24ee278685233d70168b85162ccbf7a2 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 12:54:09 +0200 Subject: [PATCH 042/129] Update close to reset all properties --- picosdk/device.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index bc48e28..a92dd00 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -133,6 +133,24 @@ def probe_attenuations(self, value): def info(self): return self.driver.get_unit_info(self) + @requires_open("The device either did not initialise correctly or has already been closed.") + def close(self): + self.driver.close_unit(self) + self._handle = None + self._max_adc = None + self._buffers.clear() + self._max_samples = None + self._channel_ranges.clear() + self._channel_offsets.clear() + self._enabled_sources.clear() + self._time_interval_ns = None + self._probe_attenuations = DEFAULT_PROBE_ATTENUATION.copy() + # Reset any cached timebase information if those attributes exist from prior modifications + if hasattr(self, '_cached_timebase_options'): + self._cached_timebase_options = None + if hasattr(self, '_cached_timebase_info'): + self._cached_timebase_info = None + def __enter__(self): return self From a4993d9e7e48863b1605f7b29c0efcf69e6222d5 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 12:54:38 +0200 Subject: [PATCH 043/129] Add a reset function to re-open the Device --- picosdk/device.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index a92dd00..7a548e2 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -160,6 +160,43 @@ def __exit__(self, *args): return True return False + @requires_open() + def reset(self): + """ + Closes and re-opens the connection to the PicoScope. + Attempts to re-open the same device by serial number if possible. + Resets internal cached state of this Device object. + Channel configurations and other settings will need to be reapplied. + """ + driver = self._driver + current_serial = None + resolution_to_use = None + + # Try to get serial number to re-open the same device + try: + device_info = driver.get_unit_info(self) + current_serial = device_info.serial + except Exception: + # If serial cannot be fetched, _python_open_unit will open the first available. + pass + + # Use the driver's default resolution if available for re-opening. + if hasattr(driver, 'DEFAULT_RESOLUTION'): + resolution_to_use = driver.DEFAULT_RESOLUTION + + self.close() + + # Re-open the unit + try: + new_handle = driver._python_open_unit(serial=current_serial, resolution=resolution_to_use) + self._handle = new_handle + except Exception as e: + self._handle = None + raise ConnectionError(f"Failed to re-open device during reset: {e}") + + if not self.is_open: + raise ConnectionError("Device reset failed: handle is invalid after re-open attempt.") + @requires_open() def set_channel(self, channel_name, enabled, coupling='DC', range_peak=float('inf'), analog_offset=None): """Configures a single analog channel. From 8d298cfa0319ff7b9306c24d1565ed4caa802060 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 13:00:57 +0200 Subject: [PATCH 044/129] Set max_adc to None in order to use the saved value in Device (uses maximum_value otherwise) --- picosdk/library.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 0c1bf89..d611d0a 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -653,14 +653,14 @@ def set_null_trigger(self, device, channel="A"): raise NotImplementedError("This device doesn't support set_null_trigger (yet)") @requires_device() - def set_simple_trigger(self, device, max_voltage, max_adc, enable=1, channel="A", threshold_mv=500, + def set_simple_trigger(self, device, max_voltage, max_adc=None, enable=1, channel="A", threshold_mv=500, direction="FALLING", delay=0, auto_trigger_ms=1000): """Set a simple trigger for a channel Args: device (picosdk.device.Device): The device instance max_voltage (int/float): The maximum voltage of the range used by the channel. (obtained from `set_channel`) - max_adc (int): The maximum ADC count. (obtained from `maximum_value`) + max_adc (int): Maximum ADC value for the device (if None, obtained via `maximum_value`) enable (bool): False to disable the trigger, True to enable it channel (str): The channel on which to trigger threshold_mv (int/float): The threshold in millivolts at which the trigger will fire. @@ -670,6 +670,8 @@ def set_simple_trigger(self, device, max_voltage, max_adc, enable=1, channel="A" If this is set to zero, the scope device will wait indefinitely for a trigger. """ if hasattr(self, '_set_simple_trigger') and len(self._set_simple_trigger.argtypes) == 7: + if not max_adc: + max_adc = self.maximum_value(device) adc_threshold = mv_to_adc(threshold_mv, max_voltage, max_adc) threshold_direction_id = None if self.PICO_THRESHOLD_DIRECTION: From e88dd9ca4435f32086bca86bc0f4178daacf838b Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 13:27:25 +0200 Subject: [PATCH 045/129] Add set_digital_port function --- picosdk/device.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/picosdk/device.py b/picosdk/device.py index 7a548e2..72dfc70 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -12,7 +12,7 @@ import math import time from picosdk.errors import DeviceCannotSegmentMemoryError, InvalidTimebaseError, ClosedDeviceError, \ - NoChannelsEnabledError, NoValidTimebaseForOptionsError + NoChannelsEnabledError, NoValidTimebaseForOptionsError, FeatureNotSupportedError DEFAULT_PROBE_ATTENUATION = { @@ -228,6 +228,20 @@ def set_channel(self, channel_name, enabled, coupling='DC', range_peak=float('in self._channel_offsets[channel_name] = analog_offset return self._channel_ranges[channel_name] + @requires_open() + def set_digital_port(self, port_number, enabled, voltage_level=1.8): + """Set the digital port + + Args: + port_number (int): identifies the port for digital data. (e.g. 0 for digital channels 0-7) + enabled (bool): whether or not to enable the channel (boolean) + voltage_level (float): the voltage at which the state transitions between 0 and 1. Range: –5.0 to 5.0 (V). + """ + info = self.info() + if not info.variant.endswith("MSO"): + raise FeatureNotSupportedError("This device has no digital ports.") + self.driver.set_digital_port(device=self, port_number=port_number, enabled=enabled, voltage_level=voltage_level) + @requires_open() def set_channels(self, *channel_configs): """ set_channels(self, *channel_configs) From 1afb8044b059695467ab5e3501de2b710c89cb2e Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 17:19:12 +0200 Subject: [PATCH 046/129] Remove trailing whitespace --- picosdk/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/device.py b/picosdk/device.py index 72dfc70..b570d71 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -48,7 +48,7 @@ def check_open_impl(self, *args, **kwargs): """TimebaseOptions: A type for specifying timebase constraints (pass to Device.find_timebase or Device.capture_*) -All are optional. Please specify the options which matter to you: +All are optional. Please specify the options which matter to you: - the maximum time interval (if you want the fastest/most precise timebase you can get), - the number of samples in one buffer, - the minimum total collection time (if you want at least x.y seconds of uninterrupted capture data) From c89f900c736020ae5c2b4de09bf0c1407f00c165 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 17:19:59 +0200 Subject: [PATCH 047/129] Use time_interval in seconds instead of ns --- picosdk/device.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index b570d71..364ff1e 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -77,7 +77,7 @@ def __init__(self, driver, handle): self._channel_ranges = {} self._channel_offsets = {} self._enabled_sources = [] - self._time_interval_ns = None + self._time_interval = None self._probe_attenuations = DEFAULT_PROBE_ATTENUATION @property @@ -117,8 +117,9 @@ def enabled_sources(self): return self._enabled_sources @property - def time_interval_ns(self): - return self._time_interval_ns + def time_interval(self): + """The time interval in seconds""" + return self._time_interval @property def probe_attenuations(self): From 2efb05816c6927ca1b473669bacbe52e7c7f1e58 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 17:20:35 +0200 Subject: [PATCH 048/129] Add enables channels or digital ports to the enabled sources --- picosdk/device.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/picosdk/device.py b/picosdk/device.py index 364ff1e..e8f8614 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -216,7 +216,8 @@ def set_channel(self, channel_name, enabled, coupling='DC', range_peak=float('in try: del self._channel_ranges[channel_name] del self._channel_offsets[channel_name] - except KeyError: + self._enabled_sources.remove(channel_name) + except (KeyError, ValueError): pass return @@ -227,6 +228,8 @@ def set_channel(self, channel_name, enabled, coupling='DC', range_peak=float('in range_peak=range_peak, analog_offset=analog_offset) self._channel_offsets[channel_name] = analog_offset + self._enabled_sources.append(channel_name) + return self._channel_ranges[channel_name] @requires_open() @@ -242,6 +245,13 @@ def set_digital_port(self, port_number, enabled, voltage_level=1.8): if not info.variant.endswith("MSO"): raise FeatureNotSupportedError("This device has no digital ports.") self.driver.set_digital_port(device=self, port_number=port_number, enabled=enabled, voltage_level=voltage_level) + if enabled: + self._enabled_sources.append(port_number) + else: + try: + self._enabled_sources.remove(port_number) + except ValueError: + pass @requires_open() def set_channels(self, *channel_configs): From 89b1af67e31c7079c7b66ef0d79a8ac73078b491 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 19:30:46 +0200 Subject: [PATCH 049/129] Add get_timebase function --- picosdk/device.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index e8f8614..c35fddc 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -328,6 +328,14 @@ def find_timebase(self, timebase_options): args = (last_error.args[0],) raise NoValidTimebaseForOptionsError(*args) + @requires_open() + def get_timebase(self, timebase_id, no_of_samples, oversample=1, segment_index=0): + """Query the device about what time precision modes it can handle.""" + timebase_info = self.driver.get_timebase(self, timebase_id, no_of_samples, oversample, segment_index) + self._max_samples = timebase_info.max_samples + self._time_interval = timebase_info.time_interval + return timebase_info + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) From f57ef74ef1f6e0edc57e331129b828304bcd220e Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:39:37 +0200 Subject: [PATCH 050/129] Add errors --- picosdk/errors.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/picosdk/errors.py b/picosdk/errors.py index ad03572..898879b 100644 --- a/picosdk/errors.py +++ b/picosdk/errors.py @@ -60,10 +60,14 @@ class PicoSDKCtypesError(PicoError, IOError): class ClosedDeviceError(PicoError, IOError): pass +class InvalidRangeOfChannel(PicoError, ValueError): + pass class NoChannelsEnabledError(PicoError, ValueError): pass +class ChannelNotEnabledError(PicoError, ValueError): + pass class NoValidTimebaseForOptionsError(PicoError, ValueError): pass From 0fc273c55881df16ab245211f7f6e4a28d4199c1 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:40:58 +0200 Subject: [PATCH 051/129] Make a set of enabled sources --- picosdk/device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index c35fddc..2c7603f 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -76,7 +76,7 @@ def __init__(self, driver, handle): self._max_samples = None self._channel_ranges = {} self._channel_offsets = {} - self._enabled_sources = [] + self._enabled_sources = set() self._time_interval = None self._probe_attenuations = DEFAULT_PROBE_ATTENUATION @@ -217,7 +217,7 @@ def set_channel(self, channel_name, enabled, coupling='DC', range_peak=float('in del self._channel_ranges[channel_name] del self._channel_offsets[channel_name] self._enabled_sources.remove(channel_name) - except (KeyError, ValueError): + except KeyError: pass return @@ -228,7 +228,7 @@ def set_channel(self, channel_name, enabled, coupling='DC', range_peak=float('in range_peak=range_peak, analog_offset=analog_offset) self._channel_offsets[channel_name] = analog_offset - self._enabled_sources.append(channel_name) + self._enabled_sources.add(channel_name) return self._channel_ranges[channel_name] From 6aa14ef5b93bb97962775a3151edf441e047781c Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:41:22 +0200 Subject: [PATCH 052/129] Update docstring of get_timebase --- picosdk/device.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/picosdk/device.py b/picosdk/device.py index 2c7603f..662d7c1 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -330,7 +330,17 @@ def find_timebase(self, timebase_options): @requires_open() def get_timebase(self, timebase_id, no_of_samples, oversample=1, segment_index=0): - """Query the device about what time precision modes it can handle.""" + """Query the device about what time precision modes it can handle. + + Returns: + namedtuple: + - timebase_id: The id corresponding to the timebase used + - time_interval: The time interval between readings at the selected timebase. + - time_units: The unit of time (not supported in e.g. 3000a) + - max_samples: The maximum number of samples available. The number may vary depending on the number of + channels enabled and the timebase chosen. + - segment_id: The index of the memory segment to use + """ timebase_info = self.driver.get_timebase(self, timebase_id, no_of_samples, oversample, segment_index) self._max_samples = timebase_info.max_samples self._time_interval = timebase_info.time_interval From fce07636eb79541fcca8947287ad4c4f561fd7dc Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:41:46 +0200 Subject: [PATCH 053/129] Add memory_segments function to Device --- picosdk/device.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index 662d7c1..afb3578 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -346,6 +346,19 @@ def get_timebase(self, timebase_id, no_of_samples, oversample=1, segment_index=0 self._time_interval = timebase_info.time_interval return timebase_info + @requires_open() + def memory_segments(self, number_segments): + """The number of segments defaults to 1, meaning that each capture fills the scope's available memory. + This function allows you to divide the memory into a number of segments so that the scope can store several + waveforms sequentially. + + Returns: + int: The number of samples available in each segment. This is the total number over all channels, + so if more than one channel is in use then the number of samples available to each + channel is max_samples divided by the number of channels. + """ + return self.driver.memory_segments(number_segments) + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) From b6ad22c7624250589f85d6643b5a2705f9bd66bc Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:42:03 +0200 Subject: [PATCH 054/129] Add maximum_value function to Device --- picosdk/device.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index afb3578..8ded628 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -359,6 +359,12 @@ def memory_segments(self, number_segments): """ return self.driver.memory_segments(number_segments) + @requires_open() + def maximum_value(self): + """Get the maximum ADC value for this device.""" + self._max_adc = self.driver.maximum_value(self) + return self._max_adc + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) From da6798707a0a69b76f906b2efe4a364baf5c52d7 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:42:37 +0200 Subject: [PATCH 055/129] Add set_null_trigger function to Device --- picosdk/device.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index 8ded628..a57bafa 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -365,6 +365,10 @@ def maximum_value(self): self._max_adc = self.driver.maximum_value(self) return self._max_adc + @requires_open() + def set_null_trigger(self): + self.driver.set_null_trigger() + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) From ebcfb0947e5ff6e259d31e81546982d28233180b Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:42:58 +0200 Subject: [PATCH 056/129] Add set_simple_trigger function to Device --- picosdk/device.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index a57bafa..74996f8 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -370,6 +370,30 @@ def set_null_trigger(self): self.driver.set_null_trigger() @requires_open() + def set_simple_trigger(self, channel, enable=True, threshold_mv=500, direction="FALLING", delay=0, + auto_trigger_ms=1000): + """Set a simple trigger for a channel + + Args: + channel (str): The channel on which to trigger + enable (bool): False to disable the trigger, True to enable it + threshold_mv (int): The threshold in millivolts at which the trigger will fire. + direction (str): The direction in which the signal must move to cause a trigger. + delay (int): The time (sample periods) between the trigger occurring and the first sample. + auto_trigger_ms (int): The number of milliseconds the device will wait if no trigger occurs. + If this is set to zero, the scope device will wait indefinitely for a trigger. + """ + if channel not in self.enabled_sources: + raise ChannelNotEnabledError(f"Channel {channel} is not enabled. Please run set_channel first.") + if channel not in self.channel_ranges: + raise InvalidRangeOfChannel(f"The range of channel {channel} is not valid or isn't correctly obtained via" + "set_channel.") + max_voltage = self.channel_ranges[channel] + max_adc = self.max_adc if self.max_adc else self.maximum_value() + + self.driver.set_simple_trigger(max_voltage, max_adc, enable, channel, threshold_mv, direction, delay, + auto_trigger_ms) + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) timebase_options: TimebaseOptions object, specifying at least 1 constraint, and optionally oversample. From 950ed57a1b02f19c636a5cf481ea6810e7c134d8 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:43:29 +0200 Subject: [PATCH 057/129] Add run_block function to Device --- picosdk/device.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index 74996f8..b83e9f2 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -393,6 +393,12 @@ def set_simple_trigger(self, channel, enable=True, threshold_mv=500, direction=" self.driver.set_simple_trigger(max_voltage, max_adc, enable, channel, threshold_mv, direction, delay, auto_trigger_ms) + + @requires_open() + def run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample=1, segment_index=0): + """This function starts collecting data in block mode.""" + self.driver.run_block(pre_trigger_samples, post_trigger_samples, timebase_id, oversample, segment_index) + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) From 1b515fb3c6c5c4e5d22329b40d458026e48c3327 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:43:48 +0200 Subject: [PATCH 058/129] Add is_ready function to Device --- picosdk/device.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index b83e9f2..2cbe01e 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -399,6 +399,12 @@ def run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, over """This function starts collecting data in block mode.""" self.driver.run_block(pre_trigger_samples, post_trigger_samples, timebase_id, oversample, segment_index) + @requires_open() + def is_ready(self): + """poll this function to find out when block mode is ready or has triggered. + returns: True if data is ready, False otherwise.""" + return self.driver.is_ready(self) + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) From 926ee598af8052e30d09f4176507a2d15bb1f017 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:44:10 +0200 Subject: [PATCH 059/129] Add stop_block_capture function to Device --- picosdk/device.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index 2cbe01e..3a3e5db 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -405,6 +405,15 @@ def is_ready(self): returns: True if data is ready, False otherwise.""" return self.driver.is_ready(self) + @requires_open() + def stop_block_capture(self, timeout_minutes=5): + """Poll the driver to see if it has finished collecting the requested samples. + + Args: + timeout_minutes (int/float): The timeout in minutes. If the time exceeds the timeout, the poll stops. + """ + self.driver.stop_block_capture(timeout_minutes) + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) From 8ff420cf585491085e5698a851f2b69af90b980d Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:44:31 +0200 Subject: [PATCH 060/129] Add set_data_buffer function to Device --- picosdk/device.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index 3a3e5db..4599d42 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -414,6 +414,18 @@ def stop_block_capture(self, timeout_minutes=5): """ self.driver.stop_block_capture(timeout_minutes) + @requires_open() + def set_data_buffer(self, channel_or_port, buffer_length, segment_index=0, mode='NONE'): + """Set the data buffer for a specific channel. + + Args: + channel_or_port: Channel (e.g. 'A', 'B') or digital port (e.g. 0, 1) to set data for + buffer_length: The size of the buffer array (equal to no_of_samples) + segment_index: The number of the memory segment to be used (default is 0) + mode: The ratio mode to be used (default is 'NONE') + """ + self._buffers[channel_or_port] = self.driver.set_data_buffer(self, channel_or_port, buffer_length, segment_index, mode) + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) From f0502f0989cf3781a82d0aef01aa60fb8d8ac5a8 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:44:50 +0200 Subject: [PATCH 061/129] Add get_values function to Device --- picosdk/device.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index 4599d42..e614ab3 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -426,6 +426,39 @@ def set_data_buffer(self, channel_or_port, buffer_length, segment_index=0, mode= """ self._buffers[channel_or_port] = self.driver.set_data_buffer(self, channel_or_port, buffer_length, segment_index, mode) + @requires_open() + def get_values(self,start_index=0, downsample_ratio=0, + downsample_ratio_mode="NONE", segment_index=0, output_dir=".", filename="data", save_to_file=False, + ): + """Get stored data values from the scope and store it in a clean SingletonScopeDataDict object. + + This function is used after data collection has stopped. It gets the stored data from the scope, with or + without downsampling, starting at the specified sample number. + + The returned captured data is converted to mV. + + Args: + + samples (int): The number of samples to retrieve from the scope. + time_interval_sec (float): The time interval between samples in seconds. (obtained from get_timebase) + max_voltage (dict): The maximum voltage of the range used per channel. (obtained from set_channel) + start_index (int): A zero-based index that indicates the start point for data collection. It is measured in + sample intervals from the start of the buffer. + downsample_ratio (int): The downsampling factor that will be applied to the raw data. + downsample_ratio_mode (str): Which downsampling mode to use. + segment_index (int): Memory segment index + output_dir (str): The output directory where the json file will be saved. + filename (str): The name of the json file where the data will be stored + save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise + probe_attenuation (dict): The attenuation factor of the probe used per the channel (1 or 10). + + Returns: + Tuple of (captured data including time, overflow warnings) + """ + self.driver.get_values(self, self.buffers, self.max_samples, self.time_interval, self.channel_ranges, + start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, + filename, save_to_file, self.probe_attenuations) + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) From bd982fc120961f88425cf8a14a57d895859828e4 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:45:19 +0200 Subject: [PATCH 062/129] Make use of max_adc property or the function maximum_value --- picosdk/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/device.py b/picosdk/device.py index e614ab3..d759a99 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -522,7 +522,7 @@ def capture_block(self, timebase_options, channel_configs=()): voltages = {} - max_adc = self.driver.maximum_value(self) + max_adc = self.max_adc if self.max_adc else self.maximum_value() for channel, raw_array in raw_data.items(): array = raw_array.astype(numpy.dtype('float32'), casting='safe') factor = self._channel_ranges[channel] / max_adc From 221c9e3aae43a34d4acee3f3be3789611e4e2f7d Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 16 Jun 2025 21:45:33 +0200 Subject: [PATCH 063/129] Import errors --- picosdk/device.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index d759a99..0727d32 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -11,8 +11,9 @@ import numpy import math import time -from picosdk.errors import DeviceCannotSegmentMemoryError, InvalidTimebaseError, ClosedDeviceError, \ - NoChannelsEnabledError, NoValidTimebaseForOptionsError, FeatureNotSupportedError +from picosdk.errors import (DeviceCannotSegmentMemoryError, InvalidTimebaseError, ClosedDeviceError, + NoChannelsEnabledError, NoValidTimebaseForOptionsError, FeatureNotSupportedError, ChannelNotEnabledError, + InvalidRangeOfChannel) DEFAULT_PROBE_ATTENUATION = { From 6d86b61a5eee843278217ee8dccf0c7567e76dfc Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 09:18:37 +0200 Subject: [PATCH 064/129] Update documentation of get_values --- picosdk/device.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index 0727d32..3ea3962 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -439,10 +439,6 @@ def get_values(self,start_index=0, downsample_ratio=0, The returned captured data is converted to mV. Args: - - samples (int): The number of samples to retrieve from the scope. - time_interval_sec (float): The time interval between samples in seconds. (obtained from get_timebase) - max_voltage (dict): The maximum voltage of the range used per channel. (obtained from set_channel) start_index (int): A zero-based index that indicates the start point for data collection. It is measured in sample intervals from the start of the buffer. downsample_ratio (int): The downsampling factor that will be applied to the raw data. From c37a0382b48ef8512eb1080f35aefcf968482f19 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 09:19:08 +0200 Subject: [PATCH 065/129] Add set_and_load_data function to Device --- picosdk/device.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index 3ea3962..3944664 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -456,6 +456,31 @@ def get_values(self,start_index=0, downsample_ratio=0, start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, filename, save_to_file, self.probe_attenuations) + @requires_open() + def set_and_load_data(self, segment_index=0, ratio_mode='NONE', start_index=0, downsample_ratio=0, + downsample_ratio_mode="NONE", output_dir=".", filename="data", save_to_file=False): + """Load values from the device. + + Combines set_data_buffer and get_values to load values from the device. + + Args: + segment_index (int): Memory segment index + ratio_mode: The ratio mode to be used (default is 'NONE') + start_index (int): A zero-based index that indicates the start point for data collection. It is measured in + sample intervals from the start of the buffer. + downsample_ratio (int): The downsampling factor that will be applied to the raw data. + downsample_ratio_mode (str): Which downsampling mode to use. + output_dir (str): The output directory where the json file will be saved. + filename (str): The name of the json file where the data will be stored + save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise + + Returns: + Tuple of (captured data including time, overflow warnings) + """ + self.driver.set_and_load_data(self.enabled_sources, self.max_samples, self.time_interval, self.channel_ranges, + segment_index, ratio_mode, start_index, downsample_ratio, downsample_ratio_mode, + self.probe_attenuations, output_dir, filename, save_to_file) + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) From c31794037a32c04e9cd154333cdfe2258e184cdb Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 09:19:24 +0200 Subject: [PATCH 066/129] Add set_trigger_channel_properties function to Device --- picosdk/device.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index 3944664..78f2d76 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -481,6 +481,27 @@ def set_and_load_data(self, segment_index=0, ratio_mode='NONE', start_index=0, d segment_index, ratio_mode, start_index, downsample_ratio, downsample_ratio_mode, self.probe_attenuations, output_dir, filename, save_to_file) + @requires_open() + def set_trigger_channel_properties(self, threshold_upper, threshold_upper_hysteresis, threshold_lower, + threshold_lower_hysteresis, channel, threshold_mode, aux_output_enable, + auto_trigger_milliseconds): + """Set the trigger channel properties for the device. + + Args: + threshold_upper: Upper threshold in ADC counts + threshold_upper_hysteresis: Hysteresis for upper threshold in ADC counts + threshold_lower: Lower threshold in ADC counts + threshold_lower_hysteresis: Hysteresis for lower threshold in ADC counts + channel: Channel to set properties for (e.g. 'A', 'B', 'C', 'D') + threshold_mode: Threshold mode (e.g. "LEVEL", "WINDOW") + aux_output_enable: Enable auxiliary output (boolean) (Not used in eg. ps2000a, ps3000a, ps4000a) + auto_trigger_milliseconds: The number of milliseconds for which the scope device will wait for a trigger + before timing out. If set to zero, the scope device will wait indefinitely for a trigger + """ + self.driver.set_trigger_channel_properties(self, threshold_upper, threshold_upper_hysteresis, threshold_lower, + threshold_lower_hysteresis, channel, threshold_mode, + aux_output_enable, auto_trigger_milliseconds) + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) From 6e748d5d3051443164e43c9a3a9d328500c0c5b5 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 09:19:35 +0200 Subject: [PATCH 067/129] Add stop function to Device --- picosdk/device.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index 78f2d76..cef54bf 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -502,6 +502,13 @@ def set_trigger_channel_properties(self, threshold_upper, threshold_upper_hyster threshold_lower_hysteresis, channel, threshold_mode, aux_output_enable, auto_trigger_milliseconds) + @requires_open() + def stop(self): + """This function stops the scope device from sampling data. + If this function is called before a trigger event occurs, the oscilloscope may not contain valid data. + """ + self.driver.stop(self) + @requires_open() def capture_block(self, timebase_options, channel_configs=()): """device.capture_block(timebase_options, channel_configs) From 44616dfbdf5ebe58594893cec5a037e252c16fd5 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 09:19:50 +0200 Subject: [PATCH 068/129] Add set_sig_gen_built_in function to Device --- picosdk/device.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index cef54bf..f668ed1 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -580,3 +580,30 @@ def capture_block(self, timebase_options, channel_configs=()): voltages[channel] = array return times, voltages, overflow_warnings + + @requires_open() + def set_sig_gen_built_in(self, offset_voltage=0, pk_to_pk=2000000, wave_type="SINE", + start_frequency=10000, stop_frequency=10000, increment=0, + dwell_time=1, sweep_type="UP", operation='ES_OFF', shots=0, sweeps=0, + trigger_type="RISING", trigger_source="NONE", ext_in_threshold=1): + """Set up the signal generator to output a built-in waveform. + + Args: + offset_voltage: Offset voltage in microvolts (default 0) + pk_to_pk: Peak-to-peak voltage in microvolts (default 2000000) + wave_type: Type of waveform (e.g. "SINE", "SQUARE", "TRIANGLE") + start_frequency: Start frequency in Hz (default 1000.0) + stop_frequency: Stop frequency in Hz (default 1000.0) + increment: Frequency increment in Hz (default 0.0) + dwell_time: Time at each frequency in seconds (default 1.0) + sweep_type: Sweep type (e.g. "UP", "DOWN", "UPDOWN") + operation: Configures the white noise/PRBS (e.g. "ES_OFF", "WHITENOISE", "PRBS") + shots: Number of shots per trigger (default 1) + sweeps: Number of sweeps (default 1) + trigger_type: Type of trigger (e.g. "RISING", "FALLING") + trigger_source: Source of trigger (e.g. "NONE", "SCOPE_TRIG") + ext_in_threshold: External trigger threshold in ADC counts + """ + self.driver.set_sig_gen_built_in(self, offset_voltage, pk_to_pk, wave_type, start_frequency, stop_frequency, + increment, dwell_time, sweep_type, operation, shots, sweeps, trigger_type, + trigger_source, ext_in_threshold) From 6f7c78b10a5f0ab3a7d02d477380ce7e6683e10d Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 11:49:13 +0200 Subject: [PATCH 069/129] Use info property correctly --- picosdk/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index f668ed1..7dabc10 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -242,8 +242,8 @@ def set_digital_port(self, port_number, enabled, voltage_level=1.8): enabled (bool): whether or not to enable the channel (boolean) voltage_level (float): the voltage at which the state transitions between 0 and 1. Range: –5.0 to 5.0 (V). """ - info = self.info() - if not info.variant.endswith("MSO"): + info = self.info + if not info.variant.decode('utf-8').endswith("MSO"): raise FeatureNotSupportedError("This device has no digital ports.") self.driver.set_digital_port(device=self, port_number=port_number, enabled=enabled, voltage_level=voltage_level) if enabled: From 893a2a7534e80b69d1bce571c6a040fc103f08f8 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 11:49:38 +0200 Subject: [PATCH 070/129] Fix adding to set instead of list --- picosdk/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/device.py b/picosdk/device.py index 7dabc10..d90ed39 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -247,7 +247,7 @@ def set_digital_port(self, port_number, enabled, voltage_level=1.8): raise FeatureNotSupportedError("This device has no digital ports.") self.driver.set_digital_port(device=self, port_number=port_number, enabled=enabled, voltage_level=voltage_level) if enabled: - self._enabled_sources.append(port_number) + self._enabled_sources.add(port_number) else: try: self._enabled_sources.remove(port_number) From 5a2bd3dc11accfde7f8cbb09717fb19c2ea70353 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 11:50:24 +0200 Subject: [PATCH 071/129] Add the device to the functions --- picosdk/device.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index d90ed39..a62b0e3 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -358,7 +358,7 @@ def memory_segments(self, number_segments): so if more than one channel is in use then the number of samples available to each channel is max_samples divided by the number of channels. """ - return self.driver.memory_segments(number_segments) + return self.driver.memory_segments(self, number_segments) @requires_open() def maximum_value(self): @@ -392,13 +392,13 @@ def set_simple_trigger(self, channel, enable=True, threshold_mv=500, direction=" max_voltage = self.channel_ranges[channel] max_adc = self.max_adc if self.max_adc else self.maximum_value() - self.driver.set_simple_trigger(max_voltage, max_adc, enable, channel, threshold_mv, direction, delay, + self.driver.set_simple_trigger(self, max_voltage, max_adc, enable, channel, threshold_mv, direction, delay, auto_trigger_ms) @requires_open() def run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample=1, segment_index=0): """This function starts collecting data in block mode.""" - self.driver.run_block(pre_trigger_samples, post_trigger_samples, timebase_id, oversample, segment_index) + self.driver.run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample, segment_index) @requires_open() def is_ready(self): @@ -413,7 +413,7 @@ def stop_block_capture(self, timeout_minutes=5): Args: timeout_minutes (int/float): The timeout in minutes. If the time exceeds the timeout, the poll stops. """ - self.driver.stop_block_capture(timeout_minutes) + self.driver.stop_block_capture(self, timeout_minutes) @requires_open() def set_data_buffer(self, channel_or_port, buffer_length, segment_index=0, mode='NONE'): @@ -452,9 +452,9 @@ def get_values(self,start_index=0, downsample_ratio=0, Returns: Tuple of (captured data including time, overflow warnings) """ - self.driver.get_values(self, self.buffers, self.max_samples, self.time_interval, self.channel_ranges, - start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, - filename, save_to_file, self.probe_attenuations) + return self.driver.get_values(self, self.buffers, self.max_samples, self.time_interval, self.channel_ranges, + start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, + filename, save_to_file, self.probe_attenuations) @requires_open() def set_and_load_data(self, segment_index=0, ratio_mode='NONE', start_index=0, downsample_ratio=0, From b9af6f00a3399328bacf5b051a09671a6101ed09 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 11:51:50 +0200 Subject: [PATCH 072/129] Update set_and_load_data to use the correct buffers --- picosdk/device.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index a62b0e3..920204e 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -477,9 +477,11 @@ def set_and_load_data(self, segment_index=0, ratio_mode='NONE', start_index=0, d Returns: Tuple of (captured data including time, overflow warnings) """ - self.driver.set_and_load_data(self.enabled_sources, self.max_samples, self.time_interval, self.channel_ranges, - segment_index, ratio_mode, start_index, downsample_ratio, downsample_ratio_mode, - self.probe_attenuations, output_dir, filename, save_to_file) + for source in self.enabled_sources: + self.set_data_buffer(source, self.max_samples, segment_index, ratio_mode) + + return self.get_values(start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, + filename, save_to_file) @requires_open() def set_trigger_channel_properties(self, threshold_upper, threshold_upper_hysteresis, threshold_lower, From 549a0ffaf7bd64eb8cfd61ce371a7731bd5cf72a Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 11:52:18 +0200 Subject: [PATCH 073/129] Remove unused import --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index d611d0a..637a686 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -11,7 +11,7 @@ import json import sys -from ctypes import c_int16, c_int32, c_uint32, c_float, c_double, c_void_p, create_string_buffer, byref +from ctypes import c_int16, c_int32, c_uint32, c_float, c_void_p, create_string_buffer, byref from ctypes.util import find_library import collections import time From 327d9580747d7b0e5715b2fc4bd1fe78a354273b Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 11:53:05 +0200 Subject: [PATCH 074/129] Update argument set_simple_trigger according to docstring and update type in docstring --- picosdk/library.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 637a686..284095e 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -653,8 +653,8 @@ def set_null_trigger(self, device, channel="A"): raise NotImplementedError("This device doesn't support set_null_trigger (yet)") @requires_device() - def set_simple_trigger(self, device, max_voltage, max_adc=None, enable=1, channel="A", threshold_mv=500, - direction="FALLING", delay=0, auto_trigger_ms=1000): + def set_simple_trigger(self, device, max_voltage, max_adc=None, enable=True, channel="A", threshold_mv=500, + direction="FALLING", delay=0, auto_trigger_ms=1000): """Set a simple trigger for a channel Args: @@ -663,7 +663,7 @@ def set_simple_trigger(self, device, max_voltage, max_adc=None, enable=1, channe max_adc (int): Maximum ADC value for the device (if None, obtained via `maximum_value`) enable (bool): False to disable the trigger, True to enable it channel (str): The channel on which to trigger - threshold_mv (int/float): The threshold in millivolts at which the trigger will fire. + threshold_mv (int): The threshold in millivolts at which the trigger will fire. direction (str): The direction in which the signal must move to cause a trigger. delay (int): The time (sample periods) between the trigger occurring and the first sample. auto_trigger_ms (int): The number of milliseconds the device will wait if no trigger occurs. From 51bfa173db55a4aef9cb28c10c09d7307393cbd6 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 11:53:46 +0200 Subject: [PATCH 075/129] Return only the buffer instead dict with set_data_buffer --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index 284095e..a933bec 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -821,7 +821,7 @@ def set_data_buffer(self, device, channel_or_port, buffer_length, segment_index= if status != self.PICO_STATUS['PICO_OK']: raise ArgumentOutOfRangeError(f"set_data_buffer failed ({constants.pico_tag(status)})") - return {channel_or_port: buffer} + return buffer @requires_device() def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={}, start_index=0, downsample_ratio=0, From 44309eccfe955400533b41a1d2594cb57e2d4967 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 11:54:22 +0200 Subject: [PATCH 076/129] Correct the time to have the same amount of samples as the data --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index a933bec..edf39d5 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -881,7 +881,7 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} time_sec = numpy.linspace(0, (samples - 1) * time_interval_sec, - samples) + num_samples_retrieved) scope_data["time"] = numpy.array(time_sec) if save_to_file: From 5efce34a4b7c31fda93c8f88a3cf246f50fcd0b0 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 11:55:09 +0200 Subject: [PATCH 077/129] Update use of set_data_buffer --- picosdk/library.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index edf39d5..06ac955 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -927,8 +927,7 @@ def set_and_load_data(self, device, active_sources, buffer_length, time_interval """ buffers = {} for source in active_sources: - buffer = self.set_data_buffer(device, source, buffer_length, segment_index, ratio_mode) - buffers = buffers | buffer + buffers[source] = self.set_data_buffer(device, source, buffer_length, segment_index, ratio_mode) return self.get_values(device, buffers, buffer_length, time_interval_sec, max_voltage, start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, filename, From 1c14e59dab77578cc64df2f9ab2227eef861c04e Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 14:01:33 +0200 Subject: [PATCH 078/129] Define max samples as the sum of pre and post trigger samples --- picosdk/device.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index 920204e..cd5557b 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -343,7 +343,6 @@ def get_timebase(self, timebase_id, no_of_samples, oversample=1, segment_index=0 - segment_id: The index of the memory segment to use """ timebase_info = self.driver.get_timebase(self, timebase_id, no_of_samples, oversample, segment_index) - self._max_samples = timebase_info.max_samples self._time_interval = timebase_info.time_interval return timebase_info @@ -398,6 +397,7 @@ def set_simple_trigger(self, channel, enable=True, threshold_mv=500, direction=" @requires_open() def run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample=1, segment_index=0): """This function starts collecting data in block mode.""" + self._max_samples = pre_trigger_samples + post_trigger_samples self.driver.run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample, segment_index) @requires_open() @@ -416,7 +416,7 @@ def stop_block_capture(self, timeout_minutes=5): self.driver.stop_block_capture(self, timeout_minutes) @requires_open() - def set_data_buffer(self, channel_or_port, buffer_length, segment_index=0, mode='NONE'): + def set_data_buffer(self, channel_or_port, segment_index=0, mode='NONE'): """Set the data buffer for a specific channel. Args: @@ -425,6 +425,8 @@ def set_data_buffer(self, channel_or_port, buffer_length, segment_index=0, mode= segment_index: The number of the memory segment to be used (default is 0) mode: The ratio mode to be used (default is 'NONE') """ + self._buffers[channel_or_port] = self.driver.set_data_buffer(self, channel_or_port, self.max_samples, + segment_index, mode) self._buffers[channel_or_port] = self.driver.set_data_buffer(self, channel_or_port, buffer_length, segment_index, mode) @requires_open() @@ -478,7 +480,7 @@ def set_and_load_data(self, segment_index=0, ratio_mode='NONE', start_index=0, d Tuple of (captured data including time, overflow warnings) """ for source in self.enabled_sources: - self.set_data_buffer(source, self.max_samples, segment_index, ratio_mode) + self.set_data_buffer(source, segment_index, ratio_mode) return self.get_values(start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, filename, save_to_file) From 159428c8cba1ac1c6b91c7a014f6c911ccb23c8e Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 14:04:09 +0200 Subject: [PATCH 079/129] Add set_all_data_buffers function to set all data buffers for each enabled channel/port --- picosdk/device.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index cd5557b..c3c0145 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -427,7 +427,19 @@ def set_data_buffer(self, channel_or_port, segment_index=0, mode='NONE'): """ self._buffers[channel_or_port] = self.driver.set_data_buffer(self, channel_or_port, self.max_samples, segment_index, mode) - self._buffers[channel_or_port] = self.driver.set_data_buffer(self, channel_or_port, buffer_length, segment_index, mode) + + @requires_open() + def set_all_data_buffers(self, segment_index=0, mode='NONE'): + """Set the data buffer for each enabled channels and ports + + Args: + channel_or_port: Channel (e.g. 'A', 'B') or digital port (e.g. 0, 1) to set data for + buffer_length: The size of the buffer array (equal to no_of_samples) + segment_index: The number of the memory segment to be used (default is 0) + mode: The ratio mode to be used (default is 'NONE') + """ + for channel_or_port in self.enabled_sources: + self.set_data_buffer(channel_or_port, segment_index, mode) @requires_open() def get_values(self,start_index=0, downsample_ratio=0, @@ -479,8 +491,7 @@ def set_and_load_data(self, segment_index=0, ratio_mode='NONE', start_index=0, d Returns: Tuple of (captured data including time, overflow warnings) """ - for source in self.enabled_sources: - self.set_data_buffer(source, segment_index, ratio_mode) + self.set_all_data_buffers(segment_index, ratio_mode) return self.get_values(start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, filename, save_to_file) From ca04a99bee4e4b2cfd9cf8fd3ecb0a6d0c55609d Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 14:42:03 +0200 Subject: [PATCH 080/129] Add function to trigger on a digital channel --- picosdk/device.py | 10 ++++++++++ picosdk/library.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index c3c0145..3f73f87 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -517,6 +517,16 @@ def set_trigger_channel_properties(self, threshold_upper, threshold_upper_hyster threshold_lower_hysteresis, channel, threshold_mode, aux_output_enable, auto_trigger_milliseconds) + @requires_open() + def set_digital_channel_trigger(self, channel_number=15, direction="DIRECTION_RISING"): + """Set a simple trigger on the digital channels. + + Args: + channel_number (int): The number of the digital channel on which to trigger.(e.g. 0 for D0, 1 for D1,...) + direction (str): The direction in which the signal must move to cause a trigger. + """ + self.driver.set_digital_channel_trigger(self, channel_number, direction) + @requires_open() def stop(self): """This function stops the scope device from sampling data. diff --git a/picosdk/library.py b/picosdk/library.py index 06ac955..51d8463 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -693,6 +693,35 @@ def set_simple_trigger(self, device, max_voltage, max_adc=None, enable=True, cha else: raise NotImplementedError("This device doesn't support set_simple_trigger (yet)") + @requires_device() + def set_digital_channel_trigger(self, device, channel_number=15, direction="DIRECTION_RISING"): + """Set a simple trigger on the digital channels. + + Args: + channel_number (int): The number of the digital channel on which to trigger.(e.g. 0 for D0, 1 for D1,...) + direction (str): The direction in which the signal must move to cause a trigger. + """ + if (hasattr(self, '_set_trigger_digital_port_properties') and + len(self._set_trigger_digital_port_properties.argtypes) == 3): + digital_properties = getattr(self, self.name.upper() +'_DIGITAL_CHANNEL_DIRECTIONS', None) + digital_channels = getattr(self, self.name.upper() +'_DIGITAL_CHANNEL', None) + directions = getattr(self, self.name.upper() +'_DIGITAL_DIRECTION', None) + if digital_properties and digital_channels and directions: + digital_channel = self.name.upper() + '_DIGITAL_CHANNEL_' + str(channel_number) + direction = self.name.upper() + '_DIGITAL_' + direction + digital_properties(digital_channels[digital_channel], + directions[direction]) + args = (device.handle, digital_properties, 1) + converted_args = self._convert_args(self._set_trigger_digital_port_properties, args) + status = self._set_trigger_digital_port_properties(*converted_args) + if status != self.PICO_STATUS['PICO_OK']: + raise InvalidTriggerParameters("set_trigger_digital_port_properties failed " + f"({constants.pico_tag(status)})") + else: + raise PicoError("Couldn't set digital channel trigger. " + f"Check if all enumerations are implemented for {self.name}") + else: + raise NotImplementedError("This device doesn't support set_digital_channel_trigger (yet)") @requires_device() def run_block(self, device, pre_trigger_samples, post_trigger_samples, timebase_id, oversample=1, segment_index=0): From 9bde7d182a215c4ae2dc1ca0d5dcb2767543dd51 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 17 Jun 2025 14:51:04 +0200 Subject: [PATCH 081/129] Add function to set trigger delay --- picosdk/device.py | 13 +++++++++++++ picosdk/library.py | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index 3f73f87..f0295fa 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -527,6 +527,19 @@ def set_digital_channel_trigger(self, channel_number=15, direction="DIRECTION_RI """ self.driver.set_digital_channel_trigger(self, channel_number, direction) + @requires_open() + def set_trigger_delay(self, delay): + """This function sets the post-trigger delay, which causes capture to start a defined time after the + trigger event. + + For example, if delay=100 then the scope would wait 100 sample periods before sampling. + At a timebase of 500 MS/s, or 2 ns per sample, the total delay would then be 100 x 2 ns = 200 ns. + + Args: + delay (int): The time between the trigger occurring and the first sample. + """ + self.driver.set_trigger_delay(self, delay) + @requires_open() def stop(self): """This function stops the scope device from sampling data. diff --git a/picosdk/library.py b/picosdk/library.py index 51d8463..49121bd 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -723,6 +723,27 @@ def set_digital_channel_trigger(self, device, channel_number=15, direction="DIRE else: raise NotImplementedError("This device doesn't support set_digital_channel_trigger (yet)") + @requires_device() + def set_trigger_delay(self, device, delay): + """This function sets the post-trigger delay, which causes capture to start a defined time after the + trigger event. + + For example, if delay=100 then the scope would wait 100 sample periods before sampling. + At a timebase of 500 MS/s, or 2 ns per sample, the total delay would then be 100 x 2 ns = 200 ns. + + Args: + delay (int): The time between the trigger occurring and the first sample. + """ + if hasattr(self, '_set_trigger_delay') and len(self._set_trigger_delay.argtypes) == 2: + args = (device.handle, delay) + converted_args = self._convert_args(self._set_trigger_delay, args) + status = self._set_trigger_delay(*converted_args) + + if status != self.PICO_STATUS['PICO_OK']: + raise InvalidTriggerParameters(f"set_trigger_delay failed ({constants.pico_tag(status)})") + else: + raise NotImplementedError("This device doesn't support set_trigger_delay (yet)") + @requires_device() def run_block(self, device, pre_trigger_samples, post_trigger_samples, timebase_id, oversample=1, segment_index=0): """tell the device to arm any triggers and start capturing in block mode now. From e25f5c3550642f7add669ead061ec7f8d67f5ee6 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 18 Jun 2025 18:04:15 +0200 Subject: [PATCH 082/129] Specify the arguments --- picosdk/library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 49121bd..7bce975 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -709,8 +709,8 @@ def set_digital_channel_trigger(self, device, channel_number=15, direction="DIRE if digital_properties and digital_channels and directions: digital_channel = self.name.upper() + '_DIGITAL_CHANNEL_' + str(channel_number) direction = self.name.upper() + '_DIGITAL_' + direction - digital_properties(digital_channels[digital_channel], - directions[direction]) + digital_properties(channel=digital_channels[digital_channel], + direction=directions[direction]) args = (device.handle, digital_properties, 1) converted_args = self._convert_args(self._set_trigger_digital_port_properties, args) status = self._set_trigger_digital_port_properties(*converted_args) From 7d17933a6924872c70a48f1e484b699917477590 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 18 Jun 2025 18:45:57 +0200 Subject: [PATCH 083/129] Set analog offset to 0 --- picosdk/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/device.py b/picosdk/device.py index f0295fa..552defc 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -200,7 +200,7 @@ def reset(self): raise ConnectionError("Device reset failed: handle is invalid after re-open attempt.") @requires_open() - def set_channel(self, channel_name, enabled, coupling='DC', range_peak=float('inf'), analog_offset=None): + def set_channel(self, channel_name, enabled, coupling='DC', range_peak=float('inf'), analog_offset=0): """Configures a single analog channel. channel_name: The channel name as a string (e.g., 'A'). From 494671ed21ef4cbe91cab115e0992eb17faa5bb8 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 18 Jun 2025 18:46:37 +0200 Subject: [PATCH 084/129] Use the instance for the digital properties correctly --- picosdk/library.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 7bce975..98929fe 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -709,9 +709,9 @@ def set_digital_channel_trigger(self, device, channel_number=15, direction="DIRE if digital_properties and digital_channels and directions: digital_channel = self.name.upper() + '_DIGITAL_CHANNEL_' + str(channel_number) direction = self.name.upper() + '_DIGITAL_' + direction - digital_properties(channel=digital_channels[digital_channel], - direction=directions[direction]) - args = (device.handle, digital_properties, 1) + properties = digital_properties(channel=digital_channels[digital_channel], + direction=directions[direction]) + args = (device.handle, properties, 1) converted_args = self._convert_args(self._set_trigger_digital_port_properties, args) status = self._set_trigger_digital_port_properties(*converted_args) if status != self.PICO_STATUS['PICO_OK']: From c02a674f2117da8ef33fcc6989b410fafc69bde3 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 18 Jun 2025 19:56:50 +0200 Subject: [PATCH 085/129] Delete unused function --- picosdk/library.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 98929fe..9f99399 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -201,15 +201,6 @@ def __getitem__(self, key: str): # Handle direct port access (0-3) or analog channels return super().__getitem__(key) - def set_port_data(self, port_number: int, data: numpy.ndarray): - """Set digital port data. - - Args: - port_number: Digital port number (0-3) - data: Numpy array containing port data - """ - self[port_number] = data - class Library(object): def __init__(self, name): From b17e70be7a12ddd15408f1baa7be53ffbe76701b Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 18 Jun 2025 20:13:33 +0200 Subject: [PATCH 086/129] Add docstrings to the properties --- picosdk/device.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/picosdk/device.py b/picosdk/device.py index 552defc..37606cd 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -83,47 +83,57 @@ def __init__(self, driver, handle): @property def driver(self): + """picosdk.library.Library: The driver object""" return self._driver @property def handle(self): + """int: The device handle""" return self._handle @property def is_open(self): + """bool: True if the device is open, False otherwise.""" return self.handle is not None and self.handle > 0 @property def max_adc(self): + """int: The maximum ADC value for this device.""" return self._max_adc @property def buffers(self): + """dict: A dictionary of buffers for each enabled channel or port.""" return self._buffers @property def max_samples(self): + """int: The number of samples for capture.""" return self._max_samples @property def channel_ranges(self): + """dict: A dictionary of channel ranges for each enabled channel.""" return self._channel_ranges @property def channel_offsets(self): + """dict: A dictionary of channel offsets for each enabled channel.""" return self._channel_offsets @property def enabled_sources(self): + """set: A set of enabled sources (channels and/or digital ports).""" return self._enabled_sources @property def time_interval(self): - """The time interval in seconds""" + """int/float: The time interval in seconds""" return self._time_interval @property def probe_attenuations(self): + """dict: A dictionary of probe attenuations for each enabled channel.""" return self._probe_attenuations @probe_attenuations.setter @@ -133,6 +143,7 @@ def probe_attenuations(self, value): @property @requires_open() def info(self): + """UnitInfo: The info of the device""" return self.driver.get_unit_info(self) @requires_open("The device either did not initialise correctly or has already been closed.") From 63a5173204005d4b218312814b09092355d93306 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 19 Jun 2025 11:04:02 +0200 Subject: [PATCH 087/129] Catch the correct error --- picosdk/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/device.py b/picosdk/device.py index 37606cd..845f8a2 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -262,7 +262,7 @@ def set_digital_port(self, port_number, enabled, voltage_level=1.8): else: try: self._enabled_sources.remove(port_number) - except ValueError: + except KeyError: pass @requires_open() From 5362d480d3e459a8f1d4f768e59244d10315d277 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 19 Jun 2025 11:39:40 +0200 Subject: [PATCH 088/129] Delete unused part --- picosdk/device.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index 845f8a2..ad685d6 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -158,11 +158,6 @@ def close(self): self._enabled_sources.clear() self._time_interval_ns = None self._probe_attenuations = DEFAULT_PROBE_ATTENUATION.copy() - # Reset any cached timebase information if those attributes exist from prior modifications - if hasattr(self, '_cached_timebase_options'): - self._cached_timebase_options = None - if hasattr(self, '_cached_timebase_info'): - self._cached_timebase_info = None def __enter__(self): return self From db6b4c9d3bbfcce7b13223a9ece130a62009d671 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 19 Jun 2025 14:49:11 +0200 Subject: [PATCH 089/129] Update docstrings of methods in Device --- picosdk/device.py | 112 +++++++++++++++++++++++++++------------------- 1 file changed, 65 insertions(+), 47 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index ad685d6..290ff6e 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -209,12 +209,12 @@ def reset(self): def set_channel(self, channel_name, enabled, coupling='DC', range_peak=float('inf'), analog_offset=0): """Configures a single analog channel. - channel_name: The channel name as a string (e.g., 'A'). - enabled: bool, True to enable the channel, False to disable. - coupling (optional): 'AC' or 'DC'. Defaults to 'DC'. - range_peak (optional): Desired +/- peak voltage. The driver selects the best range. + channel_name (str): The channel name as a string (e.g., 'A'). + enabled (bool): True to enable the channel, False to disable. + coupling (str): 'AC' or 'DC'. Defaults to 'DC'. + range_peak (int/float): Desired +/- peak voltage. The driver selects the best range. Required if enabling the channel. - analog_offset (optional): The analog offset for the channel in Volts. + analog_offset (int/float): The analog offset for the channel in Volts. """ if not enabled: self.driver.set_channel(self, @@ -246,7 +246,7 @@ def set_digital_port(self, port_number, enabled, voltage_level=1.8): Args: port_number (int): identifies the port for digital data. (e.g. 0 for digital channels 0-7) enabled (bool): whether or not to enable the channel (boolean) - voltage_level (float): the voltage at which the state transitions between 0 and 1. Range: –5.0 to 5.0 (V). + voltage_level (int/float): the voltage at which the state transitions between 0 and 1. Range: –5.0 to 5.0 (V). """ info = self.info if not info.variant.decode('utf-8').endswith("MSO"): @@ -262,10 +262,10 @@ def set_digital_port(self, port_number, enabled, voltage_level=1.8): @requires_open() def set_channels(self, *channel_configs): - """ set_channels(self, *channel_configs) - An alternative to calling set_channel for each one, you can call this method with some channel configs. + """An alternative to calling set_channel for each channel, you can call this method with + one ore more ChannelConfig. This method will also disable any missing channels from the passed configs, and disable ALL channels if the - collection is empty. """ + collection is empty.""" # Add channels which are missing as "disabled". if len(channel_configs) < len(self.driver.PICO_CHANNEL): channel_configs = list(channel_configs) @@ -339,6 +339,12 @@ def find_timebase(self, timebase_options): def get_timebase(self, timebase_id, no_of_samples, oversample=1, segment_index=0): """Query the device about what time precision modes it can handle. + Args: + timebase_id (int): The timebase id. + no_of_samples (int): The number of samples to collect at this timebase. + oversample (int): The amount of oversample required. Defaults to 1. + segment_index (int): The memory segment index to use. Defaults to 0. + Returns: namedtuple: - timebase_id: The id corresponding to the timebase used @@ -373,6 +379,10 @@ def maximum_value(self): @requires_open() def set_null_trigger(self): + """Set a null trigger on the device. + Trigger is not enabled, so the device will not wait for a trigger + before capturing data. + """ self.driver.set_null_trigger() @requires_open() @@ -402,13 +412,24 @@ def set_simple_trigger(self, channel, enable=True, threshold_mv=500, direction=" @requires_open() def run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample=1, segment_index=0): - """This function starts collecting data in block mode.""" + """This function starts collecting data in block mode. + + Args: + pre_trigger_samples (int): The number of samples to collect before the trigger event. + post_trigger_samples (int): The number of samples to collect after the trigger event. + timebase_id (int): The timebase id to use for the capture. + oversample (int): The amount of oversample required. Defaults to 1. + segment_index (int): The memory segment index to use. Defaults to 0. + + Returns: + float: The approximate time (in seconds) which the device will take to capture with these settings + """ self._max_samples = pre_trigger_samples + post_trigger_samples - self.driver.run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample, segment_index) + return self.driver.run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample, segment_index) @requires_open() def is_ready(self): - """poll this function to find out when block mode is ready or has triggered. + """Poll this function to find out when block mode is ready or has triggered. returns: True if data is ready, False otherwise.""" return self.driver.is_ready(self) @@ -426,10 +447,9 @@ def set_data_buffer(self, channel_or_port, segment_index=0, mode='NONE'): """Set the data buffer for a specific channel. Args: - channel_or_port: Channel (e.g. 'A', 'B') or digital port (e.g. 0, 1) to set data for - buffer_length: The size of the buffer array (equal to no_of_samples) - segment_index: The number of the memory segment to be used (default is 0) - mode: The ratio mode to be used (default is 'NONE') + channel_or_port (str/int): Channel (e.g. 'A', 'B') or digital port (e.g. 0, 1) to set data for + segment_index (int): The number of the memory segment to be used (default is 0) + mode (str): The ratio mode to be used (default is 'NONE') """ self._buffers[channel_or_port] = self.driver.set_data_buffer(self, channel_or_port, self.max_samples, segment_index, mode) @@ -439,18 +459,15 @@ def set_all_data_buffers(self, segment_index=0, mode='NONE'): """Set the data buffer for each enabled channels and ports Args: - channel_or_port: Channel (e.g. 'A', 'B') or digital port (e.g. 0, 1) to set data for - buffer_length: The size of the buffer array (equal to no_of_samples) - segment_index: The number of the memory segment to be used (default is 0) - mode: The ratio mode to be used (default is 'NONE') + segment_index (int): The number of the memory segment to be used (default is 0) + mode (str): The ratio mode to be used (default is 'NONE') """ for channel_or_port in self.enabled_sources: self.set_data_buffer(channel_or_port, segment_index, mode) @requires_open() - def get_values(self,start_index=0, downsample_ratio=0, - downsample_ratio_mode="NONE", segment_index=0, output_dir=".", filename="data", save_to_file=False, - ): + def get_values(self,start_index=0, downsample_ratio=0, downsample_ratio_mode="NONE", segment_index=0, + output_dir=".", filename="data", save_to_file=False): """Get stored data values from the scope and store it in a clean SingletonScopeDataDict object. This function is used after data collection has stopped. It gets the stored data from the scope, with or @@ -458,6 +475,8 @@ def get_values(self,start_index=0, downsample_ratio=0, The returned captured data is converted to mV. + Note: don't forget to change the probe attenuations of the used channels if they differ from 10 (default) + Args: start_index (int): A zero-based index that indicates the start point for data collection. It is measured in sample intervals from the start of the buffer. @@ -467,7 +486,6 @@ def get_values(self,start_index=0, downsample_ratio=0, output_dir (str): The output directory where the json file will be saved. filename (str): The name of the json file where the data will be stored save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise - probe_attenuation (dict): The attenuation factor of the probe used per the channel (1 or 10). Returns: Tuple of (captured data including time, overflow warnings) @@ -509,15 +527,15 @@ def set_trigger_channel_properties(self, threshold_upper, threshold_upper_hyster """Set the trigger channel properties for the device. Args: - threshold_upper: Upper threshold in ADC counts - threshold_upper_hysteresis: Hysteresis for upper threshold in ADC counts - threshold_lower: Lower threshold in ADC counts - threshold_lower_hysteresis: Hysteresis for lower threshold in ADC counts - channel: Channel to set properties for (e.g. 'A', 'B', 'C', 'D') - threshold_mode: Threshold mode (e.g. "LEVEL", "WINDOW") - aux_output_enable: Enable auxiliary output (boolean) (Not used in eg. ps2000a, ps3000a, ps4000a) - auto_trigger_milliseconds: The number of milliseconds for which the scope device will wait for a trigger - before timing out. If set to zero, the scope device will wait indefinitely for a trigger + threshold_upper (int): Upper threshold in ADC counts + threshold_upper_hysteresis (int): Hysteresis for upper threshold in ADC counts + threshold_lower (int): Lower threshold in ADC counts + threshold_lower_hysteresis (int): Hysteresis for lower threshold in ADC counts + channel (str): Channel to set properties for (e.g. 'A', 'B', 'C', 'D') + threshold_mode (str): Threshold mode (e.g. "LEVEL", "WINDOW") + aux_output_enable (bool): Enable auxiliary output (boolean) (Not used in eg. ps2000a, ps3000a, ps4000a) + auto_trigger_milliseconds (int): The number of milliseconds for which the scope device will wait for a + trigger before timing out. If set to zero, the scope device will wait indefinitely for a trigger """ self.driver.set_trigger_channel_properties(self, threshold_upper, threshold_upper_hysteresis, threshold_lower, threshold_lower_hysteresis, channel, threshold_mode, @@ -633,20 +651,20 @@ def set_sig_gen_built_in(self, offset_voltage=0, pk_to_pk=2000000, wave_type="SI """Set up the signal generator to output a built-in waveform. Args: - offset_voltage: Offset voltage in microvolts (default 0) - pk_to_pk: Peak-to-peak voltage in microvolts (default 2000000) - wave_type: Type of waveform (e.g. "SINE", "SQUARE", "TRIANGLE") - start_frequency: Start frequency in Hz (default 1000.0) - stop_frequency: Stop frequency in Hz (default 1000.0) - increment: Frequency increment in Hz (default 0.0) - dwell_time: Time at each frequency in seconds (default 1.0) - sweep_type: Sweep type (e.g. "UP", "DOWN", "UPDOWN") - operation: Configures the white noise/PRBS (e.g. "ES_OFF", "WHITENOISE", "PRBS") - shots: Number of shots per trigger (default 1) - sweeps: Number of sweeps (default 1) - trigger_type: Type of trigger (e.g. "RISING", "FALLING") - trigger_source: Source of trigger (e.g. "NONE", "SCOPE_TRIG") - ext_in_threshold: External trigger threshold in ADC counts + offset_voltage (int/float): Offset voltage in microvolts (default 0) + pk_to_pk (int): Peak-to-peak voltage in microvolts (default 2000000) + wave_type (str): Type of waveform (e.g. "SINE", "SQUARE", "TRIANGLE") + start_frequency (int): Start frequency in Hz (default 1000.0) + stop_frequency (int): Stop frequency in Hz (default 1000.0) + increment (int): Frequency increment in Hz (default 0.0) + dwell_time (int/float): Time at each frequency in seconds (default 1.0) + sweep_type (str): Sweep type (e.g. "UP", "DOWN", "UPDOWN") + operation (str): Configures the white noise/PRBS (e.g. "ES_OFF", "WHITENOISE", "PRBS") + shots (int): Number of shots per trigger (default 1) + sweeps (int): Number of sweeps (default 1) + trigger_type (str): Type of trigger (e.g. "RISING", "FALLING") + trigger_source (str): Source of trigger (e.g. "NONE", "SCOPE_TRIG") + ext_in_threshold (int): External trigger threshold in ADC counts """ self.driver.set_sig_gen_built_in(self, offset_voltage, pk_to_pk, wave_type, start_frequency, stop_frequency, increment, dwell_time, sweep_type, operation, shots, sweeps, trigger_type, From 98a112e656c379b917e8ec28282e12bffb2e2b0b Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 19 Jun 2025 15:18:40 +0200 Subject: [PATCH 090/129] Update docstrings methods in Library class --- picosdk/library.py | 163 +++++++++++++++++++++++++++++++-------------- 1 file changed, 113 insertions(+), 50 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 9f99399..9aa3a9b 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -433,14 +433,20 @@ def _python_get_unit_info_wrapper(self, handle, keys): @requires_device("set_channel requires a picosdk.device.Device instance, passed to the correct owning driver.") def set_channel(self, device, channel_name='A', enabled=True, coupling='DC', range_peak=float('inf'), analog_offset=None): - """optional arguments: - channel_name: a single channel (e.g. 'A') - enabled: whether to enable the channel (boolean) - coupling: string of the relevant enum member for your driver less the driver name prefix. e.g. 'DC' or 'AC'. - range_peak: float which is the largest value you expect in the input signal. We will throw an exception if no - range on the device is large enough for that value. (in Voltage) - analog_offset: the meaning of 0 for this channel. - return value: Max voltage of new range. Raises an exception in error cases.""" + """Configures a single analog channel. + + Args: + device (picosdk.device.Device): The device instance + channel_name (str): The channel name as a string (e.g., 'A'). + enabled (bool): True to enable the channel, False to disable. + coupling (str): 'AC' or 'DC'. Defaults to 'DC'. + range_peak (int/float): Desired +/- peak voltage. The driver selects the best range. + Required if enabling the channel. + analog_offset (int/float): The analog offset for the channel in Volts. + + Returns: + The range of the channel in Volts if enabled, None if disabled. + """ excluded = () reliably_resolved = False @@ -473,6 +479,7 @@ def set_digital_port(self, device, port_number=0, enabled=True, voltage_level=1. """Set the digital port Args: + device (picosdk.device.Device): The device instance port_number (int): identifies the port for digital data. (e.g. 0 for digital channels 0-7) enabled (bool): whether or not to enable the channel (boolean) voltage_level (float): the voltage at which the state transitions between 0 and 1. Range: –5.0 to 5.0 (V). @@ -550,6 +557,19 @@ def _python_set_channel(self, handle, channel_id, enabled, coupling_id, range_id @requires_device("memory_segments requires a picosdk.device.Device instance, passed to the correct owning driver.") def memory_segments(self, device, number_segments): + """The number of segments defaults to 1, meaning that each capture fills the scope's available memory. + This function allows you to divide the memory into a number of segments so that the scope can store several + waveforms sequentially. + + Args: + device (picosdk.device.Device): The device instance + number_segments (int): The number of segments to divide the memory into. + + Returns: + int: The number of samples available in each segment. This is the total number over all channels, + so if more than one channel is in use then the number of samples available to each + channel is max_samples divided by the number of channels. + """ if not hasattr(self, '_memory_segments'): raise DeviceCannotSegmentMemoryError() max_samples = c_int32(0) @@ -561,8 +581,24 @@ def memory_segments(self, device, number_segments): @requires_device("get_timebase requires a picosdk.device.Device instance, passed to the correct owning driver.") def get_timebase(self, device, timebase_id, no_of_samples, oversample=1, segment_index=0): - """query the device about what time precision modes it can handle. - note: the driver returns the timebase in nanoseconds, this function converts that into SI units (seconds)""" + """Query the device about what time precision modes it can handle. + + Args: + device (picosdk.device.Device): The device instance + timebase_id (int): The timebase id. + no_of_samples (int): The number of samples to collect at this timebase. + oversample (int): The amount of oversample required. Defaults to 1. + segment_index (int): The memory segment index to use. Defaults to 0. + + Returns: + namedtuple: + - timebase_id: The id corresponding to the timebase used + - time_interval: The time interval between readings at the selected timebase. + - time_units: The unit of time (not supported in e.g. 3000a) + - max_samples: The maximum number of samples available. The number may vary depending on the number of + channels enabled and the timebase chosen. + - segment_id: The index of the memory segment to use + """ nanoseconds_result = self._python_get_timebase(device.handle, timebase_id, no_of_samples, @@ -614,6 +650,10 @@ def _python_get_timebase(self, handle, timebase_id, no_of_samples, oversample, s @requires_device() def set_null_trigger(self, device, channel="A"): + """Set a null trigger on the device. + Trigger is not enabled, so the device will not wait for a trigger + before capturing data. + """ auto_trigger_after_millis = 1 if hasattr(self, '_set_trigger') and len(self._set_trigger.argtypes) == 6: PS2000_NONE = 5 @@ -689,6 +729,7 @@ def set_digital_channel_trigger(self, device, channel_number=15, direction="DIRE """Set a simple trigger on the digital channels. Args: + device (picosdk.device.Device): The device instance channel_number (int): The number of the digital channel on which to trigger.(e.g. 0 for D0, 1 for D1,...) direction (str): The direction in which the signal must move to cause a trigger. """ @@ -723,6 +764,7 @@ def set_trigger_delay(self, device, delay): At a timebase of 500 MS/s, or 2 ns per sample, the total delay would then be 100 x 2 ns = 200 ns. Args: + device (picosdk.device.Device): The device instance delay (int): The time between the trigger occurring and the first sample. """ if hasattr(self, '_set_trigger_delay') and len(self._set_trigger_delay.argtypes) == 2: @@ -737,8 +779,19 @@ def set_trigger_delay(self, device, delay): @requires_device() def run_block(self, device, pre_trigger_samples, post_trigger_samples, timebase_id, oversample=1, segment_index=0): - """tell the device to arm any triggers and start capturing in block mode now. - returns: the approximate time (in seconds) which the device will take to capture with these settings.""" + """This function starts collecting data in block mode. + + Args: + device (picosdk.device.Device): The device instance + pre_trigger_samples (int): The number of samples to collect before the trigger event. + post_trigger_samples (int): The number of samples to collect after the trigger event. + timebase_id (int): The timebase id to use for the capture. + oversample (int): The amount of oversample required. Defaults to 1. + segment_index (int): The memory segment index to use. Defaults to 0. + + Returns: + float: The approximate time (in seconds) which the device will take to capture with these settings + """ return self._python_run_block(device.handle, pre_trigger_samples, post_trigger_samples, @@ -799,6 +852,7 @@ def stop_block_capture(self, device, timeout_minutes=5): """Poll the driver to see if it has finished collecting the requested samples. Args: + device (picosdk.device.Device): The device instance timeout_minutes (int/float): The timeout in minutes. If the time exceeds the timeout, the poll stops. """ if timeout_minutes < 0: @@ -810,7 +864,14 @@ def stop_block_capture(self, device, timeout_minutes=5): @requires_device() def maximum_value(self, device): - """Get the maximum ADC value for this device.""" + """Get the maximum ADC value for this device. + + Args: + device (picosdk.device.Device): The device instance + + Returns: + int: The maximum ADC value for this device. + """ if not hasattr(self, '_maximum_value'): return (2**15)-1 max_adc = c_int16(0) @@ -824,11 +885,11 @@ def set_data_buffer(self, device, channel_or_port, buffer_length, segment_index= """Set the data buffer for a specific channel. Args: - device: Device instance - channel_or_port: Channel (e.g. 'A', 'B') or digital port (e.g. 0, 1) to set data for - buffer_length: The size of the buffer array (equal to no_of_samples) - segment_index: The number of the memory segment to be used (default is 0) - mode: The ratio mode to be used (default is 'NONE') + device (picosdk.device.Device): The device instance + channel_or_port (int/str): Channel (e.g. 'A', 'B') or digital port (e.g. 0, 1) to set data for + buffer_length (int): The size of the buffer array (equal to no_of_samples) + segment_index (int): The number of the memory segment to be used (default is 0) + mode (str): The ratio mode to be used (default is 'NONE') Raises: ArgumentOutOfRangeError: If parameters are invalid for device @@ -981,22 +1042,20 @@ def set_trigger_channel_properties(self, device, threshold_upper, threshold_uppe """Set the trigger channel properties for the device. Args: - device: Device instance - threshold_upper: Upper threshold in ADC counts - threshold_upper_hysteresis: Hysteresis for upper threshold in ADC counts - threshold_lower: Lower threshold in ADC counts - threshold_lower_hysteresis: Hysteresis for lower threshold in ADC counts - channel: Channel to set properties for (e.g. 'A', 'B', 'C', 'D') - threshold_mode: Threshold mode (e.g. "LEVEL", "WINDOW") - aux_output_enable: Enable auxiliary output (boolean) (Not used in eg. ps2000a, ps3000a, ps4000a) - auto_trigger_milliseconds: The number of milliseconds for which the scope device will wait for a trigger - before timing out. If set to zero, the scope device will wait indefinitely for a trigger - - Returns: - None + device (picosdk.device.Device): Device instance + threshold_upper (int): Upper threshold in ADC counts + threshold_upper_hysteresis (int): Hysteresis for upper threshold in ADC counts + threshold_lower (int): Lower threshold in ADC counts + threshold_lower_hysteresis (int): Hysteresis for lower threshold in ADC counts + channel (str): Channel to set properties for (e.g. 'A', 'B', 'C', 'D') + threshold_mode (str): Threshold mode (e.g. "LEVEL", "WINDOW") + aux_output_enable (bool): Enable auxiliary output (boolean) (Not used in eg. ps2000a, ps3000a, ps4000a) + auto_trigger_milliseconds (int): The number of milliseconds for which the scope device will wait for a + trigger before timing out. If set to zero, the scope device will wait indefinitely for a trigger Raises: - ArgumentOutOfRangeError: If parameters are invalid for device + NotImplementedError: This device does not support setting trigger channel properties. + PicoError: If the function fails to set the properties. """ if hasattr(self, '_set_trigger_channel_properties'): args = (device.handle, threshold_upper, threshold_upper_hysteresis, @@ -1013,7 +1072,14 @@ def set_trigger_channel_properties(self, device, threshold_upper, threshold_uppe @requires_device() def stop(self, device): - """Stop data capture.""" + """Stop data capture. + + Args: + device (picosdk.device.Device): Device instance + + Raises: + InvalidCaptureParameters: If the stop operation fails or parameters are invalid. + """ args = (device.handle,) converted_args = self._convert_args(self._stop, args) @@ -1035,23 +1101,20 @@ def set_sig_gen_built_in(self, device, offset_voltage=0, pk_to_pk=2000000, wave_ Args: device: Device instance - offset_voltage: Offset voltage in microvolts (default 0) - pk_to_pk: Peak-to-peak voltage in microvolts (default 2000000) - wave_type: Type of waveform (e.g. "SINE", "SQUARE", "TRIANGLE") - start_frequency: Start frequency in Hz (default 1000.0) - stop_frequency: Stop frequency in Hz (default 1000.0) - increment: Frequency increment in Hz (default 0.0) - dwell_time: Time at each frequency in seconds (default 1.0) - sweep_type: Sweep type (e.g. "UP", "DOWN", "UPDOWN") - operation: Configures the white noise/PRBS (e.g. "ES_OFF", "WHITENOISE", "PRBS") - shots: Number of shots per trigger (default 1) - sweeps: Number of sweeps (default 1) - trigger_type: Type of trigger (e.g. "RISING", "FALLING") - trigger_source: Source of trigger (e.g. "NONE", "SCOPE_TRIG") - ext_in_threshold: External trigger threshold in ADC counts - - Returns: - None + offset_voltage (int/float): Offset voltage in microvolts (default 0) + pk_to_pk (int): Peak-to-peak voltage in microvolts (default 2000000) + wave_type (str): Type of waveform (e.g. "SINE", "SQUARE", "TRIANGLE") + start_frequency (int): Start frequency in Hz (default 1000.0) + stop_frequency (int): Stop frequency in Hz (default 1000.0) + increment (int): Frequency increment in Hz (default 0.0) + dwell_time (int/float): Time at each frequency in seconds (default 1.0) + sweep_type (str): Sweep type (e.g. "UP", "DOWN", "UPDOWN") + operation (str): Configures the white noise/PRBS (e.g. "ES_OFF", "WHITENOISE", "PRBS") + shots (int): Number of shots per trigger (default 1) + sweeps (int): Number of sweeps (default 1) + trigger_type (str): Type of trigger (e.g. "RISING", "FALLING") + trigger_source (str): Source of trigger (e.g. "NONE", "SCOPE_TRIG") + ext_in_threshold (int): External trigger threshold in ADC counts Raises: ArgumentOutOfRangeError: If parameters are invalid for device From 0e0fc7eb6f085ac800510785dce17e256e3179fc Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 19 Jun 2025 15:18:52 +0200 Subject: [PATCH 091/129] Update docstring --- picosdk/device.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index 290ff6e..e3add1b 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -209,12 +209,16 @@ def reset(self): def set_channel(self, channel_name, enabled, coupling='DC', range_peak=float('inf'), analog_offset=0): """Configures a single analog channel. - channel_name (str): The channel name as a string (e.g., 'A'). - enabled (bool): True to enable the channel, False to disable. - coupling (str): 'AC' or 'DC'. Defaults to 'DC'. - range_peak (int/float): Desired +/- peak voltage. The driver selects the best range. - Required if enabling the channel. - analog_offset (int/float): The analog offset for the channel in Volts. + Args: + channel_name (str): The channel name as a string (e.g., 'A'). + enabled (bool): True to enable the channel, False to disable. + coupling (str): 'AC' or 'DC'. Defaults to 'DC'. + range_peak (int/float): Desired +/- peak voltage. The driver selects the best range. + Required if enabling the channel. + analog_offset (int/float): The analog offset for the channel in Volts. + + Returns: + The range of the channel in Volts if enabled, None if disabled. """ if not enabled: self.driver.set_channel(self, From bf146ca7438aae9189462f98e69dd836b289210f Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 19 Jun 2025 15:19:10 +0200 Subject: [PATCH 092/129] Add arguments to docstring --- picosdk/device.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index e3add1b..e356e82 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -368,6 +368,9 @@ def memory_segments(self, number_segments): This function allows you to divide the memory into a number of segments so that the scope can store several waveforms sequentially. + Args: + number_segments (int): The number of segments to divide the memory into. + Returns: int: The number of samples available in each segment. This is the total number over all channels, so if more than one channel is in use then the number of samples available to each From d11ea694d9fc07cc341ff210117b693af96bb3c4 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 11 Aug 2025 15:52:33 +0200 Subject: [PATCH 093/129] Check if there is a digit after D for digital channels to fix usage of analog channel D --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index 9aa3a9b..ce2b9f7 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -179,7 +179,7 @@ def __getitem__(self, key: str): ValueError: If digital channel number is invalid """ # Handle digital channels (D0-D15) - if isinstance(key, str) and key.upper().startswith('D'): + if isinstance(key, str) and key.upper().startswith('D') and key[1:].isdigit(): try: digital_number = int(key[1:]) if not 0 <= digital_number <= 15: From 2d4088b656faea313a27818fc6c99a0321820aae Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 21 Aug 2025 09:11:30 +0200 Subject: [PATCH 094/129] Update SingletonScopeDataDict in order to obtain the correct numpy array; Add some comments to clarify --- picosdk/library.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index ce2b9f7..8e31eb6 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -10,6 +10,7 @@ from __future__ import print_function import json +import re import sys from ctypes import c_int16, c_int32, c_uint32, c_float, c_void_p, create_string_buffer, byref from ctypes.util import find_library @@ -178,22 +179,28 @@ def __getitem__(self, key: str): KeyError: If channel doesn't exist ValueError: If digital channel number is invalid """ - # Handle digital channels (D0-D15) - if isinstance(key, str) and key.upper().startswith('D') and key[1:].isdigit(): + if isinstance(key, str): + match = re.match(r"D(?P\d+)", key, re.IGNORECASE) + else: + match = None + + if match: try: - digital_number = int(key[1:]) + digital_number = int(match.group('channel_num')) if not 0 <= digital_number <= 15: raise ValueError(f"Digital channel number must be 0-15, got {digital_number}") - # Calculate which port and bit + # Calculate which port and which row (bit) in the port's data array port_number = digital_number // 8 # Port 0 = D0-D7, Port 1 = D8-D15 - bit_number = 7 - (digital_number % 8) # Reverse bit order within port - # Get port data + # Reverse bit order within port: Assuming D7 is the first row (index 0) and D0 is the last row (index 7) + row_index = 7 - (digital_number % 8) + + # Get the data for the entire port port_data = super().__getitem__(port_number) - # Extract individual channel data - return (port_data >> bit_number) & 0x1 + # Select the correct row from the numpy array. + return port_data[row_index] except (IndexError, ValueError) as e: raise ValueError(f"Invalid digital channel {key}: {str(e)}") From f2b7d9419e133a2b13b7707fc4fd99ad90e9303e Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 7 Oct 2025 15:48:39 +0200 Subject: [PATCH 095/129] Catch KeyError as well --- picosdk/library.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 8e31eb6..8010140 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -187,8 +187,6 @@ def __getitem__(self, key: str): if match: try: digital_number = int(match.group('channel_num')) - if not 0 <= digital_number <= 15: - raise ValueError(f"Digital channel number must be 0-15, got {digital_number}") # Calculate which port and which row (bit) in the port's data array port_number = digital_number // 8 # Port 0 = D0-D7, Port 1 = D8-D15 @@ -202,7 +200,7 @@ def __getitem__(self, key: str): # Select the correct row from the numpy array. return port_data[row_index] - except (IndexError, ValueError) as e: + except (IndexError, ValueError, KeyError) as e: raise ValueError(f"Invalid digital channel {key}: {str(e)}") # Handle direct port access (0-3) or analog channels From c905e2e2479314702bd8feb693de919e4e02717d Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 8 Oct 2025 16:51:24 +0200 Subject: [PATCH 096/129] Set all properties to None when device is closed --- picosdk/device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/picosdk/device.py b/picosdk/device.py index e356e82..c1f16fa 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -149,6 +149,7 @@ def info(self): @requires_open("The device either did not initialise correctly or has already been closed.") def close(self): self.driver.close_unit(self) + self._driver = None self._handle = None self._max_adc = None self._buffers.clear() @@ -156,7 +157,7 @@ def close(self): self._channel_ranges.clear() self._channel_offsets.clear() self._enabled_sources.clear() - self._time_interval_ns = None + self._time_interval = None self._probe_attenuations = DEFAULT_PROBE_ATTENUATION.copy() def __enter__(self): From 9fed8c7dc399856807b4c4c2ce4cea3a12c2ce41 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 8 Oct 2025 17:26:35 +0200 Subject: [PATCH 097/129] Fix PytestCollectionWarning --- test/test_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_helpers.py b/test/test_helpers.py index 02184d1..9d546ff 100644 --- a/test/test_helpers.py +++ b/test/test_helpers.py @@ -39,11 +39,11 @@ class TestFailAndError(Exception): - pass + __test__ = False class TestError(Exception): - pass + __test__ = False class DriverTest(_unittest.TestCase): From 803742c7af336aa72deef30e4f50b3a5eaa79f87 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 8 Oct 2025 17:33:30 +0200 Subject: [PATCH 098/129] Use time_interval in ns as default --- picosdk/device.py | 25 ++++++++++++++----------- picosdk/library.py | 22 +++++++++++----------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index c1f16fa..4388c34 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -78,7 +78,7 @@ def __init__(self, driver, handle): self._channel_ranges = {} self._channel_offsets = {} self._enabled_sources = set() - self._time_interval = None + self._time_interval_ns = None self._probe_attenuations = DEFAULT_PROBE_ATTENUATION @property @@ -127,9 +127,9 @@ def enabled_sources(self): return self._enabled_sources @property - def time_interval(self): + def time_interval_ns(self): """int/float: The time interval in seconds""" - return self._time_interval + return self._time_interval_ns @property def probe_attenuations(self): @@ -157,7 +157,7 @@ def close(self): self._channel_ranges.clear() self._channel_offsets.clear() self._enabled_sources.clear() - self._time_interval = None + self._time_interval_ns = None self._probe_attenuations = DEFAULT_PROBE_ATTENUATION.copy() def __enter__(self): @@ -303,14 +303,15 @@ def _timebase_options_are_impossible(self, options): @staticmethod def _validate_timebase(timebase_options, timebase_info): """validate whether a timebase result matches the options requested.""" + time_interval_s = timebase_info.time_interval_ns / 1e9 if timebase_options.max_time_interval is not None: - if timebase_info.time_interval > timebase_options.max_time_interval: + if time_interval_s > timebase_options.max_time_interval: return False if timebase_options.no_of_samples is not None: if timebase_options.no_of_samples > timebase_info.max_samples: return False if timebase_options.min_collection_time is not None: - if timebase_options.min_collection_time > timebase_info.max_samples * timebase_info.time_interval: + if timebase_options.min_collection_time > timebase_info.max_samples * time_interval_s: return False return True @@ -353,14 +354,14 @@ def get_timebase(self, timebase_id, no_of_samples, oversample=1, segment_index=0 Returns: namedtuple: - timebase_id: The id corresponding to the timebase used - - time_interval: The time interval between readings at the selected timebase. + - time_interval_ns: The time interval between readings at the selected timebase. - time_units: The unit of time (not supported in e.g. 3000a) - max_samples: The maximum number of samples available. The number may vary depending on the number of channels enabled and the timebase chosen. - segment_id: The index of the memory segment to use """ timebase_info = self.driver.get_timebase(self, timebase_id, no_of_samples, oversample, segment_index) - self._time_interval = timebase_info.time_interval + self._time_interval_ns = timebase_info.time_interval_ns return timebase_info @requires_open() @@ -498,7 +499,8 @@ def get_values(self,start_index=0, downsample_ratio=0, downsample_ratio_mode="NO Returns: Tuple of (captured data including time, overflow warnings) """ - return self.driver.get_values(self, self.buffers, self.max_samples, self.time_interval, self.channel_ranges, + time_interval_sec = self.time_interval_ns / 1e9 if self.time_interval_ns else None + return self.driver.get_values(self, self.buffers, self.max_samples, time_interval_sec, self.channel_ranges, start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, filename, save_to_file, self.probe_attenuations) @@ -606,12 +608,13 @@ def capture_block(self, timebase_options, channel_configs=()): # get_timebase timebase_info = self.find_timebase(timebase_options) + time_interval_s = timebase_info.time_interval_ns / 1e9 post_trigger_samples = timebase_options.no_of_samples pre_trigger_samples = 0 if post_trigger_samples is None: - post_trigger_samples = int(math.ceil(timebase_options.min_collection_time / timebase_info.time_interval)) + post_trigger_samples = int(math.ceil(timebase_options.min_collection_time / time_interval_s)) self.driver.set_null_trigger(self) @@ -636,7 +639,7 @@ def capture_block(self, timebase_options, channel_configs=()): self.driver.stop(self) times = numpy.linspace(0., - post_trigger_samples * timebase_info.time_interval, + post_trigger_samples * time_interval_s, post_trigger_samples, dtype=numpy.dtype('float32')) diff --git a/picosdk/library.py b/picosdk/library.py index 8010140..0bfac28 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -44,7 +44,7 @@ """TimebaseInfo: A type for holding the particulars of a timebase configuration. """ TimebaseInfo = collections.namedtuple('TimebaseInfo', ['timebase_id', - 'time_interval', + 'time_interval_ns', 'time_units', 'max_samples', 'segment_id']) @@ -598,7 +598,7 @@ def get_timebase(self, device, timebase_id, no_of_samples, oversample=1, segment Returns: namedtuple: - timebase_id: The id corresponding to the timebase used - - time_interval: The time interval between readings at the selected timebase. + - time_interval_ns: The time interval between readings at the selected timebase. - time_units: The unit of time (not supported in e.g. 3000a) - max_samples: The maximum number of samples available. The number may vary depending on the number of channels enabled and the timebase chosen. @@ -610,7 +610,7 @@ def get_timebase(self, device, timebase_id, no_of_samples, oversample=1, segment oversample, segment_index) return TimebaseInfo(nanoseconds_result.timebase_id, - nanoseconds_result.time_interval * 1.e-9, + nanoseconds_result.time_interval_ns, nanoseconds_result.time_units, nanoseconds_result.max_samples, nanoseconds_result.segment_id) @@ -619,28 +619,28 @@ def _python_get_timebase(self, handle, timebase_id, no_of_samples, oversample, s # We use get_timebase on ps2000 and ps3000 and parse the nanoseconds-int into a float. # on other drivers, we use get_timebase2, which gives us a float in the first place. if len(self._get_timebase.argtypes) == 7 and self._get_timebase.argtypes[1] == c_int16: - time_interval = c_int32(0) + time_interval_ns = c_int32(0) time_units = c_int16(0) max_samples = c_int32(0) - args = (handle, timebase_id, no_of_samples, time_interval, - time_units, oversample, max_samples) + args = (handle, timebase_id, no_of_samples, time_interval_ns, + time_units, oversample, max_samples) converted_args = self._convert_args(self._get_timebase, args) return_code = self._get_timebase(*converted_args) if return_code == 0: raise InvalidTimebaseError() - return TimebaseInfo(timebase_id, float(time_interval.value), time_units.value, max_samples.value, None) + return TimebaseInfo(timebase_id, float(time_interval_ns.value), time_units.value, max_samples.value, None) elif hasattr(self, '_get_timebase2') and self._get_timebase2.argtypes[1] == c_uint32: - time_interval = c_float(0.0) + time_interval_ns = c_float(0.0) max_samples = c_int32(0) if len(self._get_timebase2.argtypes) == 7: - args = (handle, timebase_id, no_of_samples, time_interval, + args = (handle, timebase_id, no_of_samples, time_interval_ns, oversample, max_samples, segment_index) converted_args = self._convert_args(self._get_timebase2, args) elif len(self._get_timebase2.argtypes) == 6: - args = (handle, timebase_id, no_of_samples, time_interval, max_samples, segment_index) + args = (handle, timebase_id, no_of_samples, time_interval_ns, max_samples, segment_index) converted_args = self._convert_args(self._get_timebase2, args) else: raise NotImplementedError("_get_timebase2 is not implemented for this driver yet") @@ -649,7 +649,7 @@ def _python_get_timebase(self, handle, timebase_id, no_of_samples, oversample, s if status != self.PICO_STATUS['PICO_OK']: raise InvalidTimebaseError(f"get_timebase2 failed ({constants.pico_tag(status)})") - return TimebaseInfo(timebase_id, time_interval.value, None, max_samples.value, segment_index) + return TimebaseInfo(timebase_id, time_interval_ns.value, None, max_samples.value, segment_index) else: raise NotImplementedError("_get_timebase2 or _get_timebase is not implemented for this driver yet") From 3cad6a91bfd7d66dcb2be9cd8555280c5c740089 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 8 Oct 2025 17:33:56 +0200 Subject: [PATCH 099/129] Update tests --- test/test_timebase.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/test_timebase.py b/test/test_timebase.py index b5c8b43..29d8533 100644 --- a/test/test_timebase.py +++ b/test/test_timebase.py @@ -18,11 +18,11 @@ class FindTimebaseTest(DriverTest): def assertValidTimebases(self, input_config, output_info): if input_config.max_time_interval is not None: - self.assertLessEqual(output_info.time_interval, input_config.max_time_interval) + self.assertLessEqual(output_info.time_interval_ns, input_config.max_time_interval) if input_config.no_of_samples is not None: self.assertGreaterEqual(output_info.max_samples, input_config.no_of_samples) if input_config.min_collection_time is not None: - self.assertGreaterEqual(output_info.time_interval * output_info.max_samples, + self.assertGreaterEqual(output_info.time_interval_ns * output_info.max_samples, input_config.min_collection_time) def test_find_timebase_success(self): @@ -134,10 +134,11 @@ def test_valid_config(self): request = TimebaseOptions(max_time_interval=0.005, no_of_samples=None, min_collection_time=1.) - actual_timebase = 0.004 - required_max_samples = int(math.ceil(request.min_collection_time / actual_timebase)) + actual_timebase_s = 0.004 + actual_timebase_ns = 0.004 * 1e9 + required_max_samples = int(math.ceil(request.min_collection_time / actual_timebase_s)) response = TimebaseInfo(timebase_id=7, - time_interval=0.004, + time_interval_ns=actual_timebase_ns, time_units=None, max_samples=required_max_samples+1, segment_id=0) @@ -148,10 +149,11 @@ def test_invalid_config(self): request = TimebaseOptions(max_time_interval=0.005, no_of_samples=None, min_collection_time=1.) - actual_timebase = 0.004 - required_max_samples = int(math.ceil(request.min_collection_time / actual_timebase)) + actual_timebase_s = 0.004 + actual_timebase_ns = 0.004 * 1e9 + required_max_samples = int(math.ceil(request.min_collection_time / actual_timebase_s)) response = TimebaseInfo(timebase_id=7, - time_interval=0.004, + time_interval_ns=actual_timebase_ns, time_units=None, max_samples=required_max_samples-5, segment_id=0) From 346854ec34912a654facd4c5c9fdef92c1adcef9 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 8 Oct 2025 17:41:27 +0200 Subject: [PATCH 100/129] Delete trailing whitespaces --- picosdk/ps5000.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/picosdk/ps5000.py b/picosdk/ps5000.py index 131169b..1e028f3 100644 --- a/picosdk/ps5000.py +++ b/picosdk/ps5000.py @@ -54,7 +54,7 @@ def __init__(self): "PS5000_S", "PS5000_MAX_TIME_UNITS", ]) - + class PWQ_CONDITIONS (Structure): _pack_ = 1 @@ -76,7 +76,7 @@ class TRIGGER_CONDITIONS (Structure): ("external", c_int32), ("aux", c_int32), ("pulseWidthQualifier", c_int32)] - + ps5000.TRIGGER_CONDITIONS = TRIGGER_CONDITIONS class TRIGGER_CHANNEL_PROPERTIES (Structure): @@ -86,9 +86,9 @@ class TRIGGER_CHANNEL_PROPERTIES (Structure): ("hysteresis", c_uint16), ("channel", c_int32), ("thresholdMode", c_int32)] - + ps5000.TRIGGER_CHANNEL_PROPERTIES = TRIGGER_CHANNEL_PROPERTIES - + doc = """ PICO_STATUS ps5000CloseUnit ( short handle @@ -504,7 +504,7 @@ class TRIGGER_CHANNEL_PROPERTIES (Structure): c_int16, c_uint32, c_void_p) - + ps5000.BlockReadyType.__doc__ = doc doc = """ void (CALLBACK *ps5000DataReady) @@ -516,7 +516,7 @@ class TRIGGER_CHANNEL_PROPERTIES (Structure): short triggered, void *pParameter ); """ - + ps5000.DataReadyType = C_CALLBACK_FUNCTION_FACTORY(None, c_int16, c_int32, @@ -524,7 +524,7 @@ class TRIGGER_CHANNEL_PROPERTIES (Structure): c_uint32, c_int16, c_void_p) - + ps5000.DataReadyType.__doc__ = doc doc = """ void (CALLBACK *ps5000StreamingReady) @@ -542,11 +542,11 @@ class TRIGGER_CHANNEL_PROPERTIES (Structure): ps5000.StreamingReadyType = C_CALLBACK_FUNCTION_FACTORY(None, c_int16, c_int32, - c_uint32, + c_uint32, c_int16, c_uint32, c_int16, c_int16, c_void_p) - + ps5000.StreamingReadyType.__doc__ = doc From c18763e3ac5467b8788655c171855817050c6ead Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 8 Oct 2025 17:45:48 +0200 Subject: [PATCH 101/129] Replace tabs with spaces --- picosdk/ps4000a.py | 106 ++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/picosdk/ps4000a.py b/picosdk/ps4000a.py index ff2626a..0dfe4cc 100644 --- a/picosdk/ps4000a.py +++ b/picosdk/ps4000a.py @@ -242,53 +242,53 @@ class PS4000A_USER_PROBE_INTERACTIONS(Structure): ]) ps4000a.PS4000A_WAVE_TYPE = make_enum([ - 'PS4000A_SINE', - 'PS4000A_SQUARE', - 'PS4000A_TRIANGLE', - 'PS4000A_RAMP_UP', - 'PS4000A_RAMP_DOWN', - 'PS4000A_SINC', - 'PS4000A_GAUSSIAN', - 'PS4000A_HALF_SINE', - 'PS4000A_DC_VOLTAGE', - 'PS4000A_WHITE_NOISE', - 'PS4000A_MAX_WAVE_TYPES', + 'PS4000A_SINE', + 'PS4000A_SQUARE', + 'PS4000A_TRIANGLE', + 'PS4000A_RAMP_UP', + 'PS4000A_RAMP_DOWN', + 'PS4000A_SINC', + 'PS4000A_GAUSSIAN', + 'PS4000A_HALF_SINE', + 'PS4000A_DC_VOLTAGE', + 'PS4000A_WHITE_NOISE', + 'PS4000A_MAX_WAVE_TYPES', ]) ps4000a.PS4000A_SWEEP_TYPE = make_enum([ - 'PS4000A_UP', - 'PS4000A_DOWN', - 'PS4000A_UPDOWN', - 'PS4000A_DOWNUP', - 'PS4000A_MAX_SWEEP_TYPES', + 'PS4000A_UP', + 'PS4000A_DOWN', + 'PS4000A_UPDOWN', + 'PS4000A_DOWNUP', + 'PS4000A_MAX_SWEEP_TYPES', ]) ps4000a.PS4000A_SIGGEN_TRIG_TYPE = make_enum([ - 'PS4000A_SIGGEN_RISING', - 'PS4000A_SIGGEN_FALLING', - 'PS4000A_SIGGEN_GATE_HIGH', - 'PS4000A_SIGGEN_GATE_LOW', + 'PS4000A_SIGGEN_RISING', + 'PS4000A_SIGGEN_FALLING', + 'PS4000A_SIGGEN_GATE_HIGH', + 'PS4000A_SIGGEN_GATE_LOW', ]) ps4000a.PS4000A_SIGGEN_TRIG_SOURCE = make_enum([ - 'PS4000A_SIGGEN_NONE', - 'PS4000A_SIGGEN_SCOPE_TRIG', - 'PS4000A_SIGGEN_AUX_IN', - 'PS4000A_SIGGEN_EXT_IN', - 'PS4000A_SIGGEN_SOFT_TRIG', + 'PS4000A_SIGGEN_NONE', + 'PS4000A_SIGGEN_SCOPE_TRIG', + 'PS4000A_SIGGEN_AUX_IN', + 'PS4000A_SIGGEN_EXT_IN', + 'PS4000A_SIGGEN_SOFT_TRIG', ]) ps4000a.PS4000A_INDEX_MODE = make_enum([ - 'PS4000A_SINGLE', - 'PS4000A_DUAL', - 'PS4000A_QUAD', - 'PS4000A_MAX_INDEX_MODES', + 'PS4000A_SINGLE', + 'PS4000A_DUAL', + 'PS4000A_QUAD', + 'PS4000A_MAX_INDEX_MODES', ]) ps4000a.PS4000A_EXTRA_OPERATIONS = make_enum([ - 'PS4000A_ES_OFF', - 'PS4000A_WHITENOISE', - 'PS4000A_PRBS', + 'PS4000A_ES_OFF', + 'PS4000A_WHITENOISE', + 'PS4000A_PRBS', ]) def _define_conditions_info(): @@ -333,9 +333,9 @@ def _define_conditions_info(): ]) class PS4000A_CONDITION (Structure): - _pack_ = 1 - _fields_ = [("source", c_int32), - ("condition", c_int32)] + _pack_ = 1 + _fields_ = [("source", c_int32), + ("condition", c_int32)] ps4000a.PS4000A_CONDITION = PS4000A_CONDITION @@ -965,7 +965,7 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): doc = """ PICO_STATUS ps4000aMinimumValue ( - int16_t handle, + int16_t handle, int16_t * value ); """ ps4000a.make_symbol("_minimum_value", "ps4000aMinimumValue", c_uint32, [c_int16, c_void_p], doc) @@ -1076,39 +1076,39 @@ class PS4000A_TRIGGER_CHANNEL_PROPERTIES(Structure): doc = """ PICO_STATUS ps4000aOpenUnitWithResolution ( - int16_t *handle, - int8_t *serial, - PS4000A_DEVICE_RESOLUTION resolution - ); """ + int16_t *handle, + int8_t *serial, + PS4000A_DEVICE_RESOLUTION resolution + ); """ ps4000a.make_symbol("_open_unit_with_resolution", "ps4000aOpenUnitWithResolution", c_uint32, [c_void_p, c_void_p, c_int32], doc) doc = """ PICO_STATUS ps4000aGetDeviceResolution ( - int16_t handle, - PS4000A_DEVICE_RESOLUTION *resolution - ); """ + int16_t handle, + PS4000A_DEVICE_RESOLUTION *resolution + ); """ ps4000a.make_symbol("_get_resolution", "ps4000aGetDeviceResolution", c_uint32, [c_int16, c_void_p], doc) doc = """ PICO_STATUS ps4000aSetDeviceResolution ( - int16_t handle, - PS4000A_DEVICE_RESOLUTION resolution - ); """ + int16_t handle, + PS4000A_DEVICE_RESOLUTION resolution + ); """ ps4000a.make_symbol("_set_resolution", "ps4000aSetDeviceResolution", c_uint32, [c_int16, c_int32], doc) doc = """ PICO_STATUS ps4000aSetProbeInteractionCallback ( - int16_t handle, - ps4000aProbeInteractions callback - ); """ + int16_t handle, + ps4000aProbeInteractions callback + ); """ ps4000a.make_symbol("_set_probe_interaction_callback", "ps4000aSetProbeInteractionCallback", c_uint32, [c_int16, c_void_p], doc) doc = """ void *ps4000aProbeInteractions ( - int16_t handle, - PICO_STATUS status, - PS4000A_USER_PROBE_INTERACTIONS * probes, - uint32_t nProbes + int16_t handle, + PICO_STATUS status, + PS4000A_USER_PROBE_INTERACTIONS * probes, + uint32_t nProbes ); define a python function which accepts the correct arguments, and pass it to the constructor of this type. """ From 79e4ed3232b3ab10f78b6e83bb66401d2ff73bdc Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 8 Oct 2025 17:46:19 +0200 Subject: [PATCH 102/129] Remove trailing whitespaces --- picosdk/ps5000.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/picosdk/ps5000.py b/picosdk/ps5000.py index 1e028f3..6455a52 100644 --- a/picosdk/ps5000.py +++ b/picosdk/ps5000.py @@ -406,8 +406,8 @@ class TRIGGER_CHANNEL_PROPERTIES (Structure): SIGGEN_TRIG_SOURCE triggerSource, short extInThreshold ); """ -ps5000.make_symbol("_SetSigGenArbitrary", "ps5000SetSigGenArbitrary", c_uint32, - [c_int16, c_int32, c_uint32, c_uint32, c_uint32, c_uint32, c_uint32, c_void_p, +ps5000.make_symbol("_SetSigGenArbitrary", "ps5000SetSigGenArbitrary", c_uint32, + [c_int16, c_int32, c_uint32, c_uint32, c_uint32, c_uint32, c_uint32, c_void_p, c_int32, c_int32, c_int16, c_int32, c_uint32, c_uint32, c_int32, c_int32, c_int16], doc) doc = """ PICO_STATUS ps5000SetSigGenBuiltIn @@ -428,7 +428,7 @@ class TRIGGER_CHANNEL_PROPERTIES (Structure): SIGGEN_TRIG_SOURCE triggerSource, short extInThreshold ); """ -ps5000.make_symbol("_SetSigGenBuiltIn", "ps5000SetSigGenBuiltIn", c_uint32, +ps5000.make_symbol("_SetSigGenBuiltIn", "ps5000SetSigGenBuiltIn", c_uint32, [c_int16, c_int32, c_uint32, c_int16, c_int64, c_int64, c_int64, c_int64, c_int64, c_int32, c_int16, c_uint32, c_uint32, c_int32, c_int32, c_int16], doc) doc = """ PICO_STATUS ps5000SetSimpleTrigger From 80a3e4638fc89534992d7abdddf8e15f57a87681 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 16 Oct 2025 15:35:09 +0200 Subject: [PATCH 103/129] Raise error when Picoscope not ready before given timeout --- picosdk/library.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 0bfac28..5cd2703 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -859,13 +859,16 @@ def stop_block_capture(self, device, timeout_minutes=5): Args: device (picosdk.device.Device): The device instance timeout_minutes (int/float): The timeout in minutes. If the time exceeds the timeout, the poll stops. + + Raises: + TimeoutError: If the device is not ready within the specified timeout. """ if timeout_minutes < 0: raise ArgumentOutOfRangeError("timeout_minutes must be non-negative.") - timeout = time.time() + timeout_minutes*60 + timeout = time.time() + timeout_minutes * 60 while not self.is_ready(device): if time.time() > timeout: - break + raise TimeoutError(f"Picoscope not ready within {timeout_minutes} minute(s).") @requires_device() def maximum_value(self, device): From 6abb6254009f54a7f717679e8cc41e6bf88de7dc Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 3 Nov 2025 21:03:29 +0100 Subject: [PATCH 104/129] Don't use the number of retrieved samples, use max samples instead --- picosdk/library.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 5cd2703..4d5115a 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -980,18 +980,16 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} else: raise NotImplementedError("not done other driver types yet") - num_samples_retrieved = no_of_samples.value - for channel, arr in buffers.items(): - data = arr[:num_samples_retrieved] + for channel, buffer in buffers.items(): if isinstance(channel, int) or channel.isnumeric(): - scope_data[channel] = numpy.asarray(split_mso_data_fast(no_of_samples, data)) + scope_data[channel] = numpy.asarray(split_mso_data_fast(no_of_samples, buffer)) else: - scope_data[channel] = numpy.array(adc_to_mv(data, max_voltage[channel], + scope_data[channel] = numpy.array(adc_to_mv(buffer, max_voltage[channel], self.maximum_value(device))) * probe_attenuation[channel] time_sec = numpy.linspace(0, (samples - 1) * time_interval_sec, - num_samples_retrieved) + samples) scope_data["time"] = numpy.array(time_sec) if save_to_file: From ddc7e4b8e5a6caef29ea2245a009e3614b988081 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 3 Nov 2025 21:04:37 +0100 Subject: [PATCH 105/129] Fix style and add more info --- picosdk/device.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index 4388c34..1f5577d 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -434,7 +434,8 @@ def run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, over float: The approximate time (in seconds) which the device will take to capture with these settings """ self._max_samples = pre_trigger_samples + post_trigger_samples - return self.driver.run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample, segment_index) + return self.driver.run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample, + segment_index) @requires_open() def is_ready(self): @@ -444,7 +445,7 @@ def is_ready(self): @requires_open() def stop_block_capture(self, timeout_minutes=5): - """Poll the driver to see if it has finished collecting the requested samples. + """Poll the driver to see if it has finished collecting the requested samples and then stops the capture. Args: timeout_minutes (int/float): The timeout in minutes. If the time exceeds the timeout, the poll stops. @@ -475,7 +476,7 @@ def set_all_data_buffers(self, segment_index=0, mode='NONE'): self.set_data_buffer(channel_or_port, segment_index, mode) @requires_open() - def get_values(self,start_index=0, downsample_ratio=0, downsample_ratio_mode="NONE", segment_index=0, + def get_values(self, start_index=0, downsample_ratio=0, downsample_ratio_mode="NONE", segment_index=0, output_dir=".", filename="data", save_to_file=False): """Get stored data values from the scope and store it in a clean SingletonScopeDataDict object. From a2423689fb14bcad198c22fc6c4a7b698ffb9a2c Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 17 Dec 2025 16:49:35 +0100 Subject: [PATCH 106/129] Add set_trigger_conditions method --- picosdk/device.py | 10 ++++++++++ picosdk/library.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index 1f5577d..d9e591a 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -419,6 +419,16 @@ def set_simple_trigger(self, channel, enable=True, threshold_mv=500, direction=" self.driver.set_simple_trigger(self, max_voltage, max_adc, enable, channel, threshold_mv, direction, delay, auto_trigger_ms) + @requires_open() + def set_trigger_conditions(self, trigger_input): + """Sets up trigger conditions on the scope's inputs. + Sets trigger state to TRUE for given `trigger_input`, the rest will be DONT CARE + + Args: + trigger (str): What to trigger (e.g. channelA, channelB, external, aux, pulseWidthQualifier, digital) + """ + return self.driver.set_trigger_conditions(self, trigger_input) + @requires_open() def run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample=1, segment_index=0): """This function starts collecting data in block mode. diff --git a/picosdk/library.py b/picosdk/library.py index 4d5115a..dedfcfc 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -1041,6 +1041,43 @@ def set_and_load_data(self, device, active_sources, buffer_length, time_interval downsample_ratio, downsample_ratio_mode, segment_index, output_dir, filename, save_to_file, probe_attenuation) + @requires_device() + def set_trigger_conditions(self, device, trigger_input): + """Sets up trigger conditions on the scope's inputs. + Sets trigger state to TRUE for given `trigger_input`, the rest will be DONT CARE + + Args: + device (picosdk.device.Device): Device instance + trigger (str): What to trigger (e.g. channelA, channelB, external, aux, pulseWidthQualifier, digital) + """ + + if hasattr(self, '_set_trigger_channel_conditions_v2'): + trigger_conditions = getattr(self, self.name.upper() + '_TRIGGER_CONDITIONS_V2', None) + trigger_state = getattr(self, self.name.upper() + '_TRIGGER_STATE', None) + + if not trigger_conditions or not trigger_state: + raise PicoError(f"Trigger conditions not fully defined for {self.name} driver.") + + trigger_dont_care = trigger_state[self.name.upper() + '_CONDITION_DONT_CARE'] + trigger_true = trigger_state[self.name.upper() + '_CONDITION_TRUE'] + kwargs = {field[0]: trigger_dont_care for field in trigger_conditions._fields_} + + if trigger_input in kwargs: + kwargs[trigger_input] = trigger_true + else: + raise ArgumentOutOfRangeError(f"Invalid trigger source: '{trigger_input}'. " + f"Valid sources are: {list(kwargs.keys())}") + + conditions = trigger_conditions(**kwargs) + args = (device.handle, byref(conditions), 1) + converted_args = self._convert_args(self._set_trigger_channel_conditions_v2, args) + status = self._set_trigger_channel_conditions_v2(*converted_args) + + if status != self.PICO_STATUS['PICO_OK']: + raise PicoError(f"set_trigger_channel_conditions_v2 failed ({constants.pico_tag(status)})") + else: + raise NotImplementedError("This device does not support setting trigger conditions via V2 struct.") + @requires_device("set_trigger_channel_properties requires a picosdk.device.Device instance, passed to the correct owning driver.") def set_trigger_channel_properties(self, device, threshold_upper, threshold_upper_hysteresis, threshold_lower, threshold_lower_hysteresis, channel, threshold_mode, aux_output_enable, From d98577f58be187b21de61d3aa8fde60c583dc238 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 17 Dec 2025 17:03:30 +0100 Subject: [PATCH 107/129] Remove byref --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index dedfcfc..6648ca6 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -1069,7 +1069,7 @@ def set_trigger_conditions(self, device, trigger_input): f"Valid sources are: {list(kwargs.keys())}") conditions = trigger_conditions(**kwargs) - args = (device.handle, byref(conditions), 1) + args = (device.handle, conditions, 1) converted_args = self._convert_args(self._set_trigger_channel_conditions_v2, args) status = self._set_trigger_channel_conditions_v2(*converted_args) From 15e121ea72c91d6da3be031d77a1a34d133763c3 Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 19 Dec 2025 12:20:06 +0100 Subject: [PATCH 108/129] Update memory_segments method to use _convert_args --- picosdk/library.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 6648ca6..35ac899 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -578,11 +578,13 @@ def memory_segments(self, device, number_segments): if not hasattr(self, '_memory_segments'): raise DeviceCannotSegmentMemoryError() max_samples = c_int32(0) - status = self._memory_segments(c_int16(device.handle), c_uint32(number_segments), byref(max_samples)) + args = (device.handle, number_segments, max_samples) + converted_args = self._convert_args(self._get_max_segments, args) + status = self._get_max_segments(*converted_args) if status != self.PICO_STATUS['PICO_OK']: raise InvalidMemorySegmentsError("could not segment the device memory into (%s) segments (%s)" % ( number_segments, constants.pico_tag(status))) - return max_samples + return max_samples.value @requires_device("get_timebase requires a picosdk.device.Device instance, passed to the correct owning driver.") def get_timebase(self, device, timebase_id, no_of_samples, oversample=1, segment_index=0): From ac3e8f8bae4e6d3c9a444b6e8e6ef89603b7f412 Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 19 Dec 2025 12:21:01 +0100 Subject: [PATCH 109/129] Add get_max_segments method --- picosdk/device.py | 9 +++++++++ picosdk/library.py | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index d9e591a..c5505f5 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -380,6 +380,15 @@ def memory_segments(self, number_segments): """ return self.driver.memory_segments(self, number_segments) + @requires_open() + def get_max_segments(self): + """Get the maximum number of memory segments supported by the device. + + Returns: + int: The maximum number of memory segments supported by the device. + """ + return self.driver.get_max_segments(self) + @requires_open() def maximum_value(self): """Get the maximum ADC value for this device.""" diff --git a/picosdk/library.py b/picosdk/library.py index 35ac899..e2f1f07 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -586,6 +586,26 @@ def memory_segments(self, device, number_segments): number_segments, constants.pico_tag(status))) return max_samples.value + @requires_device() + def get_max_segments(self, device): + """Get the maximum number of memory segments supported by the device. + + Returns: + int: The maximum number of memory segments supported by the device. + """ + if not hasattr(self, '_get_max_segments'): + raise NotImplementedError("This device doesn't support getting maximum segments") + + max_segments = c_int32(0) + args = (device.handle, max_segments) + converted_args = self._convert_args(self._get_max_segments, args) + status = self._get_max_segments(*converted_args) + + if status != self.PICO_STATUS['PICO_OK']: + raise PicoError(f"get_max_segments failed ({constants.pico_tag(status)})") + + return max_segments.value + @requires_device("get_timebase requires a picosdk.device.Device instance, passed to the correct owning driver.") def get_timebase(self, device, timebase_id, no_of_samples, oversample=1, segment_index=0): """Query the device about what time precision modes it can handle. From 6220b511054b33aeae0426b91442de02c9d2cc6e Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 19 Dec 2025 12:53:17 +0100 Subject: [PATCH 110/129] Fix method name --- picosdk/library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index e2f1f07..a598974 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -579,8 +579,8 @@ def memory_segments(self, device, number_segments): raise DeviceCannotSegmentMemoryError() max_samples = c_int32(0) args = (device.handle, number_segments, max_samples) - converted_args = self._convert_args(self._get_max_segments, args) - status = self._get_max_segments(*converted_args) + converted_args = self._convert_args(self._memory_segments, args) + status = self._memory_segments(*converted_args) if status != self.PICO_STATUS['PICO_OK']: raise InvalidMemorySegmentsError("could not segment the device memory into (%s) segments (%s)" % ( number_segments, constants.pico_tag(status))) From 77b3f6566af7a5706f9d8c7d7c422a575702d2aa Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 24 Dec 2025 12:07:48 +0100 Subject: [PATCH 111/129] Fix style --- picosdk/library.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index a598974..1b20233 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -762,9 +762,9 @@ def set_digital_channel_trigger(self, device, channel_number=15, direction="DIRE """ if (hasattr(self, '_set_trigger_digital_port_properties') and len(self._set_trigger_digital_port_properties.argtypes) == 3): - digital_properties = getattr(self, self.name.upper() +'_DIGITAL_CHANNEL_DIRECTIONS', None) - digital_channels = getattr(self, self.name.upper() +'_DIGITAL_CHANNEL', None) - directions = getattr(self, self.name.upper() +'_DIGITAL_DIRECTION', None) + digital_properties = getattr(self, self.name.upper() + '_DIGITAL_CHANNEL_DIRECTIONS', None) + digital_channels = getattr(self, self.name.upper() + '_DIGITAL_CHANNEL', None) + directions = getattr(self, self.name.upper() + '_DIGITAL_DIRECTION', None) if digital_properties and digital_channels and directions: digital_channel = self.name.upper() + '_DIGITAL_CHANNEL_' + str(channel_number) direction = self.name.upper() + '_DIGITAL_' + direction @@ -968,8 +968,8 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} Args: device (picosdk.device.Device): Device instance - buffers (dict): Dictionary of buffers where the data will be stored. The keys are channel names or port numbers, - and the values are numpy arrays. + buffers (dict): Dictionary of buffers where the data will be stored. The keys are channel names or + port numbers, and the values are numpy arrays. samples (int): The number of samples to retrieve from the scope. time_interval_sec (float): The time interval between samples in seconds. (obtained from get_timebase) max_voltage (dict): The maximum voltage of the range used per channel. (obtained from set_channel) From d0e458d8b2563df0d3048ac3b48445076657f7ef Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 24 Dec 2025 12:08:41 +0100 Subject: [PATCH 112/129] Rename function --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index 1b20233..8e310e3 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -1064,7 +1064,7 @@ def set_and_load_data(self, device, active_sources, buffer_length, time_interval save_to_file, probe_attenuation) @requires_device() - def set_trigger_conditions(self, device, trigger_input): + def set_trigger_conditions_v2(self, device, trigger_input): """Sets up trigger conditions on the scope's inputs. Sets trigger state to TRUE for given `trigger_input`, the rest will be DONT CARE From 1bd3291d425963ad1bebe0a9b5eebc9c2ca90eba Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 24 Dec 2025 14:07:37 +0100 Subject: [PATCH 113/129] Add error handling when the requested number of samples don't match the retrieved amount of samples --- picosdk/library.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/picosdk/library.py b/picosdk/library.py index 8e310e3..465f9b1 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -997,6 +997,9 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} converted_args = self._convert_args(self._get_values, args) status = self._get_values(*converted_args) + if samples != no_of_samples.value: + raise InvalidCaptureParameters("get_values could not retrieve the requested number of samples. " + f"Requested: {samples}, Retrieved: {no_of_samples.value}") if status != self.PICO_STATUS['PICO_OK']: raise InvalidCaptureParameters(f"get_values failed ({constants.pico_tag(status)})") else: From cf38e35481eba3de326c73ac26eed2026447dfaf Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 24 Dec 2025 11:29:05 +0100 Subject: [PATCH 114/129] Simplify the data storage with intuitive indexing --- picosdk/library.py | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 465f9b1..c1d1a82 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -77,21 +77,27 @@ def voltage_to_logic_level(voltage): def split_mso_data_fast(data_length, data): - """Return a tuple of 8 arrays, each of which is the values over time of a different digital channel. + """Split port data into individual digital channels. - The tuple contains the channels in order (D7, D6, D5, ... D0) or equivalently (D15, D14, D13, ... D8). + The tuple contains the channels in order (D0, D1, D2, ... D7) or equivalently (D8, D9, D10, ... D15). Args: data_length (c_int32): The length of the data array. data (c_int16 array): The data array containing the digital port values. + + Returns: + tuple: A tuple of 8 numpy arrays, each containing the digital channel values over time """ num_samples = data_length.value # Makes an array for each digital channel buffer_binary_dj = tuple(numpy.empty(num_samples, dtype=numpy.uint8) for _ in range(8)) # Splits out the individual bits from the port into the binary values for each digital channel/pin. for i in range(num_samples): + val = data[i] for j in range(8): - buffer_binary_dj[j][i] = 1 if (data[i] & (1 << (7-j))) else 0 + # map bit j direct to buffer j + # bit 0 -> D0 (or D8), bit 1 -> D1 (or D9), ..., bit 7 -> D7 (or D15) + buffer_binary_dj[j][i] = (val >> j) & 1 return buffer_binary_dj @@ -181,29 +187,26 @@ def __getitem__(self, key: str): """ if isinstance(key, str): match = re.match(r"D(?P\d+)", key, re.IGNORECASE) - else: - match = None - - if match: - try: - digital_number = int(match.group('channel_num')) + if match: + try: + digital_number = int(match.group('channel_num')) - # Calculate which port and which row (bit) in the port's data array - port_number = digital_number // 8 # Port 0 = D0-D7, Port 1 = D8-D15 + # Calculate which port and which row (bit) in the port's data array + port_number = digital_number // 8 # Port 0 = D0-D7, Port 1 = D8-D15 - # Reverse bit order within port: Assuming D7 is the first row (index 0) and D0 is the last row (index 7) - row_index = 7 - (digital_number % 8) + # D0: 0 % 8 = index 0; D8: 8 % 8 = index 0 + row_index = digital_number % 8 - # Get the data for the entire port - port_data = super().__getitem__(port_number) + # Get the data for the entire port + port_data = super().__getitem__(port_number) - # Select the correct row from the numpy array. - return port_data[row_index] + # Select the correct row from the numpy array. + return port_data[row_index] - except (IndexError, ValueError, KeyError) as e: - raise ValueError(f"Invalid digital channel {key}: {str(e)}") + except (IndexError, ValueError, KeyError) as e: + raise ValueError(f"Invalid digital channel {key}: {str(e)}") - # Handle direct port access (0-3) or analog channels + # Handle direct port access (0-1) or analog channels return super().__getitem__(key) From 6e2f449af995a37263690c9a5362af4fca027de3 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 6 Jan 2026 14:19:35 +0100 Subject: [PATCH 115/129] Delete unused import --- picosdk/library.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index c1d1a82..51afad0 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -6,9 +6,6 @@ Note: Many of the functions in this class are missing: these are populated by the psN000(a).py modules, which subclass this type and attach the missing methods. """ - -from __future__ import print_function - import json import re import sys From 7b1506caa81e80a61767b60635ed9a7f646175e7 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 6 Jan 2026 14:19:58 +0100 Subject: [PATCH 116/129] Make sure enable is 1 or 0 instead of True or False --- picosdk/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index 51afad0..2c038a3 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -740,7 +740,7 @@ def set_simple_trigger(self, device, max_voltage, max_adc=None, enable=True, cha threshold_direction_id = threshold_directions[self.name.upper() + f'_{direction.upper()}'] else: raise NotImplementedError("This device doesn't support threshold direction") - + enable = 1 if enable else 0 args = (device.handle, enable, self.PICO_CHANNEL[channel], adc_threshold, threshold_direction_id, delay, auto_trigger_ms) converted_args = self._convert_args(self._set_simple_trigger, args) From 606a6294cd0ee80e584c886c2d5a09b8e4432dda Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 6 Jan 2026 14:20:10 +0100 Subject: [PATCH 117/129] Delete line --- picosdk/library.py | 1 - 1 file changed, 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index 2c038a3..40d9e5a 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -1249,4 +1249,3 @@ def _convert_args(self, func, args): else: converted.append(None) return tuple(converted) - From 597b4184fd960597619f457319d667c507932f96 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 6 Jan 2026 14:27:55 +0100 Subject: [PATCH 118/129] Make sure every True/False is converted to the int 1 or 0 --- picosdk/library.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index 40d9e5a..eceef78 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -498,7 +498,7 @@ def set_digital_port(self, device, port_number=0, enabled=True, voltage_level=1. if not digital_ports: raise NotImplementedError("This device doesn't support digital ports") port_id = digital_ports[self.name.upper() + "_DIGITAL_PORT" + str(port_number)] - args = (device.handle, port_id, enabled, logic_level) + args = (device.handle, port_id, 1 if enabled else 0, logic_level) converted_args = self._convert_args(self._set_digital_port, args) status = self._set_digital_port(*converted_args) if status != self.PICO_STATUS['PICO_OK']: @@ -740,9 +740,8 @@ def set_simple_trigger(self, device, max_voltage, max_adc=None, enable=True, cha threshold_direction_id = threshold_directions[self.name.upper() + f'_{direction.upper()}'] else: raise NotImplementedError("This device doesn't support threshold direction") - enable = 1 if enable else 0 - args = (device.handle, enable, self.PICO_CHANNEL[channel], adc_threshold, - threshold_direction_id, delay, auto_trigger_ms) + args = (device.handle, 1 if enable else 0, self.PICO_CHANNEL[channel], adc_threshold, + threshold_direction_id, delay, auto_trigger_ms) converted_args = self._convert_args(self._set_simple_trigger, args) status = self._set_simple_trigger(*converted_args) @@ -1127,9 +1126,9 @@ def set_trigger_channel_properties(self, device, threshold_upper, threshold_uppe """ if hasattr(self, '_set_trigger_channel_properties'): args = (device.handle, threshold_upper, threshold_upper_hysteresis, - threshold_lower, threshold_lower_hysteresis, - self.PICO_CHANNEL[channel], self.PICO_THRESHOLD_DIRECTION[threshold_mode], - aux_output_enable, auto_trigger_milliseconds) + threshold_lower, threshold_lower_hysteresis, + self.PICO_CHANNEL[channel], self.PICO_THRESHOLD_DIRECTION[threshold_mode], + 1 if aux_output_enable else 0, auto_trigger_milliseconds) converted_args = self._convert_args(self._set_trigger_channel_properties, args) status = self._set_trigger_channel_properties(*converted_args) From e558a3878ec2feaa418f677bf7bc1af6f6a699d6 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 7 Jan 2026 18:08:57 +0100 Subject: [PATCH 119/129] Add timebase property --- picosdk/device.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index c5505f5..554456a 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -126,6 +126,11 @@ def enabled_sources(self): """set: A set of enabled sources (channels and/or digital ports).""" return self._enabled_sources + @property + def timebase(self): + """int: The timebase id used for the last capture.""" + return self._timebase + @property def time_interval_ns(self): """int/float: The time interval in seconds""" @@ -157,6 +162,7 @@ def close(self): self._channel_ranges.clear() self._channel_offsets.clear() self._enabled_sources.clear() + self._timebase = None self._time_interval_ns = None self._probe_attenuations = DEFAULT_PROBE_ATTENUATION.copy() @@ -361,6 +367,7 @@ def get_timebase(self, timebase_id, no_of_samples, oversample=1, segment_index=0 - segment_id: The index of the memory segment to use """ timebase_info = self.driver.get_timebase(self, timebase_id, no_of_samples, oversample, segment_index) + self._timebase = timebase_info.timebase_id self._time_interval_ns = timebase_info.time_interval_ns return timebase_info From dea536d4fcd756265152a6d5e4ca0649afc3e506 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 7 Jan 2026 18:09:59 +0100 Subject: [PATCH 120/129] Fix style --- picosdk/library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picosdk/library.py b/picosdk/library.py index eceef78..ead60a1 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -762,7 +762,7 @@ def set_digital_channel_trigger(self, device, channel_number=15, direction="DIRE if (hasattr(self, '_set_trigger_digital_port_properties') and len(self._set_trigger_digital_port_properties.argtypes) == 3): digital_properties = getattr(self, self.name.upper() + '_DIGITAL_CHANNEL_DIRECTIONS', None) - digital_channels = getattr(self, self.name.upper() + '_DIGITAL_CHANNEL', None) + digital_channels = getattr(self, self.name.upper() + '_DIGITAL_CHANNEL', None) directions = getattr(self, self.name.upper() + '_DIGITAL_DIRECTION', None) if digital_properties and digital_channels and directions: digital_channel = self.name.upper() + '_DIGITAL_CHANNEL_' + str(channel_number) @@ -943,7 +943,7 @@ def set_data_buffer(self, device, channel_or_port, buffer_length, segment_index= if mode not in self.PICO_RATIO_MODE: raise ArgumentOutOfRangeError(f"Invalid ratio mode '{mode}' for {self.name} driver" - "or PICO_RATIO_MODE doesn't exist for this driver") + "or PICO_RATIO_MODE doesn't exist for this driver") buffer = (c_int16 * buffer_length)() args = (device.handle, id, buffer, buffer_length, segment_index, self.PICO_RATIO_MODE[mode]) converted_args = self._convert_args(self._set_data_buffer, args) From fbf97db0a8da121e05885da381e2a7658a45b48b Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 7 Jan 2026 18:25:29 +0100 Subject: [PATCH 121/129] Add setter for timebase --- picosdk/device.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index 554456a..1ce7286 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -128,9 +128,15 @@ def enabled_sources(self): @property def timebase(self): - """int: The timebase id used for the last capture.""" + """int: The timebase id used for the (last) capture.""" return self._timebase + @timebase.setter + def timebase(self, value): + if value <= 0: + raise ValueError(f"Timebase can not be {value!r}, must be greater than 0") + self._timebase = value + @property def time_interval_ns(self): """int/float: The time interval in seconds""" @@ -367,7 +373,7 @@ def get_timebase(self, timebase_id, no_of_samples, oversample=1, segment_index=0 - segment_id: The index of the memory segment to use """ timebase_info = self.driver.get_timebase(self, timebase_id, no_of_samples, oversample, segment_index) - self._timebase = timebase_info.timebase_id + self.timebase = timebase_info.timebase_id self._time_interval_ns = timebase_info.time_interval_ns return timebase_info @@ -460,7 +466,8 @@ def run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, over float: The approximate time (in seconds) which the device will take to capture with these settings """ self._max_samples = pre_trigger_samples + post_trigger_samples - return self.driver.run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample, + self.timebase = timebase_id + return self.driver.run_block(self, pre_trigger_samples, post_trigger_samples, self.timebase, oversample, segment_index) @requires_open() From a135632df8c266f6355df68e92f00c48d5fc3d15 Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 14 Jan 2026 18:05:28 +0100 Subject: [PATCH 122/129] Rename function in device.py to match library.py --- picosdk/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index 1ce7286..e86003a 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -442,14 +442,14 @@ def set_simple_trigger(self, channel, enable=True, threshold_mv=500, direction=" auto_trigger_ms) @requires_open() - def set_trigger_conditions(self, trigger_input): + def set_trigger_conditions_v2(self, trigger_input): """Sets up trigger conditions on the scope's inputs. Sets trigger state to TRUE for given `trigger_input`, the rest will be DONT CARE Args: trigger (str): What to trigger (e.g. channelA, channelB, external, aux, pulseWidthQualifier, digital) """ - return self.driver.set_trigger_conditions(self, trigger_input) + return self.driver.set_trigger_conditions_v2(self, trigger_input) @requires_open() def run_block(self, pre_trigger_samples, post_trigger_samples, timebase_id, oversample=1, segment_index=0): From d4d0f243b65c848c3ed43529060e27487334d06c Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 29 Jan 2026 09:18:13 +0100 Subject: [PATCH 123/129] Do not return all data; Use SingletonScopeDataDict to obtain data --- picosdk/device.py | 17 +++++------------ picosdk/library.py | 20 ++++++-------------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index e86003a..76da506 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -529,14 +529,11 @@ def get_values(self, start_index=0, downsample_ratio=0, downsample_ratio_mode="N output_dir (str): The output directory where the json file will be saved. filename (str): The name of the json file where the data will be stored save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise - - Returns: - Tuple of (captured data including time, overflow warnings) """ time_interval_sec = self.time_interval_ns / 1e9 if self.time_interval_ns else None - return self.driver.get_values(self, self.buffers, self.max_samples, time_interval_sec, self.channel_ranges, - start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, - filename, save_to_file, self.probe_attenuations) + self.driver.get_values(self, self.buffers, self.max_samples, time_interval_sec, self.channel_ranges, + start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, + filename, save_to_file, self.probe_attenuations) @requires_open() def set_and_load_data(self, segment_index=0, ratio_mode='NONE', start_index=0, downsample_ratio=0, @@ -555,14 +552,10 @@ def set_and_load_data(self, segment_index=0, ratio_mode='NONE', start_index=0, d output_dir (str): The output directory where the json file will be saved. filename (str): The name of the json file where the data will be stored save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise - - Returns: - Tuple of (captured data including time, overflow warnings) """ self.set_all_data_buffers(segment_index, ratio_mode) - - return self.get_values(start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, - filename, save_to_file) + self.get_values(start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, + filename, save_to_file) @requires_open() def set_trigger_channel_properties(self, threshold_upper, threshold_upper_hysteresis, threshold_lower, diff --git a/picosdk/library.py b/picosdk/library.py index ead60a1..309f9a4 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -981,9 +981,6 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} filename (str): The name of the json file where the data will be stored save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise probe_attenuation (dict): The attenuation factor of the probe used per the channel (1 or 10). - - Returns: - Tuple of (captured data including time, overflow warnings) """ scope_data = SingletonScopeDataDict() scope_data.clean_dict() @@ -1026,13 +1023,11 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} if overflow.value & (1 >> self.PICO_CHANNEL[channel]): overflow_warning[channel] = True - return scope_data, overflow_warning - @requires_device() def set_and_load_data(self, device, active_sources, buffer_length, time_interval_sec, max_voltage={}, - segment_index=0, ratio_mode='NONE', start_index=0, - downsample_ratio=0, downsample_ratio_mode="NONE", probe_attenuation=DEFAULT_PROBE_ATTENUATION, - output_dir=".", filename="data", save_to_file=False): + segment_index=0, ratio_mode='NONE', start_index=0, + downsample_ratio=0, downsample_ratio_mode="NONE", probe_attenuation=DEFAULT_PROBE_ATTENUATION, + output_dir=".", filename="data", save_to_file=False): """Load values from the device. Combines set_data_buffer and get_values to load values from the device. @@ -1053,17 +1048,14 @@ def set_and_load_data(self, device, active_sources, buffer_length, time_interval output_dir (str): The output directory where the json file will be saved. filename (str): The name of the json file where the data will be stored save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise - - Returns: - Tuple of (captured data including time, overflow warnings) """ buffers = {} for source in active_sources: buffers[source] = self.set_data_buffer(device, source, buffer_length, segment_index, ratio_mode) - return self.get_values(device, buffers, buffer_length, time_interval_sec, max_voltage, start_index, - downsample_ratio, downsample_ratio_mode, segment_index, output_dir, filename, - save_to_file, probe_attenuation) + self.get_values(device, buffers, buffer_length, time_interval_sec, max_voltage, start_index, + downsample_ratio, downsample_ratio_mode, segment_index, output_dir, filename, + save_to_file, probe_attenuation) @requires_device() def set_trigger_conditions_v2(self, device, trigger_input): From fbbda290f341b1da455c4b0b07eaa9f600954700 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 29 Jan 2026 09:27:07 +0100 Subject: [PATCH 124/129] Only return overflow warnings (for debugging purpose) --- picosdk/device.py | 16 +++++++++++----- picosdk/library.py | 14 +++++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index 76da506..0a91db5 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -529,11 +529,14 @@ def get_values(self, start_index=0, downsample_ratio=0, downsample_ratio_mode="N output_dir (str): The output directory where the json file will be saved. filename (str): The name of the json file where the data will be stored save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise + + Returns: + overflow warnings (dict): A dictionary indicating which channels had an overflow. """ time_interval_sec = self.time_interval_ns / 1e9 if self.time_interval_ns else None - self.driver.get_values(self, self.buffers, self.max_samples, time_interval_sec, self.channel_ranges, - start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, - filename, save_to_file, self.probe_attenuations) + return self.driver.get_values(self, self.buffers, self.max_samples, time_interval_sec, self.channel_ranges, + start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, + filename, save_to_file, self.probe_attenuations) @requires_open() def set_and_load_data(self, segment_index=0, ratio_mode='NONE', start_index=0, downsample_ratio=0, @@ -552,10 +555,13 @@ def set_and_load_data(self, segment_index=0, ratio_mode='NONE', start_index=0, d output_dir (str): The output directory where the json file will be saved. filename (str): The name of the json file where the data will be stored save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise + + Returns: + overflow warnings (dict): A dictionary indicating which channels had an overflow. """ self.set_all_data_buffers(segment_index, ratio_mode) - self.get_values(start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, - filename, save_to_file) + return self.get_values(start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, + filename, save_to_file) @requires_open() def set_trigger_channel_properties(self, threshold_upper, threshold_upper_hysteresis, threshold_lower, diff --git a/picosdk/library.py b/picosdk/library.py index 309f9a4..c1bc70c 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -981,6 +981,9 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} filename (str): The name of the json file where the data will be stored save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise probe_attenuation (dict): The attenuation factor of the probe used per the channel (1 or 10). + + Returns: + overflow warnings (dict): A dictionary indicating which channels had an overflow. """ scope_data = SingletonScopeDataDict() scope_data.clean_dict() @@ -1023,6 +1026,8 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} if overflow.value & (1 >> self.PICO_CHANNEL[channel]): overflow_warning[channel] = True + return overflow_warning + @requires_device() def set_and_load_data(self, device, active_sources, buffer_length, time_interval_sec, max_voltage={}, segment_index=0, ratio_mode='NONE', start_index=0, @@ -1048,14 +1053,17 @@ def set_and_load_data(self, device, active_sources, buffer_length, time_interval output_dir (str): The output directory where the json file will be saved. filename (str): The name of the json file where the data will be stored save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise + + Returns: + overflow warnings (dict): A dictionary indicating which channels had an overflow. """ buffers = {} for source in active_sources: buffers[source] = self.set_data_buffer(device, source, buffer_length, segment_index, ratio_mode) - self.get_values(device, buffers, buffer_length, time_interval_sec, max_voltage, start_index, - downsample_ratio, downsample_ratio_mode, segment_index, output_dir, filename, - save_to_file, probe_attenuation) + return self.get_values(device, buffers, buffer_length, time_interval_sec, max_voltage, start_index, + downsample_ratio, downsample_ratio_mode, segment_index, output_dir, filename, + save_to_file, probe_attenuation) @requires_device() def set_trigger_conditions_v2(self, device, trigger_input): From 16e65ab4455f881f0b6b51caae88bbb43a3d6f44 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 29 Jan 2026 10:41:08 +0100 Subject: [PATCH 125/129] Add store_values method --- picosdk/device.py | 26 +++++++++++++++++++++++++- picosdk/library.py | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index 0a91db5..075c465 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -531,13 +531,37 @@ def get_values(self, start_index=0, downsample_ratio=0, downsample_ratio_mode="N save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise Returns: - overflow warnings (dict): A dictionary indicating which channels had an overflow. + scope_data (SingletonScopeDataDict): The captured scope data. + overflow_warning (dict): A dictionary indicating which channels had an overflow. """ time_interval_sec = self.time_interval_ns / 1e9 if self.time_interval_ns else None return self.driver.get_values(self, self.buffers, self.max_samples, time_interval_sec, self.channel_ranges, start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, filename, save_to_file, self.probe_attenuations) + @requires_open() + def store_values(self, start_index=0, downsample_ratio=0, downsample_ratio_mode="NONE", segment_index=0, + output_dir=".", filename="data", save_to_file=False): + """Same as `get_values` but only returns overflow warnings and keeps the data stored in SingletonScopeDataDict. + + Args: + start_index (int): A zero-based index that indicates the start point for data collection. It is measured in + sample intervals from the start of the buffer. + downsample_ratio (int): The downsampling factor that will be applied to the raw data. + downsample_ratio_mode (str): Which downsampling mode to use. + segment_index (int): Memory segment index + output_dir (str): The output directory where the json file will be saved. + filename (str): The name of the json file where the data will be stored + save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise + + Returns: + overflow_warning (dict): A dictionary indicating which channels had an overflow. + """ + time_interval_sec = self.time_interval_ns / 1e9 if self.time_interval_ns else None + return self.driver.store_values(self, self.buffers, self.max_samples, time_interval_sec, self.channel_ranges, + start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, + filename, save_to_file, self.probe_attenuations) + @requires_open() def set_and_load_data(self, segment_index=0, ratio_mode='NONE', start_index=0, downsample_ratio=0, downsample_ratio_mode="NONE", output_dir=".", filename="data", save_to_file=False): diff --git a/picosdk/library.py b/picosdk/library.py index c1bc70c..8d20d45 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -983,7 +983,8 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} probe_attenuation (dict): The attenuation factor of the probe used per the channel (1 or 10). Returns: - overflow warnings (dict): A dictionary indicating which channels had an overflow. + scope_data (SingletonScopeDataDict): The captured scope data. + overflow_warning (dict): A dictionary indicating which channels had an overflow. """ scope_data = SingletonScopeDataDict() scope_data.clean_dict() @@ -1026,7 +1027,37 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} if overflow.value & (1 >> self.PICO_CHANNEL[channel]): overflow_warning[channel] = True - return overflow_warning + return scope_data, overflow_warning + + @requires_device() + def store_values(self, device, buffers, samples, time_interval_sec, max_voltage={}, start_index=0, + downsample_ratio=0, downsample_ratio_mode="NONE", segment_index=0, output_dir=".", + filename="data", save_to_file=False, probe_attenuation=DEFAULT_PROBE_ATTENUATION): + """Same as `get_values` but only returns overflow warnings and keeps the data stored in SingletonScopeDataDict. + + Args: + device (picosdk.device.Device): Device instance + buffers (dict): Dictionary of buffers where the data will be stored. The keys are channel names or + port numbers, and the values are numpy arrays. + samples (int): The number of samples to retrieve from the scope. + time_interval_sec (float): The time interval between samples in seconds. (obtained from get_timebase) + max_voltage (dict): The maximum voltage of the range used per channel. (obtained from set_channel) + start_index (int): A zero-based index that indicates the start point for data collection. It is measured in + sample intervals from the start of the buffer. + downsample_ratio (int): The downsampling factor that will be applied to the raw data. + downsample_ratio_mode (str): Which downsampling mode to use. + segment_index (int): Memory segment index + output_dir (str): The output directory where the json file will be saved. + filename (str): The name of the json file where the data will be stored + save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise + probe_attenuation (dict): The attenuation factor of the probe used per the channel (1 or 10). + + Returns: + overflow warnings (dict): A dictionary indicating which channels had an overflow. + """ + return self.get_values(device, buffers, samples, time_interval_sec, max_voltage, start_index, + downsample_ratio, downsample_ratio_mode, segment_index, output_dir, + filename, save_to_file, probe_attenuation)[1] @requires_device() def set_and_load_data(self, device, active_sources, buffer_length, time_interval_sec, max_voltage={}, From 668b6c378e84d5be64a9d878b088f955f78b80ed Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 6 Feb 2026 12:58:20 +0100 Subject: [PATCH 126/129] Make sure get timebase is called before get values --- picosdk/device.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index 075c465..f508e44 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -534,6 +534,11 @@ def get_values(self, start_index=0, downsample_ratio=0, downsample_ratio_mode="N scope_data (SingletonScopeDataDict): The captured scope data. overflow_warning (dict): A dictionary indicating which channels had an overflow. """ + if not self.time_interval_ns: + if self.timebase: + self.get_timebase(self.timebase, self.max_samples) + else: + raise InvalidTimebaseError("Timebase is not set. Please call get_timebase before get_values.") time_interval_sec = self.time_interval_ns / 1e9 if self.time_interval_ns else None return self.driver.get_values(self, self.buffers, self.max_samples, time_interval_sec, self.channel_ranges, start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, From 7b9a9a793d10f236be9ca23a0163b7aae1bb4e30 Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 6 Feb 2026 13:14:14 +0100 Subject: [PATCH 127/129] Add same error handling to store_values method --- picosdk/device.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/picosdk/device.py b/picosdk/device.py index f508e44..6b4ffd2 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -562,6 +562,11 @@ def store_values(self, start_index=0, downsample_ratio=0, downsample_ratio_mode= Returns: overflow_warning (dict): A dictionary indicating which channels had an overflow. """ + if not self.time_interval_ns: + if self.timebase: + self.get_timebase(self.timebase, self.max_samples) + else: + raise InvalidTimebaseError("Timebase is not set. Please call get_timebase before get_values.") time_interval_sec = self.time_interval_ns / 1e9 if self.time_interval_ns else None return self.driver.store_values(self, self.buffers, self.max_samples, time_interval_sec, self.channel_ranges, start_index, downsample_ratio, downsample_ratio_mode, segment_index, output_dir, From 2e7b53eef82768c8222381f5f5a7b3c50d44e29a Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 6 Feb 2026 16:39:18 +0100 Subject: [PATCH 128/129] Update error handling in SingletonScopeDataDict --- picosdk/library.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/picosdk/library.py b/picosdk/library.py index 8d20d45..6e590a8 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -201,7 +201,21 @@ def __getitem__(self, key: str): return port_data[row_index] except (IndexError, ValueError, KeyError) as e: - raise ValueError(f"Invalid digital channel {key}: {str(e)}") + if isinstance(e, KeyError): + available_keys = ", ".join(map(str, sorted(self.keys()))) if self.keys() else "None" + raise ValueError( + f"Could not retrieve data for digital channel {key}. The data for the corresponding " + f"digital port ({port_number}) has not been loaded into the data dictionary. " + f"Available data keys are: [{available_keys}]." + ) from e + elif isinstance(e, IndexError): + raise ValueError( + f"Could not retrieve data for digital channel {key}. The data for the corresponding " + f"digital port ({port_number}) appears to be corrupted or in an unexpected format." + ) from e + else: + raise ValueError(f"An unexpected error occurred while processing digital channel '{key}': {e}" + ) from e # Handle direct port access (0-1) or analog channels return super().__getitem__(key) From c9790b60a4e4829405ee283e923fbfa12bee0606 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 10 Feb 2026 17:10:26 +0100 Subject: [PATCH 129/129] Fix overflow keyerror when port is in buffer.keys(); Save overflow value as well --- picosdk/device.py | 2 +- picosdk/library.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/picosdk/device.py b/picosdk/device.py index 6b4ffd2..ab66f4a 100644 --- a/picosdk/device.py +++ b/picosdk/device.py @@ -560,7 +560,7 @@ def store_values(self, start_index=0, downsample_ratio=0, downsample_ratio_mode= save_to_file (bool): True if the data has to be saved to a file on the disk, False otherwise Returns: - overflow_warning (dict): A dictionary indicating which channels had an overflow. + overflow warnings (dict): A dictionary indicating which channels had an overflow. """ if not self.time_interval_ns: if self.timebase: diff --git a/picosdk/library.py b/picosdk/library.py index 6e590a8..9c58b50 100644 --- a/picosdk/library.py +++ b/picosdk/library.py @@ -1037,8 +1037,9 @@ def get_values(self, device, buffers, samples, time_interval_sec, max_voltage={} overflow_warning = {} if overflow.value: + overflow_warning["bit field"] = bin(overflow.value)[2:] for channel in buffers.keys(): - if overflow.value & (1 >> self.PICO_CHANNEL[channel]): + if channel in self.PICO_CHANNEL and (overflow.value & (1 << self.PICO_CHANNEL[channel])): overflow_warning[channel] = True return scope_data, overflow_warning