Initial Upload

This commit is contained in:
Martin Zietz
2020-12-03 14:56:11 +01:00
parent d044c753e5
commit 22fc08fb95
7 changed files with 689 additions and 1 deletions
+96
View File
@@ -0,0 +1,96 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
.venv/
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
# PyCharm
.idea
# VScode
.vscode/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Sören Sprößig
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+67 -1
View File
@@ -1,2 +1,68 @@
# Magnetfeldteststand
# Python-PS2000B
Python library to work with Elektro-Automatik PS 2000 B power supplies via USB.
## Compatibility
Tested with:
+ Python 2.7
+ Python 3.5, 3.6
Tested on:
+ Windows 7 x64
+ Windows 10 x64 Version 1607 (OS Build 14393.2035)
+ Ubuntu 16.04.1 LTS
+ Ubuntu 20.04.1 LTS
## Features of Python-PS2000B
### Supported
- read static device information (manufacturer, serial, device type ...)
- read dynamic device information (current, voltage)
- read/write remote control
- read/write output control
### Still todo
- set voltage and current
- wrap error results in own telegram
## Prerequisites
### Python
The following third-party Python libraries are needed:
* `pyserial` - serial communication library for Python, see https://pypi.python.org/pypi/pyserial
### Windows
On Windows the USB driver (fetch it from http://www.elektroautomatik.de/files/eautomatik/treiber/usb/ea_device_driver.rar) must be installed. Afterwards you can find the serial port `COMxx` in the *device manager*.
### Linux
On Linux the device is detected as `/dev/ttyACMx` (abstract control model, see https://www.rfc1149.net/blog/2013/03/05/what-is-the-difference-between-devttyusbx-and-devttyacmx/). Use `dmesg` after connecting the device to find out `x`.
Most Linuxes will require users to elevate for accessing the device. If you want to access the device as your current user, just add it to the group `dialout` (`ls -lah /dev/ttyACM0` will show you the group to use, usually this is `dialout`) and login again.
## Usage
```python
import time
from pyps2000b import PS2000B
device = PS2000B.PS2000B("/dev/ttyACM0")
print("Device status: %s" % device.get_device_status_information())
device.enable_remote_control()
device.voltage1 = 5.1
device.current1 = 1
device.enable_output()
time.sleep(1)
print("Current output: %0.2f V , %0.2f A" % (device.get_voltage(), device.get_current()))
device.output1 = False
```
## Documentation
+ product website: http://www.elektroautomatik.de/en/ps2000b.html
+ programming guide PS 2000 B: http://www.elektroautomatik.de/files/eautomatik/treiber/ps2000b/programming_ps2000b.zip
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
# coding=utf-8
import platform
import time
from pyps2000b import PS2000B
DEVICE = "COM7" if platform.system() == "Windows" else "/dev/ttyACM0"
# connection to the device is automatically opened
print("Connecting to device at %s..." % DEVICE)
device1 = PS2000B.PS2000B(DEVICE) # create Object of class PS2000B, pass COM-port and channel to functions inside
# static device information can be read
print("Connection open: %s" % device1.is_open())
print("Device: %s" % device1.get_device_information())
# dynamic device status information can be read
device_status_info1 = device1.get_device_status_information(0)
device_status_info2 = device1.get_device_status_information(1)
print("Device status 1: %s" % device1.get_device_status_information(0))
print("Device status 2: %s" % device1.get_device_status_information(1))
print("Current output 1: %0.2f V , %0.2f A" % (device1.voltage1, device1.current1))
print("Current output 2: %0.2f V , %0.2f A" % (device1.voltage2, device1.current2))
# device can be controlled
if not device_status_info1.remote_control_active:
print("...will enable remote control...")
device1.enable_remote_control(0)
if not device_status_info2.remote_control_active:
print("...will enable remote control...")
device1.enable_remote_control(1)
print("...set voltage 1 to 12V and max current to 1A...")
device1.voltage1 = 12
device1.current1 = 1
time.sleep(2)
print("...now enabling the power output control 1...")
device1.enable_output(0)
time.sleep(2)
print("... set voltage 2 to 5V and max current to 1A...")
device1.voltage2 = 5
device1.current2 = 1
time.sleep(2)
print("...now enabling the power output control 2...")
device1.enable_output(1)
time.sleep(5)
print("Device status 1: %s" % device1.get_device_status_information(0))
print("Device status 2: %s" % device1.get_device_status_information(1))
print("Current output 1: %0.2f V , %0.2f A" % (device1.voltage1, device1.current1))
print("Current output 2: %0.2f V , %0.2f A" % (device1.voltage2, device1.current2))
time.sleep(5)
device1.disable_output(0)
device1.disable_output(1)
print("...and disabling remote control again.")
device1.disable_remote_control(0)
device1.disable_remote_control(1)
print("Device status 1: %s" % device1.get_device_status_information(0))
print("Device status 2: %s" % device1.get_device_status_information(1))
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
# coding=utf-8
import platform
import time
from pyps2000b import PS2000B
DEVICE = "COM7" if platform.system() == "Windows" else "/dev/ttyACM0"
# connection to the device is automatically opened
print("Connecting to device at %s..." % DEVICE)
device = PS2000B.PS2000B(DEVICE) # create Object of class PS2000B, pass COM-port to functions inside
# static device information can be read
print("Connection open: %s" % device.is_open())
print("Device: %s" % device.get_device_information())
# dynamic device status information can be read
device_status_info1 = device.get_device_status_information(0)
device_status_info2 = device.get_device_status_information(1)
print("Device status 1: %s" % device_status_info1)
print("Device status 2: %s" % device_status_info2)
print("Current output: %0.2f V , %0.2f A" % (device.get_voltage(), device.get_current()))
# device can be controlled
if not device_status_info.remote_control_active:
print("...will enable remote control...")
device.enable_remote_control()
print("...set voltage to 12V and max current to 1A...")
device.voltage1 = 12
device.current1 = 1
time.sleep(10)
print("...now enabling the power output control...")
device.enable_output()
time.sleep(2)
device_status_info1 = device.get_device_status_information(0)
device_status_info2 = device.get_device_status_information(1)
print("Device status 1: %s" % device_status_info1)
print("Device status 2: %s" % device_status_info2)
print("Current output: %0.2f V , %0.2f A" % (device.voltage1, device.current1))
time.sleep(10)
print("...set voltage to 5.1V...")
device.voltage1 = 5.1
time.sleep(10)
print("Current output: %0.2f V , %0.2f A" % (device.get_voltage(), device.get_current()))
print("...after 5 seconds power output will be disabled again ...")
time.sleep(5)
device.disable_output()
print("...and disabling remote control again.")
device.disable_remote_control()
print("Device status 1: %s" % device.get_device_status_information(0))
print("Device status 2: %s" % device.get_device_status_information(1))
+382
View File
@@ -0,0 +1,382 @@
#!/usr/bin/env python3
# coding=utf-8
# Python access to Elektro Automatik PS 2000 B devices via USB/serial
#
# Supported features:
# - read static device information (manufacturer, serial, device type ...)
# - read dynamic device information (current, voltage)
# - read/write remote control
# - read/write output control
#
# Todo:
# - wrap error results in own telegram
#
# References
# [1] = "PS 2000B Programming Guide" from 2015-05-28
# [2] = "PS 2000B object list"
#
import serial
import struct
import sys
PY_3 = sys.version_info >= (3, 0)
__author__ = "Sören Sprößig <ssproessig@gmail.com>"
def as_string(raw_data):
"""Converts the given raw bytes to a string (removes NULL)"""
return bytearray(raw_data[:-1])
def as_float(raw_data):
"""Converts the given raw bytes to a float"""
f = struct.unpack_from(">f", bytearray(raw_data))[0]
return f
def as_word(raw_data):
"""Converts the given raw bytes to a word"""
w = struct.unpack_from(">H", bytearray(raw_data))[0]
return w
def _ord(x):
"""Wrap ord() call as we only need it in Python 2"""
return x if PY_3 else ord(x)
# noinspection PyClassHasNoInit
class Constants:
"""Communication constants"""
# communication parameters taken from [1], chapter 2.2
CONNECTION_BAUD_RATE = 115200
CONNECTION_PARITY = serial.PARITY_ODD
CONNECTION_STOP_BITS = 1
# timeout taken from [1], chapter 3.7
TIMEOUT_BETWEEN_COMMANDS = 0.05
# according to spec [1] 2.4:
# maximum length of a telegram is 21 bytes (Byte 0..20)
MAX_LEN_IN_BYTES = 21
# noinspection PyClassHasNoInit
class Objects:
"""Supported objects ids / commands"""
DEVICE_TYPE = 0
DEVICE_SERIAL_NO = 1
NOMINAL_VOLTAGE = 2
NOMINAL_CURRENT = 3
NOMINAL_POWER = 4
DEVICE_ARTICLE_NO = 6
MANUFACTURER = 8
SOFTWARE_VERSION = 9
SET_VALUE_VOLTAGE = 50
SET_VALUE_CURRENT = 51
POWER_SUPPLY_CONTROL = 54
STATUS_ACTUAL_VALUES = 71
# noinspection PyClassHasNoInit
class ControlParameters:
"""Parameters for controlling the device"""
SWITCH_MODE_CMD = 0x10
SWITCH_MODE_REMOTE = 0x10
SWITCH_MODE_MANUAL = 0x00
SWITCH_POWER_OUTPUT_CMD = 0x1
SWITCH_POWER_OUTPUT_ON = 0x1
SWITCH_POWER_OUTPUT_OFF = 0x0
class Telegram:
"""Base class of a PS2000B telegram - basically allows accessing the raw bytes and does checksum calculation"""
def __init__(self):
self._bytes = []
self._checksum = []
self.checksum_ok = False
def _calc_checksum(self):
cs = 0
for b in self._bytes:
cs += b
cs_high = (cs & 0xff00) >> 8
cs_low = cs & 0xff
return [cs_high, cs_low]
@staticmethod
def _get_start_delimiter(transmission, expected_data_length):
result = 0b00000000
if expected_data_length > 16:
raise Exception("only 4 bits for expected length can be used")
result |= (expected_data_length - 1)
result |= 0b10000
result |= 0b100000
result |= transmission << 6
return result
def get_byte_array(self):
return bytearray(self._bytes + self._checksum)
class FromPowerSupply(Telegram):
"""Telegram received from the power supply"""
def __init__(self, raw_data):
Telegram.__init__(self)
data = [_ord(x) for x in raw_data]
self._bytes = data[0:-2]
self._checksum = data[len(data) - 2:len(data)]
self.checksum_ok = self._checksum == self._calc_checksum()
def get_sd(self):
return self._bytes[0]
def get_device_node(self):
return self._bytes[1]
def get_object(self):
return self._bytes[3]
def get_data(self):
return self._bytes[3:len(self._bytes)]
# noinspection PyMethodMayBeStatic
def get_error(self):
# FIXME: [1] chapter 3.6 add support for error codes here
return None
class ToPowerSupply(Telegram):
"""A telegram sent to the power supply"""
def __init__(self, transmission, data, expected_data_length):
Telegram.__init__(self)
self._bytes = []
self._bytes.append(self._get_start_delimiter(transmission, expected_data_length))
self._bytes.extend(data)
self._checksum = self._calc_checksum()
self.checksum_ok = True
class DeviceInformation:
"""A class carrying all static device information read from the device"""
def __init__(self):
self.device_type = ""
self.device_serial_no = ""
self.nominal_voltage = 0
self.nominal_current = 0
self.nominal_power = 0
self.manufacturer = ""
self.device_article_number = ""
self.software_version = ""
def __str__(self):
return "%s %s [%s], SW: %s, Art-Nr: %s, [%0.2f V, %0.2f A, %0.2f W]" % \
(self.manufacturer,
self.device_type, self.device_serial_no,
self.software_version, self.device_article_number,
self.nominal_voltage, self.nominal_current, self.nominal_power)
class DeviceStatusInformation:
"""A class carrying all dynamic device status information"""
def __init__(self, raw_data):
self.remote_control_active = raw_data[0] & 0b1
self.output_active = raw_data[1] & 0b1
self.actual_voltage_percent = float(as_word(raw_data[2:4])) / 256
self.actual_current_percent = float(as_word(raw_data[4:6])) / 256
def __str__(self):
return "Remote control active: %s, Output active: %s" % (self.remote_control_active, self.output_active)
class PS2000B:
"""PS 2000 B main communication class"""
def __init__(self, serial_port):
self.__device_status_information1 = None
self.__device_status_information2 = None
self.__serial = serial.Serial(serial_port,
baudrate=Constants.CONNECTION_BAUD_RATE,
timeout=Constants.TIMEOUT_BETWEEN_COMMANDS * 2,
parity=serial.PARITY_ODD,
stopbits=Constants.CONNECTION_STOP_BITS)
self.__device_information = self.__read_device_information()
def is_open(self):
return self.__serial.is_open
def get_device_information(self):
return self.__device_information
def __read_device_information(self, channel=0):
result = DeviceInformation()
# taken from [2]
result.device_type = as_string(self.__read_device_data(16, Objects.DEVICE_TYPE, channel).get_data())
result.device_serial_no = as_string(self.__read_device_data(16, Objects.DEVICE_SERIAL_NO, channel).get_data())
result.nominal_voltage = as_float(self.__read_device_data(4, Objects.NOMINAL_VOLTAGE, channel).get_data())
result.nominal_current = as_float(self.__read_device_data(4, Objects.NOMINAL_CURRENT, channel).get_data())
result.nominal_power = as_float(self.__read_device_data(4, Objects.NOMINAL_POWER, channel).get_data())
result.device_article_number = as_string(self.__read_device_data(16, Objects.DEVICE_ARTICLE_NO, channel).get_data())
result.manufacturer = as_string(self.__read_device_data(16, Objects.MANUFACTURER, channel).get_data())
result.software_version = as_string(self.__read_device_data(16, Objects.SOFTWARE_VERSION, channel).get_data())
return result
def __read_device_data(self, expected_length, object_id, channel):
telegram = ToPowerSupply(0b01, [channel, object_id], expected_length)
result = self.__send_and_receive(telegram.get_byte_array())
return result
def __send_and_receive(self, raw_bytes):
self.__serial.write(raw_bytes)
result = FromPowerSupply(self.__serial.read(Constants.MAX_LEN_IN_BYTES))
return result
def get_device_status_information(self, channel=0):
""":returns DeviceStatusInformation"""
if channel == 0:
if self.__device_status_information1 is None:
self.update_device_information(0)
info = self.__device_status_information1
elif channel == 1:
if self.__device_status_information2 is None:
self.update_device_information(1)
info = self.__device_status_information2
else:
info = None
raise ValueError("Invalid Channel")
return info
def update_device_information(self, channel=0):
telegram = ToPowerSupply(0b01, [channel, Objects.STATUS_ACTUAL_VALUES], 6)
device_information = self.__send_and_receive(telegram.get_byte_array())
if channel == 0:
self.__device_status_information1 = DeviceStatusInformation(device_information.get_data())
elif channel == 1:
self.__device_status_information2 = DeviceStatusInformation(device_information.get_data())
else:
raise ValueError("Invalid Channel")
def __send_device_control(self, p1, p2, channel=0):
telegram = ToPowerSupply(0b11, [channel, Objects.POWER_SUPPLY_CONTROL, p1, p2], 2)
_ = self.__send_and_receive(telegram.get_byte_array())
self.update_device_information(channel)
def __send_device_data(self, obj, data, channel):
'''
Send integer data with obj-id to the PSU
'''
telegram = ToPowerSupply(0b11, [channel, obj, data >> 8, data & 0xff], 4)
_ = self.__send_and_receive(telegram.get_byte_array())
self.update_device_information(channel)
def enable_remote_control(self, channel):
self.__send_device_control(ControlParameters.SWITCH_MODE_CMD, ControlParameters.SWITCH_MODE_REMOTE, channel)
def disable_remote_control(self, channel):
self.__send_device_control(ControlParameters.SWITCH_MODE_CMD, ControlParameters.SWITCH_MODE_MANUAL, channel)
def enable_output(self, channel):
self.__send_device_control(ControlParameters.SWITCH_POWER_OUTPUT_CMD, ControlParameters.SWITCH_POWER_OUTPUT_ON, channel)
def disable_output(self, channel):
self.__send_device_control(ControlParameters.SWITCH_POWER_OUTPUT_CMD, ControlParameters.SWITCH_POWER_OUTPUT_OFF, channel)
@property
def output1(self,):
return self.get_device_status_information(0).output_active
@output1.setter
def output1(self, value):
if value:
self.enable_output()
else:
self.disable_output()
def get_voltage(self, channel=0):
self.update_device_information(channel)
if channel == 0:
v_perc = self.__device_status_information1.actual_voltage_percent
elif channel == 1:
v_perc = self.__device_status_information2.actual_voltage_percent
else:
raise ValueError("Invalid channel")
voltage = self.__device_information.nominal_voltage * v_perc
return voltage / 100
def get_voltage_setpoint(self, channel=0):
res = self.__read_device_data(2, Objects.SET_VALUE_VOLTAGE, channel).get_data()
return self.__device_information.nominal_voltage * ((res[0]<<8) + res[1]) / 25600.0
def set_voltage(self, value, channel):
self.update_device_information(channel)
self.enable_remote_control(channel)
volt = int(round( (value * 25600.0) / self.__device_information.nominal_voltage ))
self.__send_device_data(Objects.SET_VALUE_VOLTAGE, volt, channel)
#self.disable_remote_control()
@property
def voltage1(self):
return self.get_voltage(0)
@voltage1.setter
def voltage1(self, value): # voltage of channel 1
self.set_voltage(value, 0)
@property
def voltage2(self):
return self.get_voltage(1)
@voltage2.setter
def voltage2(self, value): # voltage of channel 2
self.set_voltage(value, 1)
def get_current(self, channel=0):
self.update_device_information(channel)
if channel == 0:
c_perc = self.__device_status_information1.actual_current_percent
elif channel == 1:
c_perc = self.__device_status_information2.actual_current_percent
else:
raise ValueError("Invalid channel")
current = self.__device_information.nominal_current * c_perc
return current / 100
def get_current_setpoint(self, channel=0):
res = self.__read_device_data(2, Objects.SET_VALUE_CURRENT, channel).get_data()
return self.__device_information.nominal_current * ((res[0] << 8) + res[1]) / 25600.0
def set_current(self, value, channel):
self.update_device_information(channel)
self.enable_remote_control(channel)
curr = int(round( (value * 25600.0) / self.__device_information.nominal_current))
self.__send_device_data(Objects.SET_VALUE_CURRENT, curr, channel)
# self.disable_remote_control()
@property
def current1(self):
return self.get_current(0)
@current1.setter
def current1(self, value): # current of channel 1
self.set_current(value, 0)
@property
def current2(self):
return self.get_current(1)
@current2.setter
def current2(self, value): # current of channel 2
self.set_current(value, 1)
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/python
# coding=utf-8
__author__ = "Sören Sprößig <ssproessig@gmail.com>"