Skip to content

Commit 722e22c

Browse files
pypicosdk v0.2.23 (#4)
* Added 6000a FFT example * added ps6000a stop * Fixed autotrigger for 6000A devices * Close_unit returned none * Added conversions to docs * Added timebase conversions * Added timebased enums * Added hatch target to project.toml * Upversioned to 0.2.23
1 parent ad69225 commit 722e22c

File tree

12 files changed

+233
-18
lines changed

12 files changed

+233
-18
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@ Once tested, try an [example script from github](https://github.com/JamesPicoTec
3333
- [PicoScope Support (Compatibility)](https://jamespicotech.github.io/pyPicoSDK/dev/current)
3434

3535
## Version Control
36-
pyPicoSDK: 0.2.22
36+
pyPicoSDK: 0.2.23
3737

38-
Docs: 0.1.3
38+
Docs: 0.1.4

docs/docs/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@ Once tested, try an [example script from github](https://github.com/JamesPicoTec
3333
- [PicoScope Support (Compatibility)](https://jamespicotech.github.io/pyPicoSDK/dev/current)
3434

3535
## Version Control
36-
pyPicoSDK: 0.2.22
36+
pyPicoSDK: 0.2.23
3737

38-
Docs: 0.1.3
38+
Docs: 0.1.4

docs/docs/ref/conversions.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# PicoScope conversions
2+
These functions are general functions to convert data to another format.
3+
This is particularly useful for converting ADC data to mV or calculating
4+
the needed timebase for your PicoScope.
5+
6+
As the conversions talk to the PicoScope to retrieve the resolution and ADC limits,
7+
the PicoScope needs to be initialized using `scope.open_unit()` followed by the conversion.
8+
9+
## Example
10+
```
11+
>>> import pypicosdk as psdk
12+
>>> scope = psdk.ps6000a()
13+
>>> scope.open_unit(resolution=psdk.RESOLUTION._8BIT)
14+
>>> scope.mv_to_adc(100, channel_range=psdk.RANGE.V1)
15+
3251
16+
>>> scope.close_unit()
17+
```
18+
19+
## Reference
20+
::: pypicosdk.pypicosdk.PicoScopeBase
21+
options:
22+
filters:
23+
- ".*_to_.*"
24+
#- "!^_[^_]"
25+
show_root_toc_entry: false
26+
summary: true

docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ nav:
99
- Introduction: ref/introduction.md
1010
- PicoScope 6000 (A) Specific Functions: ref/ps6000a.md
1111
- PicoScope 5000 (A) Specific Functions: ref/ps5000a.md
12+
- General Conversions: ref/conversions.md
1213
- General Constants: ref/constants.md
1314
- Development:
1415
- Changelog: dev/changelog.md

examples/example_6000a_fft.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
##################################################################
2+
# FFT example for a PicoScope 6000E.
3+
#
4+
# Description:
5+
# This will convert the voltage data to the frequency domain and
6+
# display it in pyplot
7+
#
8+
# Requirements:
9+
# - PicoScope 6000E
10+
# - Python packages:
11+
# pip install matplotlib scipy numpy pypicosdk
12+
#
13+
# Setup:
14+
# - Connect 6000E SigGen (AWG) to Channel A of the oscilloscope
15+
# using a BNC cable or probe
16+
#
17+
##################################################################
18+
19+
import pypicosdk as psdk
20+
from matplotlib import pyplot as plt
21+
import numpy as np
22+
from scipy.fft import fft, fftfreq
23+
from scipy.signal import windows
24+
25+
# Setup variables
26+
timebase = 3
27+
samples = 5_000_000
28+
channel_a = psdk.CHANNEL.A
29+
range = psdk.RANGE.V1
30+
threshold = 0
31+
32+
# SigGen variables
33+
frequency = 1_000_000
34+
pk2pk = 0.8
35+
wave_type = psdk.WAVEFORM.SQUARE
36+
37+
# Initialise PicoScope 6000
38+
scope = psdk.ps6000a()
39+
scope.open_unit()
40+
41+
# Setup siggen
42+
scope.set_siggen(frequency, pk2pk, wave_type)
43+
44+
# Setup channels and trigger
45+
scope.set_channel(channel=channel_a, range=range)
46+
scope.set_simple_trigger(channel=channel_a, threshold_mv=threshold)
47+
48+
# Run the block capture
49+
channel_buffer, time_axis = scope.run_simple_block_capture(timebase, samples)
50+
51+
# Finish with PicoScope
52+
scope.close_unit()
53+
54+
# Take out data (converting ns time axis to s)
55+
v = np.array(channel_buffer[channel_a])
56+
t = np.array(time_axis) * 1E-9
57+
58+
# Get sample rate
59+
dt = t[1] - t[0]
60+
61+
# Create a window and apply to data
62+
window = windows.hann(samples)
63+
v_windowed = v * window
64+
65+
# Create fft from data
66+
V_f = fft(v_windowed)
67+
freqs = fftfreq(samples, dt)
68+
69+
# Remove negative frequency data
70+
positive_freqs = freqs[:samples//2]
71+
amplitudes = np.abs(V_f[:samples//2])
72+
73+
# Plot data to pyplot
74+
plt.figure(figsize=(10, 4))
75+
plt.plot(positive_freqs / 1e6, amplitudes) # Convert Hz to MHz
76+
plt.xlabel("Frequency (MHz)")
77+
plt.ylabel("Amplitude (mV)")
78+
plt.title("FFT of Voltage Signal")
79+
plt.grid(True)
80+
plt.tight_layout()
81+
plt.show()

examples/example_6000a_timebase.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
Example to figure out the correct timebase value for a specific interval.
3+
"""
4+
from pypicosdk import ps6000a, CHANNEL, RANGE, SAMPLE_RATE, TIME_UNIT
5+
6+
# Variables
7+
interval_s = 10E-9 # 10 us
8+
9+
# Open PicoScope 6000
10+
scope = ps6000a()
11+
scope.open_unit()
12+
13+
# Setup channels to make sure sample interval is accurate
14+
scope.set_channel(CHANNEL.A, RANGE.V1)
15+
scope.set_channel(CHANNEL.C, RANGE.mV100)
16+
17+
# Return suggested timebase and actual sample interval
18+
print(scope.sample_rate_to_timebase(100, unit=SAMPLE_RATE.MSPS))
19+
print(scope.interval_to_timebase(0.001, unit=TIME_UNIT.US))
20+
print(scope.get_nearest_sampling_interval(1E-9))

pypicosdk/constants.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,4 +265,17 @@ class POWER_SOURCE:
265265
"""
266266
SUPPLY_CONNECTED = 0x00000119
267267
SUPPLY_NOT_CONNECTED = 0x0000011A
268-
USB3_0_DEVICE_NON_USB3_0_PORT= 0x0000011E
268+
USB3_0_DEVICE_NON_USB3_0_PORT= 0x0000011E
269+
270+
class SAMPLE_RATE(IntEnum):
271+
SPS = 1
272+
KSPS = 1_000
273+
MSPS = 1_000_000
274+
GSPS = 1_000_000_000
275+
276+
class TIME_UNIT(IntEnum):
277+
PS = 1_000_000_000_000
278+
NS = 1_000_000_000
279+
US = 1_000_000
280+
MS = 1_000
281+
S = 1

pypicosdk/pypicosdk.py

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def _open_unit(self, serial_number:int=None, resolution:RESOLUTION=0) -> None:
123123
)
124124
self.resolution = resolution
125125

126-
def close_unit(self) -> int:
126+
def close_unit(self) -> None:
127127
"""
128128
Closes the PicoScope device and releases the hardware handle.
129129
@@ -135,6 +135,15 @@ def close_unit(self) -> int:
135135

136136
self._get_attr_function('CloseUnit')(self.handle)
137137

138+
def stop(self) -> None:
139+
"""
140+
This function stops the scope device from sampling data
141+
"""
142+
self._call_attr_function(
143+
'Stop',
144+
self.handle
145+
)
146+
138147
def is_ready(self) -> None:
139148
"""
140149
Blocks execution until the PicoScope device is ready.
@@ -204,7 +213,7 @@ def _get_enabled_channel_flags(self) -> int:
204213
enabled_channel_byte += 2**channel
205214
return enabled_channel_byte
206215

207-
def get_nearest_sampling_interval(self, sample_rate:float) -> dict:
216+
def get_nearest_sampling_interval(self, interval_s:float) -> dict:
208217
"""
209218
This function returns the nearest possible sample interval to the requested
210219
sample interval. It does not change the configuration of the oscilloscope.
@@ -213,18 +222,18 @@ def get_nearest_sampling_interval(self, sample_rate:float) -> dict:
213222
increase sample interval.
214223
215224
Args:
216-
sample_rate (float): Time value in seconds (s) you would like to obtain.
225+
interval_s (float): Time value in seconds (s) you would like to obtain.
217226
218227
Returns:
219-
dict: Dictionary of suggested timebase and actual sample interval.
228+
dict: Dictionary of suggested timebase and actual sample interval in seconds (s).
220229
"""
221230
timebase = ctypes.c_uint32()
222231
time_interval = ctypes.c_double()
223232
self._call_attr_function(
224233
'NearestSampleIntervalStateless',
225234
self.handle,
226235
self._get_enabled_channel_flags(),
227-
ctypes.c_double(sample_rate),
236+
ctypes.c_double(interval_s),
228237
self.resolution,
229238
ctypes.byref(timebase),
230239
ctypes.byref(time_interval),
@@ -293,6 +302,39 @@ def _get_timebase_2(self, timebase: int, samples: int, segment:int=0):
293302
return {"Interval(ns)": time_interval_ns.value,
294303
"Samples": max_samples.value}
295304

305+
def sample_rate_to_timebase(self, sample_rate:float, unit=SAMPLE_RATE.MSPS):
306+
"""
307+
Converts sample rate to a PicoScope timebase value based on the
308+
attached PicoScope.
309+
310+
This function will return the closest possible timebase.
311+
Use `get_nearest_sample_interval(interval_s)` to get the full timebase and
312+
actual interval achieved.
313+
314+
Args:
315+
sample_rate (int): Desired sample rate
316+
unit (SAMPLE_RATE): unit of sample rate.
317+
"""
318+
interval_s = 1 / (sample_rate * unit)
319+
320+
return self.get_nearest_sampling_interval(interval_s)["timebase"]
321+
322+
def interval_to_timebase(self, interval:float, unit=TIME_UNIT.S):
323+
"""
324+
Converts a time interval (between samples) into a PicoScope timebase
325+
value based on the attached PicoScope.
326+
327+
This function will return the closest possible timebase.
328+
Use `get_nearest_sample_interval(interval_s)` to get the full timebase and
329+
actual interval achieved.
330+
331+
Args:
332+
interval (float): Desired time interval between samples
333+
unit (TIME_UNIT, optional): Time unit of interval.
334+
"""
335+
interval_s = interval / unit
336+
return self.get_nearest_sampling_interval(interval_s)["timebase"]
337+
296338
def _get_adc_limits(self) -> tuple:
297339
"""
298340
Gets the ADC limits for specified devices.
@@ -442,7 +484,7 @@ def _set_channel(self, channel, range, enabled=True, coupling=COUPLING.DC, offse
442484
)
443485
return self._error_handler(status)
444486

445-
def set_simple_trigger(self, channel, threshold_mv, enable=True, direction=TRIGGER_DIR.RISING, delay=0, auto_trigger_ms=3000):
487+
def set_simple_trigger(self, channel, threshold_mv, enable=True, direction=TRIGGER_DIR.RISING, delay=0, auto_trigger=0):
446488
"""
447489
Sets up a simple trigger from a specified channel and threshold in mV
448490
@@ -452,7 +494,7 @@ def set_simple_trigger(self, channel, threshold_mv, enable=True, direction=TRIGG
452494
enable (bool, optional): Enables or disables the trigger.
453495
direction (TRIGGER_DIR, optional): Trigger direction (e.g., TRIGGER_DIR.RISING, TRIGGER_DIR.FALLING).
454496
delay (int, optional): Delay in samples after the trigger condition is met before starting capture.
455-
auto_trigger_ms (int, optional): Timeout in milliseconds after which data capture proceeds even if no trigger occurs.
497+
auto_trigger (int, optional): Timeout after which data capture proceeds even if no trigger occurs.
456498
"""
457499
threshold_adc = self.mv_to_adc(threshold_mv, self.range[channel])
458500
self._call_attr_function(
@@ -463,7 +505,7 @@ def set_simple_trigger(self, channel, threshold_mv, enable=True, direction=TRIGG
463505
threshold_adc,
464506
direction,
465507
delay,
466-
auto_trigger_ms
508+
auto_trigger
467509
)
468510

469511
def set_data_buffer_for_enabled_channels():
@@ -747,6 +789,21 @@ def set_channel(self, channel:CHANNEL, range:RANGE, enabled=True, coupling:COUPL
747789
super()._set_channel_on(channel, range, coupling, offset, bandwidth)
748790
else:
749791
super()._set_channel_off(channel)
792+
793+
def set_simple_trigger(self, channel, threshold_mv, enable=True, direction=TRIGGER_DIR.RISING, delay=0, auto_trigger_ms=5_000):
794+
"""
795+
Sets up a simple trigger from a specified channel and threshold in mV
796+
797+
Args:
798+
channel (int): The input channel to apply the trigger to.
799+
threshold_mv (float): Trigger threshold level in millivolts.
800+
enable (bool, optional): Enables or disables the trigger.
801+
direction (TRIGGER_DIR, optional): Trigger direction (e.g., TRIGGER_DIR.RISING, TRIGGER_DIR.FALLING).
802+
delay (int, optional): Delay in samples after the trigger condition is met before starting capture.
803+
auto_trigger_ms (int, optional): Timeout in milliseconds after which data capture proceeds even if no trigger occurs.
804+
"""
805+
auto_trigger_us = auto_trigger_ms * 1000
806+
return super().set_simple_trigger(channel, threshold_mv, enable, direction, delay, auto_trigger_us)
750807

751808
def set_data_buffer(self, channel:CHANNEL, samples:int, segment:int=0, datatype:DATA_TYPE=DATA_TYPE.INT16_T,
752809
ratio_mode:RATIO_MODE=RATIO_MODE.RAW, action:ACTION=ACTION.CLEAR_ALL | ACTION.ADD) -> ctypes.Array:
@@ -870,6 +927,20 @@ def set_channel(self, channel, range, enabled=True, coupling=COUPLING.DC, offset
870927
def get_timebase(self, timebase, samples, segment=0):
871928
return super()._get_timebase_2(timebase, samples, segment)
872929

930+
def set_simple_trigger(self, channel, threshold_mv, enable=True, direction=TRIGGER_DIR.RISING, delay=0, auto_trigger_ms=5000):
931+
"""
932+
Sets up a simple trigger from a specified channel and threshold in mV
933+
934+
Args:
935+
channel (int): The input channel to apply the trigger to.
936+
threshold_mv (float): Trigger threshold level in millivolts.
937+
enable (bool, optional): Enables or disables the trigger.
938+
direction (TRIGGER_DIR, optional): Trigger direction (e.g., TRIGGER_DIR.RISING, TRIGGER_DIR.FALLING).
939+
delay (int, optional): Delay in samples after the trigger condition is met before starting capture.
940+
auto_trigger_ms (int, optional): Timeout in milliseconds after which data capture proceeds even if no trigger occurs.
941+
"""
942+
return super().set_simple_trigger(channel, threshold_mv, enable, direction, delay, auto_trigger_ms)
943+
873944
def set_data_buffer(self, channel, samples, segment=0, ratio_mode=0):
874945
return super()._set_data_buffer_ps5000a(channel, samples, segment, ratio_mode)
875946

pypicosdk/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "0.2.22"
1+
VERSION = "0.2.23"

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "pypicosdk"
7-
version = "0.2.22"
7+
version = "0.2.23"
88
description = "Modern Python wrapper for PicoSDK"
99
readme = "README.md"
1010
requires-python = ">=3.9"
@@ -31,3 +31,6 @@ pypicosdk = ["lib/**/*.dll"]
3131

3232
[tool.pytest.ini_options]
3333
testpaths = ["tests"]
34+
35+
[tool.hatch.build.targets.wheel]
36+
packages = ["pypicosdk"]

0 commit comments

Comments
 (0)