YorkUphysLabV2 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- YorkUphysLab/Actuator/Actuator.py +133 -0
- YorkUphysLab/Actuator/__init__.py +0 -0
- YorkUphysLab/GwINSTEK/AFG2000.py +164 -0
- YorkUphysLab/GwINSTEK/GPD3303D.py +218 -0
- YorkUphysLab/GwINSTEK/__init__.py +0 -0
- YorkUphysLab/HVcontrol/HV_control.py +133 -0
- YorkUphysLab/HVcontrol/__init__.py +0 -0
- YorkUphysLab/ScoutScale/ScoutSTX.py +131 -0
- YorkUphysLab/ScoutScale/__init__.py +0 -0
- YorkUphysLab/Tektronix/TBS1000.py +456 -0
- YorkUphysLab/Tektronix/__init__.py +0 -0
- YorkUphysLab/Utility/Utility.py +56 -0
- YorkUphysLab/Utility/__init__.py +0 -0
- YorkUphysLab/__init__.py +1 -0
- YorkUphysLab/__version__.py +1 -0
- example/YorkUphysLab_example.py +50 -0
- yorkuphyslabv2-2.0.0.dist-info/METADATA +43 -0
- yorkuphyslabv2-2.0.0.dist-info/RECORD +21 -0
- yorkuphyslabv2-2.0.0.dist-info/WHEEL +5 -0
- yorkuphyslabv2-2.0.0.dist-info/licenses/LICENSE +21 -0
- yorkuphyslabv2-2.0.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import serial
|
|
2
|
+
import re
|
|
3
|
+
import serial.tools.list_ports
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ScoutSTX:
|
|
9
|
+
def __init__(self, emul=False, keyword='ES', baudrate=9600, timeout=1, port=None) -> None:
|
|
10
|
+
self.port = port
|
|
11
|
+
self.keyword = keyword
|
|
12
|
+
self.timeout = timeout
|
|
13
|
+
self.baudrate = baudrate
|
|
14
|
+
self.inst = None
|
|
15
|
+
self.emul = emul
|
|
16
|
+
self.inst_is_open = False
|
|
17
|
+
if self.emul:
|
|
18
|
+
self.emul_str = "[Emulation mode]:"
|
|
19
|
+
else:
|
|
20
|
+
self.emul_str = ""
|
|
21
|
+
|
|
22
|
+
def connect(self):
|
|
23
|
+
if not self.emul:
|
|
24
|
+
if not self.inst_is_open:
|
|
25
|
+
if self.port:
|
|
26
|
+
self.inst = serial.Serial(self.port, self.baudrate, timeout=self.timeout)
|
|
27
|
+
else:
|
|
28
|
+
self.inst = self.port_search(self.keyword)
|
|
29
|
+
|
|
30
|
+
if self.inst is not None:
|
|
31
|
+
print(f'Connected to ScoutSTX scale.')
|
|
32
|
+
self.inst_is_open = True
|
|
33
|
+
return True
|
|
34
|
+
else:
|
|
35
|
+
print('ScoutSTX Connection failed.')
|
|
36
|
+
return False
|
|
37
|
+
else:
|
|
38
|
+
print('ScoutSTX is already connected.')
|
|
39
|
+
else:
|
|
40
|
+
print(f'{self.emul_str} Connected to ScoutSTX scale.')
|
|
41
|
+
self.inst = 'emulated scale'
|
|
42
|
+
self.inst_is_open = True
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
def port_search(self, keyword):
|
|
46
|
+
print('Searching for the device...')
|
|
47
|
+
ports = serial.tools.list_ports.comports()
|
|
48
|
+
for port, desc, hwid in sorted(ports):
|
|
49
|
+
ser = serial.Serial(port, self.baudrate, timeout=self.timeout)
|
|
50
|
+
ser.write(b'*IDN?\r\n')
|
|
51
|
+
idn = ser.readline().strip().decode('ascii')
|
|
52
|
+
|
|
53
|
+
if keyword in idn:
|
|
54
|
+
print(f'"{keyword}" found in: {port}')
|
|
55
|
+
return ser
|
|
56
|
+
else:
|
|
57
|
+
ser.close()
|
|
58
|
+
|
|
59
|
+
print(f'"{keyword}" is not found on any port')
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def close_connection(self):
|
|
63
|
+
if not self.emul:
|
|
64
|
+
if self.inst is not None and self.inst.is_open:
|
|
65
|
+
self.inst.close()
|
|
66
|
+
print('ScoutSTX Connection closed.')
|
|
67
|
+
else:
|
|
68
|
+
print('No active ScoutSTX connection to close.')
|
|
69
|
+
else:
|
|
70
|
+
if self.inst is not None and self.inst_is_open:
|
|
71
|
+
print(f'{self.emul_str} ScoutSTX Connection closed.')
|
|
72
|
+
self.inst_is_open = False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_connected(self):
|
|
76
|
+
if self.inst is None:
|
|
77
|
+
return False
|
|
78
|
+
if self.emul:
|
|
79
|
+
return self.inst_is_open
|
|
80
|
+
else:
|
|
81
|
+
return self.inst.is_open
|
|
82
|
+
|
|
83
|
+
def read_weight(self):
|
|
84
|
+
# Send command to scale to read weight
|
|
85
|
+
if self.is_connected():
|
|
86
|
+
if not self.emul:
|
|
87
|
+
time.sleep(1) # wait for the scale to settle
|
|
88
|
+
self.inst.write(b'S\r\n')
|
|
89
|
+
response = self.inst.readline().decode().strip()
|
|
90
|
+
# Parse weight from response
|
|
91
|
+
if response.startswith('S'):
|
|
92
|
+
match = re.search(r"[-+]?\d*\.\d+|\d+", response)
|
|
93
|
+
if match:
|
|
94
|
+
weight = float(match.group())
|
|
95
|
+
else:
|
|
96
|
+
print("Error parsing weight from scale's response")
|
|
97
|
+
return None
|
|
98
|
+
else:
|
|
99
|
+
print("Error scale's response")
|
|
100
|
+
return None
|
|
101
|
+
else:
|
|
102
|
+
# Generate random weight
|
|
103
|
+
min_value = 1.0
|
|
104
|
+
max_value = 10.0
|
|
105
|
+
weight = round(random.uniform(min_value, max_value), 2)
|
|
106
|
+
|
|
107
|
+
return weight
|
|
108
|
+
|
|
109
|
+
else:
|
|
110
|
+
print(f'{self.emul_str} ScoutSTX Connection is not established.')
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
#==============================================================================
|
|
114
|
+
|
|
115
|
+
# how to use this class
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
|
|
118
|
+
scale = ScoutSTX(emul=True)
|
|
119
|
+
#scale = ScoutSTX(port='COM7')
|
|
120
|
+
|
|
121
|
+
# connect to the device
|
|
122
|
+
scale.connect()
|
|
123
|
+
|
|
124
|
+
wt = scale.read_weight_time()
|
|
125
|
+
if wt: print(f"Weight: {wt[0]} g, at {wt[1]}")
|
|
126
|
+
|
|
127
|
+
w = scale.read_weight()
|
|
128
|
+
if w: print(f"Weight: {w} g")
|
|
129
|
+
|
|
130
|
+
# close the connection
|
|
131
|
+
scale.close_connection()
|
|
File without changes
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import pyvisa
|
|
2
|
+
import numpy as np
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
#----------------------------------------------
|
|
6
|
+
logging.getLogger().setLevel(logging.INFO)
|
|
7
|
+
|
|
8
|
+
class TBS1000:
|
|
9
|
+
"""
|
|
10
|
+
Represents a Tektronix TBS1000 oscilloscope.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
keyword (str): The keyword used to identify the oscilloscope.
|
|
14
|
+
timeout (int): The timeout value for communication with the oscilloscope.
|
|
15
|
+
encoding (str): The encoding used for communication with the oscilloscope.
|
|
16
|
+
read_termination (str): The read termination character for communication with the oscilloscope.
|
|
17
|
+
write_termination (str): The write termination character for communication with the oscilloscope.
|
|
18
|
+
inst (visa.Resource): The instrument resource representing the connected oscilloscope.
|
|
19
|
+
is_open (bool): Indicates whether the oscilloscope connection is open or closed.
|
|
20
|
+
total_time (flote): total time span of the waveform in seconds
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, keyword='TBS', timeout=10000, encoding = 'latin_1', read_termination = '\n') -> None:
|
|
24
|
+
self.keyword = keyword
|
|
25
|
+
self.timeout = timeout
|
|
26
|
+
self.encoding = encoding
|
|
27
|
+
self.read_termination = read_termination
|
|
28
|
+
self.write_termination = None
|
|
29
|
+
|
|
30
|
+
self.inst = None
|
|
31
|
+
self.is_open = False
|
|
32
|
+
|
|
33
|
+
self.total_time = 0
|
|
34
|
+
self.record = 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
#----------------------------------------------
|
|
38
|
+
def connect(self):
|
|
39
|
+
rm = pyvisa.ResourceManager()
|
|
40
|
+
resources_list = rm.list_resources()
|
|
41
|
+
|
|
42
|
+
for re in resources_list:
|
|
43
|
+
if 'USB' in re:
|
|
44
|
+
dev = rm.open_resource(re)
|
|
45
|
+
if self.keyword in dev.query('*idn?'):
|
|
46
|
+
self.inst = dev
|
|
47
|
+
break
|
|
48
|
+
else:
|
|
49
|
+
dev.close()
|
|
50
|
+
if self.inst:
|
|
51
|
+
logging.info(f"Tektronix TBS scope found: {self.inst.query('*idn?')}")
|
|
52
|
+
self.is_open = True
|
|
53
|
+
self.inst.timeout = self.timeout # ms
|
|
54
|
+
self.inst.encoding = self.encoding
|
|
55
|
+
self.inst.read_termination = self.read_termination
|
|
56
|
+
self.inst.write_termination = self.write_termination
|
|
57
|
+
self.inst.write('*cls') # clear Event Status Register (ESR)
|
|
58
|
+
#self.config()
|
|
59
|
+
return True
|
|
60
|
+
else:
|
|
61
|
+
logging.info('No Tektronix TBS scope was found!')
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
#----------------------------------------------
|
|
65
|
+
def config(self, hscale='5E-3', ch1scale='50E-3', ch2scale='2', trig='CH2'):
|
|
66
|
+
"""
|
|
67
|
+
Configures the Tektronix TBS oscilloscope with the specified settings.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
hscale (str): Horizontal scale (time/division) setting in seconds. Default is '5E-3'.
|
|
71
|
+
ch1scale (str): CH1 vertical scale (voltage/division) setting in seconds. Default is '50E-3'.
|
|
72
|
+
ch2scale (str): CH2 vertical scale (voltage/division) setting in seconds. Default is '2'.
|
|
73
|
+
trig (str): Trigger source setting. Default is 'CH2'. Valid options are CH1 or CH2
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
bool: True if the oscilloscope is successfully configured, False otherwise.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
TypeError: If any of the arguments are not of type str.
|
|
80
|
+
"""
|
|
81
|
+
if not all(isinstance(arg, str) for arg in (hscale, ch1scale, ch2scale, trig)):
|
|
82
|
+
raise TypeError("All arguments must be of type string.")
|
|
83
|
+
|
|
84
|
+
# Rest of the code...
|
|
85
|
+
if self.is_connected():
|
|
86
|
+
self.inst.write('*rst') # reset the instrument to a known state.
|
|
87
|
+
r = self.inst.query('*opc?') # queries the instrument to check if it has completed the previous operation.
|
|
88
|
+
self.inst.write('autoset EXECUTE') # autoset: automatically adjusts the oscilloscope's settings based on the input signal
|
|
89
|
+
r = self.inst.query('*opc?')
|
|
90
|
+
self.inst.write(f'HORIZONTAL:MAIN:SCALE {hscale}') # set horizontal scale (time/division)
|
|
91
|
+
r = self.inst.query('*opc?')
|
|
92
|
+
self.inst.write('CH1:COUPLING AC')
|
|
93
|
+
r = self.inst.query('*opc?')
|
|
94
|
+
self.inst.write(f'CH1:SCALE {ch1scale}') # set ch1 vertical scale (voltage/division)
|
|
95
|
+
r = self.inst.query('*opc?')
|
|
96
|
+
self.inst.write(f'CH2:SCALE {ch2scale}') # set ch2 vertical scale (voltage/division)
|
|
97
|
+
r = self.inst.query('*opc?')
|
|
98
|
+
self.inst.write(f'TRIGGER:MAIN:EDGE:SOURCE {trig}') # set trigger source to channel 2
|
|
99
|
+
r = self.inst.query('*opc?')
|
|
100
|
+
return True
|
|
101
|
+
else:
|
|
102
|
+
logging.info('Tektronix TBS scope is not connected!')
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
#----------------------------------------------
|
|
106
|
+
def close(self):
|
|
107
|
+
if self.is_connected():
|
|
108
|
+
self.inst.close()
|
|
109
|
+
self.is_open = False
|
|
110
|
+
logging.info('Tektronix TBS scope connection is closed.')
|
|
111
|
+
else:
|
|
112
|
+
logging.info('Tektronix TBS scope is not connected!')
|
|
113
|
+
|
|
114
|
+
#----------------------------------------------
|
|
115
|
+
def is_connected(self):
|
|
116
|
+
return self.is_open
|
|
117
|
+
|
|
118
|
+
#----------------------------------------------
|
|
119
|
+
def get_idn(self):
|
|
120
|
+
if self.is_connected():
|
|
121
|
+
return self.inst.query('*idn?')
|
|
122
|
+
else:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
#----------------------------------------------
|
|
126
|
+
def get_period(self, channel):
|
|
127
|
+
"""
|
|
128
|
+
Get the period of the waveform on the specified channel.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
channel (int): The channel number (1 or 2).
|
|
132
|
+
Returns:
|
|
133
|
+
float: The measured period in seconds.
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If the channel number is not 1 or 2.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
if channel not in [1,2]:
|
|
139
|
+
raise ValueError("Channel must be 1 or 2")
|
|
140
|
+
|
|
141
|
+
self.inst.write('MEASUrement:IMMed:TYPE PERiod')
|
|
142
|
+
self.inst.write(f'MEASUrement:IMMed:SOUrce CH{channel}')
|
|
143
|
+
measured_period = self.inst.query('MEASUrement:IMMed:VALue?')
|
|
144
|
+
|
|
145
|
+
return float(measured_period) # in seconds
|
|
146
|
+
|
|
147
|
+
#----------------------------------------------
|
|
148
|
+
def get_frequency(self, channel):
|
|
149
|
+
"""
|
|
150
|
+
Get the frequency of the waveform on the specified channel.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
channel (int): The channel number (1 or 2).
|
|
154
|
+
Returns:
|
|
155
|
+
float: The measured frequency in Hz.
|
|
156
|
+
Raises:
|
|
157
|
+
ValueError: If the channel number is not 1 or 2.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
if channel not in [1,2]:
|
|
161
|
+
raise ValueError("Channel must be 1 or 2")
|
|
162
|
+
|
|
163
|
+
self.inst.write('MEASUrement:IMMed:TYPE FREQUENCY')
|
|
164
|
+
self.inst.write(f'MEASUrement:IMMed:SOUrce CH{channel}')
|
|
165
|
+
measured_freq = self.inst.query('MEASUrement:IMMed:VALue?')
|
|
166
|
+
|
|
167
|
+
return float(measured_freq) # in seconds
|
|
168
|
+
|
|
169
|
+
def get_amplitude(self, channel):
|
|
170
|
+
"""
|
|
171
|
+
Get the amplitude of the waveform on the specified channel.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
channel (int): The channel number (1 or 2).
|
|
175
|
+
Returns:
|
|
176
|
+
float: The measured amplitude in V.
|
|
177
|
+
Raises:
|
|
178
|
+
ValueError: If the channel number is not 1 or 2.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
if channel not in [1,2]:
|
|
182
|
+
raise ValueError("Channel must be 1 or 2")
|
|
183
|
+
|
|
184
|
+
self.inst.write('MEASUrement:IMMed:TYPE AMplitude')
|
|
185
|
+
self.inst.write(f'MEASUrement:IMMed:SOUrce CH{channel}')
|
|
186
|
+
measured_amplitude = self.inst.query('MEASUrement:IMMed:VALue?')
|
|
187
|
+
|
|
188
|
+
return float(measured_amplitude) # in seconds
|
|
189
|
+
|
|
190
|
+
def get_phase(self, channel):
|
|
191
|
+
"""
|
|
192
|
+
Get the phase difference from the selected waveform to the designated waveform
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
channel (int): The channel number (1 or 2).
|
|
196
|
+
Returns:
|
|
197
|
+
float: The measured phase in degrees.
|
|
198
|
+
Raises:
|
|
199
|
+
ValueError: If the channel number is not 1 or 2.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
if channel not in [1,2]:
|
|
203
|
+
raise ValueError("Channel must be 1 or 2")
|
|
204
|
+
|
|
205
|
+
self.inst.write(f'MEASUrement:IMMed:SOUrce1 CH1')
|
|
206
|
+
self.inst.write(f'MEASUrement:IMMed:SOUrce2 CH2')
|
|
207
|
+
self.inst.write('MEASUrement:IMMed:TYPE PHAse')
|
|
208
|
+
measured_phase = self.inst.query('MEASUrement:IMMed:VALue?')
|
|
209
|
+
|
|
210
|
+
if channel == 1:
|
|
211
|
+
return float(measured_phase) # in degrees
|
|
212
|
+
else:
|
|
213
|
+
return -1*float(measured_phase) # in degrees
|
|
214
|
+
#----------------------------------------------
|
|
215
|
+
def get_data(self, channel):
|
|
216
|
+
"""
|
|
217
|
+
Retrieves the waveform data from the oscilloscope for the specified channel.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
channel (int): The channel number (1 or 2) for which to retrieve the data.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
A two-dimentional list(array) of the waveform containing the following elements:
|
|
224
|
+
waveform[0]: An array of scaled time values in milliseconds, i.e., the x-axis values
|
|
225
|
+
waveform[1]: An array of scaled amplitude values in volts, i.e., the y-axis values
|
|
226
|
+
"""
|
|
227
|
+
if not self.is_connected():
|
|
228
|
+
return None
|
|
229
|
+
if channel not in [1, 2]:
|
|
230
|
+
raise ValueError("Channel must be 1 or 2")
|
|
231
|
+
|
|
232
|
+
# io config
|
|
233
|
+
self.inst.write('header 0')
|
|
234
|
+
self.inst.write('data:encdg RIBINARY')
|
|
235
|
+
self.inst.write(f'data:source CH{channel}')
|
|
236
|
+
self.inst.write('data:start 1') # first sample
|
|
237
|
+
self.record = int(self.inst.query('wfmpre:nr_pt?')) # number of samples
|
|
238
|
+
self.inst.write(f'data:stop {self.record}') # last sample
|
|
239
|
+
self.inst.write('wfmpre:byt_nr 1') # 1 byte per sample
|
|
240
|
+
# acq config
|
|
241
|
+
self.inst.write('acquire:state 0') # stop data acquisition
|
|
242
|
+
self.inst.write('acquire:stopafter SEQUENCE') # sets the acquisition mode to 'SEQUENCE': acquires a single waveform and then stops
|
|
243
|
+
self.inst.write('acquire:state 1') # run
|
|
244
|
+
|
|
245
|
+
# data query
|
|
246
|
+
bin_wave = self.inst.query_binary_values('curve?', datatype='b', container=np.array)
|
|
247
|
+
tscale = float(self.inst.query('wfmpre:xincr?')) # retrieve scaling factors
|
|
248
|
+
tstart = float(self.inst.query('wfmpre:xzero?'))
|
|
249
|
+
vscale = float(self.inst.query('wfmpre:ymult?')) # volts / level
|
|
250
|
+
voff = float(self.inst.query('wfmpre:yzero?')) # reference voltage
|
|
251
|
+
vpos = float(self.inst.query('wfmpre:yoff?')) # reference position (level)
|
|
252
|
+
|
|
253
|
+
# error checking
|
|
254
|
+
r = int(self.inst.query('*esr?'))
|
|
255
|
+
if r != 0b00000000:
|
|
256
|
+
logging.info('event status register: 0b{:08b}'.format(r))
|
|
257
|
+
r = self.inst.query('allev?').strip()
|
|
258
|
+
if 'No events' not in r:
|
|
259
|
+
logging.info(f'all event messages: {r}')
|
|
260
|
+
|
|
261
|
+
total_time = tscale * self.record # create scaled vectors
|
|
262
|
+
tstop = tstart + total_time
|
|
263
|
+
scaled_time = np.linspace(tstart, tstop, num=self.record, endpoint=False) * 1000 # time in ms
|
|
264
|
+
|
|
265
|
+
unscaled_amp = np.array(bin_wave, dtype='double') # data type conversion
|
|
266
|
+
scaled_amp = (unscaled_amp - vpos) * vscale + voff
|
|
267
|
+
|
|
268
|
+
self.total_time = total_time
|
|
269
|
+
|
|
270
|
+
waveform = np.zeros((2,len(scaled_amp)))
|
|
271
|
+
waveform[0] = scaled_time
|
|
272
|
+
waveform[1] = scaled_amp
|
|
273
|
+
|
|
274
|
+
return waveform
|
|
275
|
+
|
|
276
|
+
#----------------------------------------------
|
|
277
|
+
def get_data2(self):
|
|
278
|
+
"""
|
|
279
|
+
Retrieves waveforms data of both channels of the Tektronix TBS1000 oscilloscope, simultaneously.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
A two-dimentional list(array) of the waveform, for each channel, containing the following elements:
|
|
283
|
+
waveform[0]: An array of scaled time values in milliseconds, i.e., the x-axis values
|
|
284
|
+
waveform[1]: An array of scaled amplitude values in volts, i.e., the y-axis values
|
|
285
|
+
|
|
286
|
+
the returned values are in the following order: (waveform_1, waveform_2)
|
|
287
|
+
"""
|
|
288
|
+
if not self.is_connected():
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
scaled_amp = []
|
|
292
|
+
|
|
293
|
+
# acq config
|
|
294
|
+
self.inst.write('acquire:state RUN') # RUN acquisition
|
|
295
|
+
self.inst.write('acquire:stopafter SEQUENCE') # sets the acquisition mode to 'SEQUENCE': acquires a single waveform and then stops
|
|
296
|
+
self.inst.write('acquire:state 0') # stop data acquisition
|
|
297
|
+
|
|
298
|
+
# io config
|
|
299
|
+
self.inst.write('header 0')
|
|
300
|
+
self.inst.write('data:encdg RIBINARY')
|
|
301
|
+
|
|
302
|
+
for channel in [1,2]:
|
|
303
|
+
# ch1
|
|
304
|
+
self.inst.write(f'data:source CH{channel}')
|
|
305
|
+
self.inst.write('data:start 1') # first sample
|
|
306
|
+
self.record = int(self.inst.query('wfmpre:nr_pt?')) # number of samples
|
|
307
|
+
self.inst.write(f'data:stop {self.record}') # last sample
|
|
308
|
+
self.inst.write('wfmpre:byt_nr 1') # 1 byte per sample
|
|
309
|
+
|
|
310
|
+
# data query
|
|
311
|
+
bin_wave = self.inst.query_binary_values('curve?', datatype='b', container=np.array)
|
|
312
|
+
tscale = float(self.inst.query('wfmpre:xincr?')) # retrieve scaling factors
|
|
313
|
+
tstart = float(self.inst.query('wfmpre:xzero?'))
|
|
314
|
+
vscale = float(self.inst.query('wfmpre:ymult?')) # volts / level
|
|
315
|
+
voff = float(self.inst.query('wfmpre:yzero?')) # reference voltage
|
|
316
|
+
vpos = float(self.inst.query('wfmpre:yoff?')) # reference position (level)
|
|
317
|
+
|
|
318
|
+
# error checking
|
|
319
|
+
r = int(self.inst.query('*esr?'))
|
|
320
|
+
if r != 0b00000000:
|
|
321
|
+
logging.info('event status register: 0b{:08b}'.format(r))
|
|
322
|
+
r = self.inst.query('allev?').strip()
|
|
323
|
+
if 'No events' not in r:
|
|
324
|
+
logging.info(f'all event messages: {r}')
|
|
325
|
+
|
|
326
|
+
total_time = tscale * self.record # create scaled vectors
|
|
327
|
+
tstop = tstart + total_time
|
|
328
|
+
scaled_time = np.linspace(tstart, tstop, num=self.record, endpoint=False) * 1000 # time in ms
|
|
329
|
+
|
|
330
|
+
unscaled_amp = np.array(bin_wave, dtype='double') # data type conversion
|
|
331
|
+
scaled_amp.append((unscaled_amp - vpos) * vscale + voff)
|
|
332
|
+
|
|
333
|
+
self.total_time = total_time
|
|
334
|
+
|
|
335
|
+
waveform_1 = np.zeros((2,len(scaled_amp[0])))
|
|
336
|
+
waveform_2 = np.zeros((2,len(scaled_amp[1])))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
waveform_1[0] = scaled_time
|
|
340
|
+
waveform_1[1] = scaled_amp[0]
|
|
341
|
+
waveform_2[0] = scaled_time
|
|
342
|
+
waveform_2[1] = scaled_amp[1]
|
|
343
|
+
|
|
344
|
+
self.inst.write('acquire:state RUN') # RUN acquisition
|
|
345
|
+
|
|
346
|
+
return waveform_1, waveform_2
|
|
347
|
+
#----------------------------------------------
|
|
348
|
+
def shift_phase(self, scaled_time, waveform_1, waveform_2, phi):
|
|
349
|
+
"""
|
|
350
|
+
Apply phase shift to waveform_2 based on the given phi value. To keep the length of the waveforms the same, the shifted waveforms and scaled_time are truncated.
|
|
351
|
+
|
|
352
|
+
Parameters:
|
|
353
|
+
scaled_time (array-like): Array of scaled time values.
|
|
354
|
+
waveform_1 (array-like): Array of waveform 1 values.
|
|
355
|
+
waveform_2 (array-like): Array of waveform 2 values.
|
|
356
|
+
phi (float): Phase shift value in degrees.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
tuple: A tuple containing the shifted scaled_time, waveform_1, and shifted waveform_2.
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
_, phase = divmod(phi, 360)
|
|
363
|
+
# get the period of waveform 2
|
|
364
|
+
period = self.get_period(channel=2)
|
|
365
|
+
samples_in_period = int(period/self.total_time * len(waveform_2)) # number of samples in one period
|
|
366
|
+
shift_samples = int((phase / 360) * samples_in_period) # number of samples to shift
|
|
367
|
+
|
|
368
|
+
if shift_samples == 0:
|
|
369
|
+
return scaled_time, waveform_1, waveform_2
|
|
370
|
+
else:
|
|
371
|
+
return scaled_time[:-1*shift_samples], waveform_1[:-1*shift_samples], waveform_2[shift_samples:]
|
|
372
|
+
|
|
373
|
+
#----------------------------------------------
|
|
374
|
+
def shift_phase2(self, ref_waveform, ref_channel, phi_shift):
|
|
375
|
+
"""
|
|
376
|
+
Constructing an internal reference waveform with the given phase shift w.r.t the main waveform based on the given parameters.
|
|
377
|
+
|
|
378
|
+
Parameters:
|
|
379
|
+
ref_waveform (float): The oscilloscope's readout of the rwaveform (2-dimentional list) of the reference channel
|
|
380
|
+
ref_channel (str): The reference channel for obtaining the frequency
|
|
381
|
+
phi_shift (float): The phase shift value in degrees.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
A two-dimentional list(array), with the first row element the time values and the second element being the constructed amplitude values with the shifted phase.
|
|
385
|
+
"""
|
|
386
|
+
internal_ref_waveform = np.zeros((2,len(ref_waveform)))
|
|
387
|
+
t = ref_waveform[0]
|
|
388
|
+
freq = self.get_frequency(ref_channel)
|
|
389
|
+
ampl = self.get_amplitude(ref_channel)
|
|
390
|
+
|
|
391
|
+
# constructing a pure analytical internal ref. wave
|
|
392
|
+
internal_ref_waveform[0] = t
|
|
393
|
+
internal_ref_waveform[1] = ampl*np.cos(2*np.pi*freq*t*0.001 + np.radians(phi_shift))
|
|
394
|
+
|
|
395
|
+
return internal_ref_waveform
|
|
396
|
+
|
|
397
|
+
#==============================================================================
|
|
398
|
+
# how to use this class
|
|
399
|
+
if __name__ == '__main__':
|
|
400
|
+
# create a scope object
|
|
401
|
+
scope = TBS1000()
|
|
402
|
+
# connect to the scope
|
|
403
|
+
if scope.connect():
|
|
404
|
+
scope.config(hscale='5E-3', ch1scale='50E-3', ch2scale='2', trig='CH2')
|
|
405
|
+
|
|
406
|
+
color = {1:'orange', 2:'blue', 'mix':'red'}
|
|
407
|
+
|
|
408
|
+
#"""
|
|
409
|
+
try:
|
|
410
|
+
# get data from the scope
|
|
411
|
+
scaled_time, scaled_wave_1, scaled_wave_2 = scope.get_data2()
|
|
412
|
+
|
|
413
|
+
phi = 0
|
|
414
|
+
#stime, wf1, wf2_shifted = scope.shift_phase(scaled_time, scaled_wave_1, scaled_wave_2, phi)
|
|
415
|
+
wf2_shifted = scope.shift_phase2(scaled_time, ref_channel=2, phi_shift = 330)
|
|
416
|
+
|
|
417
|
+
mix_wf = [x*y for x,y in zip(scaled_wave_1,wf2_shifted)]
|
|
418
|
+
|
|
419
|
+
avg_mix_wf = np.average(mix_wf)*1000 # in mV
|
|
420
|
+
|
|
421
|
+
# --plotting
|
|
422
|
+
#'''
|
|
423
|
+
import pylab as pl
|
|
424
|
+
#pl.plot(scaled_time, scaled_wave_1, label=f'Ch 1', color=color[1])
|
|
425
|
+
#y_max = max(scaled_wave_1)
|
|
426
|
+
|
|
427
|
+
pl.plot(scaled_time, scaled_wave_1, label=f'Ch 1', color=color[1])
|
|
428
|
+
pl.plot(scaled_time, wf2_shifted, label=f'Ch 2', color=color[2])
|
|
429
|
+
#pl.plot(stime, mix_wf, label=f'Mix', color=color['mix'])
|
|
430
|
+
|
|
431
|
+
y_max = max(max(scaled_wave_1), max(wf2_shifted))
|
|
432
|
+
#y_max = max(max(wf1), max(wf2_shifted), max(mix_wf))
|
|
433
|
+
|
|
434
|
+
pl.ylim(top=y_max*1.5)
|
|
435
|
+
pl.xlabel('time [ms]') # x label
|
|
436
|
+
pl.ylabel('voltage [v]') # y label
|
|
437
|
+
# Add legend
|
|
438
|
+
pl.legend(loc='upper right')
|
|
439
|
+
|
|
440
|
+
pl.rc('grid', linestyle=':', color='gray', linewidth=1)
|
|
441
|
+
pl.grid(True)
|
|
442
|
+
pl.title(f'Lock-in Output: {round(avg_mix_wf,2)} mV, $\Delta\phi: {phi}\degree$', fontsize = 10)
|
|
443
|
+
#'''
|
|
444
|
+
|
|
445
|
+
except ValueError as e:
|
|
446
|
+
print(e)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
scope.close()
|
|
450
|
+
|
|
451
|
+
print("\nlook for plot window...")
|
|
452
|
+
pl.show()
|
|
453
|
+
print("\nend of demonstration")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
scope.close()
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import csv
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
#from YorkUphysLab.Actuator import Actuator as ACT
|
|
6
|
+
#from YorkUphysLab.HVcontrol import HV_control as HV
|
|
7
|
+
#----------------------------------------------
|
|
8
|
+
|
|
9
|
+
logging.getLogger().setLevel(logging.INFO)
|
|
10
|
+
|
|
11
|
+
def write_data_to_csv(data, path, filename):
|
|
12
|
+
header = ['Position', 'Weight']
|
|
13
|
+
|
|
14
|
+
# Create the full path for the CSV file on the desktop
|
|
15
|
+
file_path = os.path.join(path, filename)
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
with open(file_path, 'w', newline='') as csvfile:
|
|
19
|
+
csv_writer = csv.writer(csvfile)
|
|
20
|
+
csv_writer.writerow(header)
|
|
21
|
+
|
|
22
|
+
for position, weight in data:
|
|
23
|
+
csv_writer.writerow([position, weight])
|
|
24
|
+
print(f"Data written to '{file_path}' successfully!")
|
|
25
|
+
return True
|
|
26
|
+
except Exception as e:
|
|
27
|
+
print(f"An error occurred: {e}")
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
#----------------------------------------------
|
|
31
|
+
def reset_devices(actuator, hv):
|
|
32
|
+
if not actuator.actuator_on:
|
|
33
|
+
actuator.switch_on()
|
|
34
|
+
actuator.set_position(0)
|
|
35
|
+
|
|
36
|
+
if not hv.HV_on:
|
|
37
|
+
hv.switch_on()
|
|
38
|
+
hv.set_hv(0)
|
|
39
|
+
|
|
40
|
+
#----------------------------------------------
|
|
41
|
+
def multiply_lists(list_1, list_2):
|
|
42
|
+
"""
|
|
43
|
+
Multiplies (mixes) two waveforms element-wise.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
list_1 (list)
|
|
47
|
+
list_2 (list)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
list: The resulting list after element-wise multiplication.
|
|
51
|
+
"""
|
|
52
|
+
if len(list_1) != len(list_2):
|
|
53
|
+
logging.error(f"Lists must be the same length: {len(list_1)} != {len(list_2)}")
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
return [x*y for x,y in zip(list_1,list_2)]
|
|
File without changes
|
YorkUphysLab/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . __version__ import __version__
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.0.27'
|