eaps2000 0.1.4__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.
- eaps2000-0.1.4.dist-info/METADATA +99 -0
- eaps2000-0.1.4.dist-info/RECORD +5 -0
- eaps2000-0.1.4.dist-info/WHEEL +4 -0
- eaps2000-0.1.4.dist-info/entry_points.txt +2 -0
- eaps2000.py +630 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eaps2000
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Elektro-Automatik Series PS 2000 Python Controller
|
|
5
|
+
Project-URL: Homepage, https://github.com/KozhinovAlexander/eaps2000
|
|
6
|
+
Project-URL: Documentation, https://github.com/KozhinovAlexander/eaps2000/blob/main/README.md
|
|
7
|
+
Author-email: Alexander Kozhinov <ak.alexander.kozhinov@gmail.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: ds-power-supply,ea-ps-2042-06b,ea-ps-2042-10b,ea-ps-2042-20b,ea-ps-2084-03b,ea-ps-2084-05b,ea-ps-2342-06b,ea-ps-2342-10b,ea-ps-2384-05b,psu,psu-controller,pypi-package,python-psu-controller,series-ps-2000-b
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Requires-Dist: pyserial>=3.5
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: flake8>=7.1.2; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest-flake8>=1.3.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=6.2.2; extra == 'dev'
|
|
18
|
+
Requires-Dist: versioningit>=3.1.2; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
- [eaps2000 - PS 2000B Series PSU Python Control Unit](#eaps2000---ps-2000b-series-psu-python-control-unit)
|
|
22
|
+
- [Installing the package](#installing-the-package)
|
|
23
|
+
- [Getting Started](#getting-started)
|
|
24
|
+
- [Building the Project](#building-the-project)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# eaps2000 - PS 2000B Series PSU Python Control Unit
|
|
28
|
+
|
|
29
|
+
The `eaps2000` is a python module for [Elektro-Automatik PS 2000B Series][_ps_2kb_url_] PSU control.
|
|
30
|
+
|
|
31
|
+
This software implements following functionality:
|
|
32
|
+
|
|
33
|
+
- Reading out device information (serial number, model etc.)
|
|
34
|
+
- Setting ovewr-voltage and over-current protection
|
|
35
|
+
- Setting voltage and current for an output
|
|
36
|
+
- Controlling the output stage on/off
|
|
37
|
+
- Acknowledging alarms
|
|
38
|
+
|
|
39
|
+
## Installing the package
|
|
40
|
+
|
|
41
|
+
Install the project from PyPi or [build](#building-the-project) it first.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install eaps2000
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Getting Started
|
|
48
|
+
|
|
49
|
+
Using CLI interface:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
eaps2000 --help
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Using Python interface:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from eaps2000 import eaps2k
|
|
59
|
+
|
|
60
|
+
port = 'COM123' # use /tty/ACM0 for linux based system
|
|
61
|
+
with eaps2k(port) as ps:
|
|
62
|
+
# Prepare config:
|
|
63
|
+
cfg = eaps2k.get_config_template()
|
|
64
|
+
cfg['ACK'] = True # acknowledge alarms if any
|
|
65
|
+
cfg['OVP'] = 5.0 # over-voltage-protection value
|
|
66
|
+
cfg['OCP'] = 0.5 # over-current-protection value
|
|
67
|
+
cfg['Iset'] = 0.1 # current to be set
|
|
68
|
+
cfg['Vset'] = 3.3 # coltage to be set
|
|
69
|
+
|
|
70
|
+
# Turn off the output stage:
|
|
71
|
+
ps.set_output_state(False)
|
|
72
|
+
|
|
73
|
+
# Apply configuration:
|
|
74
|
+
ps.configure(cfg)
|
|
75
|
+
|
|
76
|
+
# Turn on the output stage:
|
|
77
|
+
# ATTENTION: The power will be applied to your probe here!
|
|
78
|
+
# ps.set_output_state(True)
|
|
79
|
+
|
|
80
|
+
# Show information:
|
|
81
|
+
ps.print_info()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Building the Project
|
|
85
|
+
|
|
86
|
+
The project is built with [`hatchling`][_hatchling_home_]
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install hatchling && flake8 . -v && hatchling build && pytest --flake8
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Installing freshly built project may be done by invoking:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install ./dist/eaps2000-*.whl --upgrade --force-reinstall
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
[_ps_2kb_url_]: https://elektroautomatik.com/shop/de/produkte/programmierbare-dc-laborstromversorgungen/dc-laborstromversorgungen/serie-ps-2000-b-br-100-bis-332-w/
|
|
99
|
+
[_hatchling_home_]: https://hatch.pypa.io/1.9/
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
eaps2000.py,sha256=uniBkyTgJd8X9UXa1mQOqZTiQ2TMJs97vUVVhRrketw,23511
|
|
2
|
+
eaps2000-0.1.4.dist-info/METADATA,sha256=dMP_C3eNMPxDwBVgYRuEfFP99uCCvNVf5SktqH8NXbY,3145
|
|
3
|
+
eaps2000-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
4
|
+
eaps2000-0.1.4.dist-info/entry_points.txt,sha256=JrFCoI1FpNHhV7IHgfpOi5kJYD03LECodYZx2DLjCnk,43
|
|
5
|
+
eaps2000-0.1.4.dist-info/RECORD,,
|
eaps2000.py
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
'''
|
|
4
|
+
Elektro-Automatik Series PS 2000 Python Controller
|
|
5
|
+
|
|
6
|
+
License: Apache 2.0
|
|
7
|
+
|
|
8
|
+
Disclaimer: This is NOT an official software made by the manufacturer.
|
|
9
|
+
|
|
10
|
+
Description: The Elektro-Automatik Series PS 2000 Python Controller
|
|
11
|
+
is a versatile software designed to interface with the Elektro-Automatik
|
|
12
|
+
PS 2000 series power supplies.
|
|
13
|
+
Developed with Python, this controller allows users to automate and
|
|
14
|
+
manage their power supply operations efficiently.
|
|
15
|
+
|
|
16
|
+
IMPORTANT NOTICE: This software is provided as-is. There is NO WARRANTY,
|
|
17
|
+
NO GUARANTEE of performance, and it is NOT officially endorsed by the manufacturer.
|
|
18
|
+
Use at your own risk.
|
|
19
|
+
|
|
20
|
+
Author: Alexander Kozhinov <ak.alexander.kozhinov@gmail.com>
|
|
21
|
+
'''
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import serial
|
|
25
|
+
import struct
|
|
26
|
+
import sys
|
|
27
|
+
from importlib.metadata import version
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class eaps2k(object):
|
|
31
|
+
PS_QUERY = 0x40
|
|
32
|
+
PS_SEND = 0xc0
|
|
33
|
+
|
|
34
|
+
def __init__(self, port: str, timeout: float = 0.06, baudrate: int = 115200,
|
|
35
|
+
parity: str = serial.PARITY_ODD, verbosity_level=0):
|
|
36
|
+
'''
|
|
37
|
+
Initialize the PS2000 device with the specified serial port settings.
|
|
38
|
+
Args:
|
|
39
|
+
port (str): The serial port to which the PS2000 device is connected.
|
|
40
|
+
timeout (float, optional): The timeout for serial communication in seconds. Default is 0.06.
|
|
41
|
+
baudrate (int, optional): The baud rate for serial communication. Default is 115200.
|
|
42
|
+
parity (str, optional): The parity bit setting for serial communication. Default is serial.PARITY_ODD.
|
|
43
|
+
verbosity_level (int, optional): The verbosity level for logging. Use 3 to see more information. Default is 0.
|
|
44
|
+
Attributes:
|
|
45
|
+
_verbose (int): Stores the verbosity level for logging.
|
|
46
|
+
ser_dev (serial.Serial): The serial device object for communication with the PS2000.
|
|
47
|
+
_u_nom (float): The nominal voltage of the PS2000 device.
|
|
48
|
+
_i_nom (float): The nominal current of the PS2000 device.
|
|
49
|
+
'''
|
|
50
|
+
self._verbosity_lvl = verbosity_level
|
|
51
|
+
# set timeout to 0.06s to guarantee minimum interval time of 50ms
|
|
52
|
+
self.ser_dev = serial.Serial(port, timeout=timeout, baudrate=baudrate,
|
|
53
|
+
parity=parity)
|
|
54
|
+
self._translation_factor = 25600.0
|
|
55
|
+
self._u_nom = self.get_nominal_voltage()
|
|
56
|
+
self._i_nom = self.get_nominal_current()
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def pkg_version() -> str:
|
|
60
|
+
'''
|
|
61
|
+
Returns this packgae version
|
|
62
|
+
'''
|
|
63
|
+
ver = '0.0.0'
|
|
64
|
+
try:
|
|
65
|
+
ver = version(__name__)
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
return ver
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def description():
|
|
72
|
+
'''
|
|
73
|
+
Returns description of this module.
|
|
74
|
+
'''
|
|
75
|
+
descr = \
|
|
76
|
+
f'{__name__} {eaps2k.pkg_version()}. ' \
|
|
77
|
+
'\nElektro-Automatik Series PS 2000 Python Controller. ' \
|
|
78
|
+
'\nLicense: Apache 2.0 ' \
|
|
79
|
+
'\nDisclaimer: This is NOT an official software made by the manufacturer. ' \
|
|
80
|
+
'\nIMPORTANT NOTICE: This software is provided as-is. There is NO WARRANTY,' \
|
|
81
|
+
'\nNO GUARANTEE of performance, and it is NOT officially endorsed by the manufacturer. ' \
|
|
82
|
+
'\nUse at your own risk. ' \
|
|
83
|
+
'\nAuthor: Alexander Kozhinov <ak.alexander.kozhinov@gmail.com>'
|
|
84
|
+
return descr
|
|
85
|
+
|
|
86
|
+
def __enter__(self):
|
|
87
|
+
self.set_remote(True)
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
91
|
+
self.set_remote(False)
|
|
92
|
+
self.ser_dev.close()
|
|
93
|
+
if exc_type is not None:
|
|
94
|
+
print(f'An exception occurred: {exc_value}\nTraceback: {traceback}')
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _construct_telegram(telegram_type, node, obj, data) -> bytearray:
|
|
98
|
+
'''
|
|
99
|
+
Constructs a telegram message for communication.
|
|
100
|
+
Args:
|
|
101
|
+
telegram_type (int): The type of the telegram.
|
|
102
|
+
node (int): The device node.
|
|
103
|
+
obj (int): The object identifier.
|
|
104
|
+
data (bytes): The data to be included in the telegram.
|
|
105
|
+
Returns:
|
|
106
|
+
bytearray: The constructed telegram message.
|
|
107
|
+
The telegram message format is as follows:
|
|
108
|
+
- Start delimiter (SD): 0x30 + telegram_type
|
|
109
|
+
- Device node (DN): node
|
|
110
|
+
- Object (OBJ): obj
|
|
111
|
+
- Data (DATA): data (if any)
|
|
112
|
+
- Checksum (CS): sum of all previous bytes, split into two bytes (CS0 and CS1)
|
|
113
|
+
'''
|
|
114
|
+
telegram = bytearray()
|
|
115
|
+
telegram.append(0x30 + telegram_type) # SD (start delimiter)
|
|
116
|
+
telegram.append(node) # DN (device node)
|
|
117
|
+
telegram.append(obj) # OBJ (object)
|
|
118
|
+
if len(data) > 0: # DATA
|
|
119
|
+
telegram.extend(data)
|
|
120
|
+
telegram[0] += len(data) - 1 # update length
|
|
121
|
+
|
|
122
|
+
cs = sum(telegram)
|
|
123
|
+
telegram.append(cs >> 8) # CS0
|
|
124
|
+
telegram.append(cs & 0xff) # CS1 (checksum)
|
|
125
|
+
|
|
126
|
+
return telegram
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _checksum_verify(ans):
|
|
130
|
+
'''
|
|
131
|
+
Compare checksum with header and data in response from device.
|
|
132
|
+
This function calculates the checksum of the given response data and
|
|
133
|
+
compares it with the checksum provided in the last two bytes of the response.
|
|
134
|
+
If the calculated checksum does not match the provided checksum, an assertion
|
|
135
|
+
error is raised indicating a checksum mismatch.
|
|
136
|
+
Args:
|
|
137
|
+
ans (list or bytearray): The response data from the device, including the
|
|
138
|
+
checksum in the last two bytes.
|
|
139
|
+
Raises:
|
|
140
|
+
AssertionError: If the calculated checksum does not match the provided checksum.
|
|
141
|
+
'''
|
|
142
|
+
cs = sum(ans[0:-2])
|
|
143
|
+
assert ans[-2] == (cs >> 8) and ans[-1] == (cs & 0xff), \
|
|
144
|
+
'ERROR: Checksum mismatch'
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _check_error(ans):
|
|
148
|
+
'''
|
|
149
|
+
Checks for errors in the given answer and raises an assertion error with
|
|
150
|
+
a detailed message if an error is found.
|
|
151
|
+
Args:
|
|
152
|
+
ans (list of int): The answer to check, represented as a list of integers.
|
|
153
|
+
Raises:
|
|
154
|
+
AssertionError: If an error is found, with a message detailing the
|
|
155
|
+
error type and the answer in hexadecimal format.
|
|
156
|
+
'''
|
|
157
|
+
if ans[2] != 0xff:
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
response_state_table = {
|
|
161
|
+
0x00: 'OK: Acknowledge', # got acknowledge - not an error
|
|
162
|
+
0x03: 'ERROR: Com: Checksum incorrect',
|
|
163
|
+
0x04: 'ERROR: Com: Start delimiter incorrect',
|
|
164
|
+
0x05: 'ERROR: Com: Wrong address for output',
|
|
165
|
+
0x07: 'ERROR: Com: Object not defined',
|
|
166
|
+
0x08: 'ERROR: Usr: Object length incorrect',
|
|
167
|
+
0x09: 'ERROR: Usr: Access denied',
|
|
168
|
+
0x0f: 'ERROR: Usr: Device is locked',
|
|
169
|
+
0x30: 'ERROR: Usr: Upper limit exceeded',
|
|
170
|
+
0x31: 'ERROR: Usr: Lower limit exceeded',
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
resp_str = 'unknown error'
|
|
174
|
+
if ans[3] in response_state_table.keys():
|
|
175
|
+
resp_str = response_state_table[ans[3]]
|
|
176
|
+
assert ans[3] == 0x00, f'{resp_str}\n-- answer:\t\t{eaps2k.bytes2hex(ans)}'
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def bytes2hex(bytes_arr):
|
|
180
|
+
'''
|
|
181
|
+
Converts a byte array to a string of hexadecimal values.
|
|
182
|
+
Args:
|
|
183
|
+
bytes_arr (bytes): The byte array to convert.
|
|
184
|
+
Returns:
|
|
185
|
+
str: A string of hexadecimal values separated by spaces.
|
|
186
|
+
'''
|
|
187
|
+
return ' '.join(hex(b) for b in bytes_arr)
|
|
188
|
+
|
|
189
|
+
def _transfer(self, telegram_type, node, obj, data,
|
|
190
|
+
read_buff_len: int = 100) -> bytes:
|
|
191
|
+
'''
|
|
192
|
+
Transfers data to and from a serial device.
|
|
193
|
+
This method constructs a telegram based on the provided telegram_type, node, object, and data,
|
|
194
|
+
sends it to the serial device, and reads the response. It also performs verbosity-based
|
|
195
|
+
logging, checks the response length, and validates the checksum and error status.
|
|
196
|
+
Args:
|
|
197
|
+
telegram_type (int): The type of telegram to construct.
|
|
198
|
+
node (int): The node identifier.
|
|
199
|
+
obj (int): The object identifier.
|
|
200
|
+
data (bytes): The data to be included in the telegram.
|
|
201
|
+
read_buff_len(int): Bytes read buffer length.
|
|
202
|
+
Returns:
|
|
203
|
+
bytes: The response received from the serial device.
|
|
204
|
+
Raises:
|
|
205
|
+
SystemExit: If the response received is shorter than expected.
|
|
206
|
+
'''
|
|
207
|
+
telegram = eaps2k._construct_telegram(telegram_type, node, obj, data)
|
|
208
|
+
|
|
209
|
+
if self._verbosity_lvl >= 3:
|
|
210
|
+
print(f'-- telegram:\t\t{eaps2k.bytes2hex(telegram)}')
|
|
211
|
+
|
|
212
|
+
self.ser_dev.write(telegram) # send telegram
|
|
213
|
+
ans = self.ser_dev.read(read_buff_len)
|
|
214
|
+
|
|
215
|
+
if self._verbosity_lvl >= 3:
|
|
216
|
+
print(f'-- answer:\t\t{eaps2k.bytes2hex(telegram)}')
|
|
217
|
+
|
|
218
|
+
min_len = 5 # 5 bytes is the minimum length of a valid answer
|
|
219
|
+
assert len(ans) >= min_len, \
|
|
220
|
+
f'Short answer {len(ans)} bytes received, expected at least {min_len} bytes)'
|
|
221
|
+
|
|
222
|
+
# check the answer
|
|
223
|
+
eaps2k._checksum_verify(ans)
|
|
224
|
+
eaps2k._check_error(ans)
|
|
225
|
+
|
|
226
|
+
return ans
|
|
227
|
+
|
|
228
|
+
def _read_obj(self, obj, obj_type: type = bytes):
|
|
229
|
+
allowed_obj_types = [bytes, str, float, int]
|
|
230
|
+
assert obj_type in allowed_obj_types, \
|
|
231
|
+
f'ERROR: Object type shall be one of {allowed_obj_types} ' \
|
|
232
|
+
f'but it is {obj_type if obj_type is not type else type(obj_type)}'
|
|
233
|
+
|
|
234
|
+
msg = self._transfer(self.PS_QUERY, 0, obj, '')[3:-2]
|
|
235
|
+
if obj_type is bytes:
|
|
236
|
+
return msg
|
|
237
|
+
elif obj_type is str:
|
|
238
|
+
return msg.decode('ascii')
|
|
239
|
+
elif obj_type is float:
|
|
240
|
+
return struct.unpack('>f', msg)[0]
|
|
241
|
+
elif obj_type is int:
|
|
242
|
+
return struct.unpack('>H', msg)[0]
|
|
243
|
+
else:
|
|
244
|
+
assert False, 'ERROR: Unknown!'
|
|
245
|
+
|
|
246
|
+
def _write_obj(self, obj, data, obj_type: type = bytes, mask=None):
|
|
247
|
+
allowed_obj_types = [bytes, int]
|
|
248
|
+
assert obj_type in allowed_obj_types, \
|
|
249
|
+
f'ERROR: Object type shall be one of {allowed_obj_types} ' \
|
|
250
|
+
f'but it is {obj_type if obj_type is not type else type(obj_type)}'
|
|
251
|
+
|
|
252
|
+
if obj_type is bytes:
|
|
253
|
+
assert mask is not None, f'ERROR: The mask argument value {mask} is not allowed!'
|
|
254
|
+
ans = self._transfer(self.PS_SEND, 0, obj, [mask, data])
|
|
255
|
+
return ans[3:-2]
|
|
256
|
+
elif obj_type is int:
|
|
257
|
+
ans = self._transfer(self.PS_SEND, 0, obj, [int(data) >> 8, int(data) & 0xff])
|
|
258
|
+
return (ans[3] << 8) + ans[4]
|
|
259
|
+
else:
|
|
260
|
+
assert False, 'ERROR: Unknown!'
|
|
261
|
+
|
|
262
|
+
def get_type(self):
|
|
263
|
+
'''
|
|
264
|
+
object 0
|
|
265
|
+
see: object_list_ps2000b_de_en.pdf
|
|
266
|
+
'''
|
|
267
|
+
return self._read_obj(0, str)
|
|
268
|
+
|
|
269
|
+
def get_serial(self):
|
|
270
|
+
'''
|
|
271
|
+
object 1
|
|
272
|
+
see: object_list_ps2000b_de_en.pdf
|
|
273
|
+
'''
|
|
274
|
+
return self._read_obj(1, str)
|
|
275
|
+
|
|
276
|
+
def get_nominal_voltage(self):
|
|
277
|
+
'''
|
|
278
|
+
object 2
|
|
279
|
+
see: object_list_ps2000b_de_en.pdf
|
|
280
|
+
'''
|
|
281
|
+
return float(self._read_obj(2, float))
|
|
282
|
+
|
|
283
|
+
def get_nominal_current(self):
|
|
284
|
+
'''
|
|
285
|
+
object 3
|
|
286
|
+
see: object_list_ps2000b_de_en.pdf
|
|
287
|
+
'''
|
|
288
|
+
return float(self._read_obj(3, float))
|
|
289
|
+
|
|
290
|
+
def get_nominal_power(self):
|
|
291
|
+
'''
|
|
292
|
+
object 4
|
|
293
|
+
see: object_list_ps2000b_de_en.pdf
|
|
294
|
+
'''
|
|
295
|
+
return self._read_obj(4, float)
|
|
296
|
+
|
|
297
|
+
def get_article(self):
|
|
298
|
+
'''
|
|
299
|
+
object 6
|
|
300
|
+
see: object_list_ps2000b_de_en.pdf
|
|
301
|
+
'''
|
|
302
|
+
return self._read_obj(6, str)
|
|
303
|
+
|
|
304
|
+
def get_manufacturer(self):
|
|
305
|
+
'''
|
|
306
|
+
object 8
|
|
307
|
+
see: object_list_ps2000b_de_en.pdf
|
|
308
|
+
'''
|
|
309
|
+
return self._read_obj(8, str)
|
|
310
|
+
|
|
311
|
+
def get_version(self):
|
|
312
|
+
'''
|
|
313
|
+
object 9
|
|
314
|
+
see: object_list_ps2000b_de_en.pdf
|
|
315
|
+
'''
|
|
316
|
+
return self._read_obj(9, str)
|
|
317
|
+
|
|
318
|
+
def get_device_class(self):
|
|
319
|
+
'''
|
|
320
|
+
object 19
|
|
321
|
+
see: object_list_ps2000b_de_en.pdf
|
|
322
|
+
|
|
323
|
+
avialable classes:
|
|
324
|
+
0x0010 = PS 2000 B Single, 0x0018 = PS 2000 B Triple
|
|
325
|
+
'''
|
|
326
|
+
ans = int(self._read_obj(19, int))
|
|
327
|
+
dev_class_str = 'unknown'
|
|
328
|
+
if ans == 0x0010:
|
|
329
|
+
dev_class_str = 'PS 2000 B Single'
|
|
330
|
+
elif ans == 0x0018:
|
|
331
|
+
dev_class_str = 'PS 2000 B Triple'
|
|
332
|
+
return (ans, dev_class_str)
|
|
333
|
+
|
|
334
|
+
def get_ovp(self):
|
|
335
|
+
'''
|
|
336
|
+
object 38
|
|
337
|
+
see: object_list_ps2000b_de_en.pdf
|
|
338
|
+
'''
|
|
339
|
+
return self.percent2real(self._u_nom, float(self._read_obj(38, int)))
|
|
340
|
+
|
|
341
|
+
def set_ovp(self, u):
|
|
342
|
+
'''
|
|
343
|
+
object 38
|
|
344
|
+
see: object_list_ps2000b_de_en.pdf
|
|
345
|
+
'''
|
|
346
|
+
if self._verbosity_lvl >= 1:
|
|
347
|
+
print(f'Set Over-Voltage-Protection: {u}')
|
|
348
|
+
return self._write_obj(38, int(round(self.real2percent(self._u_nom, u))), int)
|
|
349
|
+
|
|
350
|
+
def get_ocp(self):
|
|
351
|
+
'''
|
|
352
|
+
object 39
|
|
353
|
+
see: object_list_ps2000b_de_en.pdf
|
|
354
|
+
'''
|
|
355
|
+
return self.percent2real(self._i_nom, float(self._read_obj(39, int)))
|
|
356
|
+
|
|
357
|
+
def set_ocp(self, i):
|
|
358
|
+
'''
|
|
359
|
+
object 39
|
|
360
|
+
see: object_list_ps2000b_de_en.pdf
|
|
361
|
+
'''
|
|
362
|
+
if self._verbosity_lvl >= 1:
|
|
363
|
+
print(f'Set Over-Current-Protection: {i}')
|
|
364
|
+
return self._write_obj(39, int(round(self.real2percent(self._i_nom, i))), int)
|
|
365
|
+
|
|
366
|
+
def get_voltage_setpoint(self):
|
|
367
|
+
'''
|
|
368
|
+
object 50
|
|
369
|
+
see: object_list_ps2000b_de_en.pdf
|
|
370
|
+
'''
|
|
371
|
+
return self.percent2real(self._u_nom, float(self._read_obj(50, int)))
|
|
372
|
+
|
|
373
|
+
def set_voltage(self, u):
|
|
374
|
+
'''
|
|
375
|
+
object 50
|
|
376
|
+
see: object_list_ps2000b_de_en.pdf
|
|
377
|
+
'''
|
|
378
|
+
if self._verbosity_lvl >= 1:
|
|
379
|
+
print(f'Set Voltage: {u}')
|
|
380
|
+
return self._write_obj(50, int(round(self.real2percent(self._u_nom, u))), int)
|
|
381
|
+
|
|
382
|
+
def get_current_setpoint(self):
|
|
383
|
+
'''
|
|
384
|
+
object 51
|
|
385
|
+
see: object_list_ps2000b_de_en.pdf
|
|
386
|
+
'''
|
|
387
|
+
return self.percent2real(self._i_nom, float(self._read_obj(51, int)))
|
|
388
|
+
|
|
389
|
+
def set_current(self, i):
|
|
390
|
+
'''
|
|
391
|
+
object 51
|
|
392
|
+
see: object_list_ps2000b_de_en.pdf
|
|
393
|
+
'''
|
|
394
|
+
if self._verbosity_lvl >= 1:
|
|
395
|
+
print(f'Set Current: {i}')
|
|
396
|
+
return self._write_obj(51, int(round(self.real2percent(self._i_nom, i))), int)
|
|
397
|
+
|
|
398
|
+
def get_control(self):
|
|
399
|
+
'''
|
|
400
|
+
object 54
|
|
401
|
+
see: object_list_ps2000b_de_en.pdf
|
|
402
|
+
'''
|
|
403
|
+
ans = bytes(self._read_obj(54))
|
|
404
|
+
control = {
|
|
405
|
+
'output_on': True if ans[1] & 0x01 else False,
|
|
406
|
+
'remote': True if ans[0] & 0x01 else False
|
|
407
|
+
}
|
|
408
|
+
return control
|
|
409
|
+
|
|
410
|
+
def _set_control(self, mask, data):
|
|
411
|
+
'''
|
|
412
|
+
object 54
|
|
413
|
+
see: object_list_ps2000b_de_en.pdf
|
|
414
|
+
'''
|
|
415
|
+
ans = bytes(self._write_obj(54, data, bytes, mask))
|
|
416
|
+
# return True if command was acknowledged ("error 0")
|
|
417
|
+
return ans[0] == 0xff and ans[1] == 0x00
|
|
418
|
+
|
|
419
|
+
def ack_alarm(self):
|
|
420
|
+
if self._verbosity_lvl >= 2:
|
|
421
|
+
print('ACK Alarm')
|
|
422
|
+
return self._set_control(0x0a, 0x0a)
|
|
423
|
+
|
|
424
|
+
def get_remote(self):
|
|
425
|
+
return self.get_control()['remote']
|
|
426
|
+
|
|
427
|
+
def set_remote(self, remote=True):
|
|
428
|
+
if remote:
|
|
429
|
+
return self._set_control(0x10, 0x10)
|
|
430
|
+
else:
|
|
431
|
+
return self._set_control(0x10, 0x00)
|
|
432
|
+
|
|
433
|
+
def set_local(self, local=True):
|
|
434
|
+
return self.set_remote(not local)
|
|
435
|
+
|
|
436
|
+
def get_output_state(self):
|
|
437
|
+
return self.get_control()['output_on']
|
|
438
|
+
|
|
439
|
+
def set_output_state(self, on=True):
|
|
440
|
+
data = 0x01 if on else 0x00
|
|
441
|
+
if self._verbosity_lvl >= 2:
|
|
442
|
+
print(f"Set Output: {'on' if on else 'off'}")
|
|
443
|
+
return self._set_control(0x01, data)
|
|
444
|
+
|
|
445
|
+
def percent2real(self, nominal_value: float, percent_value: float) -> float:
|
|
446
|
+
'''
|
|
447
|
+
Convert percent value to real one by utilizing following equation:
|
|
448
|
+
(nominal_value * percent_value) / translation_factor
|
|
449
|
+
see: PS2000B_TFT_Programming_English.pdf, page 4
|
|
450
|
+
'''
|
|
451
|
+
return (nominal_value * percent_value) / self._translation_factor
|
|
452
|
+
|
|
453
|
+
def real2percent(self, nominal_value: float, value: float) -> float:
|
|
454
|
+
'''
|
|
455
|
+
Convert real value to percent one by utilizing following equation:
|
|
456
|
+
(translation_factor * value) / nominal_value
|
|
457
|
+
see: PS2000B_TFT_Programming_English.pdf, page 4
|
|
458
|
+
'''
|
|
459
|
+
return (self._translation_factor * value) / nominal_value
|
|
460
|
+
|
|
461
|
+
def get_actual(self):
|
|
462
|
+
'''
|
|
463
|
+
object 71
|
|
464
|
+
see: object_list_ps2000b_de_en.pdf
|
|
465
|
+
'''
|
|
466
|
+
ans = bytes(self._read_obj(71))
|
|
467
|
+
state = {
|
|
468
|
+
'remote': True if ans[0] & 0x03 else False,
|
|
469
|
+
'on': True if ans[1] & 0x01 else False,
|
|
470
|
+
'CC': True if ans[1] & 0x06 else False,
|
|
471
|
+
'CV': False if ans[1] & 0x06 else True, # not CC
|
|
472
|
+
'tracking': True if ans[1] & 0x08 else False,
|
|
473
|
+
'OVP': True if ans[1] & 0x10 else False,
|
|
474
|
+
'OCP': True if ans[1] & 0x20 else False,
|
|
475
|
+
'OPP': True if ans[1] & 0x40 else False,
|
|
476
|
+
'OTP': True if ans[1] & 0x80 else False,
|
|
477
|
+
'V': self.percent2real(self._u_nom, float((int(ans[2]) << 8) + int(ans[3]))),
|
|
478
|
+
'I': self.percent2real(self._i_nom, float((int(ans[4]) << 8) + int(ans[5]))),
|
|
479
|
+
}
|
|
480
|
+
return state
|
|
481
|
+
|
|
482
|
+
def get_setpoints(self):
|
|
483
|
+
'''
|
|
484
|
+
object 72
|
|
485
|
+
see: object_list_ps2000b_de_en.pdf
|
|
486
|
+
'''
|
|
487
|
+
ans = bytes(self._read_obj(72))
|
|
488
|
+
state = {
|
|
489
|
+
'remote': True if ans[0] & 0x03 else False,
|
|
490
|
+
'on': True if ans[1] & 0x01 else False,
|
|
491
|
+
'CC': True if ans[1] & 0x06 else False,
|
|
492
|
+
'OVP': True if ans[1] & 0x10 else False,
|
|
493
|
+
'OCP': True if ans[1] & 0x20 else False,
|
|
494
|
+
'OPP': True if ans[1] & 0x40 else False,
|
|
495
|
+
'OTP': True if ans[1] & 0x80 else False,
|
|
496
|
+
'V': self.percent2real(self._u_nom, float((int(ans[2]) << 8) + int(ans[3]))),
|
|
497
|
+
'I': self.percent2real(self._i_nom, float((int(ans[4]) << 8) + int(ans[5]))),
|
|
498
|
+
}
|
|
499
|
+
return state
|
|
500
|
+
|
|
501
|
+
@staticmethod
|
|
502
|
+
def get_config_template() -> dict:
|
|
503
|
+
'''
|
|
504
|
+
Returns a dictionary with the configuration template for the system.
|
|
505
|
+
The dictionary contains the following keys:
|
|
506
|
+
- 'ACK': Acknowledge alarms, initialized to False (bool)
|
|
507
|
+
- 'OVP': Over Voltage Protection, initialized to 0.0 (float)
|
|
508
|
+
- 'OCP': Over Current Protection, initialized to 0.0 (float)
|
|
509
|
+
- 'Iset': Current setpoint, initialized to 0.0 (float)
|
|
510
|
+
- 'Vset': Voltage setpoint, initialized to 0.0 (float)
|
|
511
|
+
Returns:
|
|
512
|
+
dict: A dictionary with the configuration template.
|
|
513
|
+
'''
|
|
514
|
+
return {'ACK': bool(False), 'OVP': int(0), 'OCP': int(0),
|
|
515
|
+
'Iset': float(0.0), 'Vset': float(0.0)}
|
|
516
|
+
|
|
517
|
+
def configure(self, cfg: dict):
|
|
518
|
+
'''
|
|
519
|
+
Configures the device with the provided settings.
|
|
520
|
+
Parameters:
|
|
521
|
+
cfg (dict): A dictionary containing configuration settings.
|
|
522
|
+
The dictionary should have the following keys:
|
|
523
|
+
- 'ACK' (bool): Acknowledge alarms.
|
|
524
|
+
- 'OCP' (float): Over-Current-Protection threshold.
|
|
525
|
+
- 'OVP' (float): Over-Voltage-Protection threshold.
|
|
526
|
+
- 'Vset' (float): Voltage setting.
|
|
527
|
+
- 'Iset' (float): Current setting.
|
|
528
|
+
The method sets the corresponding thresholds and settings for the
|
|
529
|
+
device based on the provided configuration.
|
|
530
|
+
'''
|
|
531
|
+
if cfg['ACK'] is True:
|
|
532
|
+
self.ack_alarm()
|
|
533
|
+
|
|
534
|
+
if isinstance(cfg['OCP'], float):
|
|
535
|
+
self.set_ocp(cfg['OCP'])
|
|
536
|
+
|
|
537
|
+
if isinstance(cfg['OVP'], float):
|
|
538
|
+
self.set_ovp(cfg['OVP'])
|
|
539
|
+
|
|
540
|
+
if isinstance(cfg['Vset'], float):
|
|
541
|
+
self.set_voltage(cfg['Vset'])
|
|
542
|
+
|
|
543
|
+
if isinstance(cfg['Iset'], float):
|
|
544
|
+
self.set_current(cfg['Iset'])
|
|
545
|
+
|
|
546
|
+
def print_info(self):
|
|
547
|
+
'''
|
|
548
|
+
Get info from device and print it.
|
|
549
|
+
'''
|
|
550
|
+
dev_class_nr, dev_class_str = self.get_device_class()
|
|
551
|
+
dev_state = self.get_actual()
|
|
552
|
+
print(
|
|
553
|
+
f'type {self.get_type()}\n'
|
|
554
|
+
f'serial {self.get_serial()}\n'
|
|
555
|
+
f'article {self.get_article()}\n'
|
|
556
|
+
f'manuf {self.get_manufacturer()}\n'
|
|
557
|
+
f'version {self.get_version()}\n'
|
|
558
|
+
f'nom. voltage {self.get_nominal_voltage()}\n'
|
|
559
|
+
f'nom. current {self.get_nominal_current()}\n'
|
|
560
|
+
f'nom. power {self.get_nominal_power()}\n'
|
|
561
|
+
f'class {hex(dev_class_nr)} ({dev_class_str})\n'
|
|
562
|
+
f'OVP {self.get_ovp()}\n'
|
|
563
|
+
f'OCP {self.get_ocp()}\n'
|
|
564
|
+
f'control {self.get_control()}\n'
|
|
565
|
+
f'state {dev_state}'
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def main():
|
|
570
|
+
parser = argparse.ArgumentParser(description=eaps2k.description())
|
|
571
|
+
parser.add_argument('--version', action='version',
|
|
572
|
+
version=f'%(prog)s {eaps2k.pkg_version()}')
|
|
573
|
+
parser.add_argument(
|
|
574
|
+
'-p', '--port', type=str, help='serial port to use', required=True)
|
|
575
|
+
|
|
576
|
+
default_voltage = None
|
|
577
|
+
default_current = None
|
|
578
|
+
parser.add_argument('-V', '--voltage', type=float, default=default_voltage,
|
|
579
|
+
required=False,
|
|
580
|
+
help=f'Voltage to be set. Nothing will be changed if None.'
|
|
581
|
+
f'(default: {default_voltage})')
|
|
582
|
+
parser.add_argument('--ovp', type=float, default=default_voltage,
|
|
583
|
+
required=False,
|
|
584
|
+
help=f'OVP - Over-Voltage-Protection. '
|
|
585
|
+
f'Nothing will be changed if None.'
|
|
586
|
+
f'(default: {default_voltage})')
|
|
587
|
+
parser.add_argument('-I', '--current', type=float, default=default_current,
|
|
588
|
+
required=False,
|
|
589
|
+
help=f'Current to be set. Nothing will be changed if None.'
|
|
590
|
+
f'(default: {default_current})')
|
|
591
|
+
parser.add_argument('--ocp', type=float, default=default_current,
|
|
592
|
+
required=False,
|
|
593
|
+
help=f'OCP - Over-Current-Protection. '
|
|
594
|
+
f'Nothing will be changed if None.'
|
|
595
|
+
f'(default: {default_current})')
|
|
596
|
+
|
|
597
|
+
group_verb = parser.add_mutually_exclusive_group(required=False)
|
|
598
|
+
group_verb.add_argument('-v', dest='verbose', action='count', default=0,
|
|
599
|
+
help='increase verbosity level')
|
|
600
|
+
|
|
601
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
602
|
+
group.add_argument('--on', help='turn on', action='store_true')
|
|
603
|
+
group.add_argument('--off', help='turn off', action='store_true')
|
|
604
|
+
group.add_argument('--toggle', help='toggle output on/off', action='store_true')
|
|
605
|
+
group.add_argument('--info', help='print info and exit', action='store_true')
|
|
606
|
+
group.add_argument('--ack', help='Acknowledge alarms', action='store_true')
|
|
607
|
+
args = parser.parse_args()
|
|
608
|
+
|
|
609
|
+
cfg = eaps2k.get_config_template()
|
|
610
|
+
cfg['ACK'] = args.ack
|
|
611
|
+
cfg['OVP'] = args.ovp
|
|
612
|
+
cfg['OCP'] = args.ocp
|
|
613
|
+
cfg['Iset'] = args.current
|
|
614
|
+
cfg['Vset'] = args.voltage
|
|
615
|
+
|
|
616
|
+
with eaps2k(args.port, verbosity_level=args.verbose) as ps:
|
|
617
|
+
ps.configure(cfg) # set configuration do nothing if value(s) is/are None
|
|
618
|
+
if args.on:
|
|
619
|
+
ps.set_output_state(True)
|
|
620
|
+
elif args.off:
|
|
621
|
+
ps.set_output_state(False)
|
|
622
|
+
elif args.toggle:
|
|
623
|
+
state = ps.get_output_state()
|
|
624
|
+
ps.set_output_state(not state)
|
|
625
|
+
elif args.info:
|
|
626
|
+
ps.print_info()
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
if __name__ == '__main__':
|
|
630
|
+
sys.exit(main())
|