Merge pull request 'Development_zietz' (#1) from Development_zietz into master

Reviewed-on: zietzm/Magnetfeldteststand#1
This commit is contained in:
2021-02-20 18:12:15 +01:00
25 changed files with 1893 additions and 539 deletions
+5
View File
@@ -98,3 +98,8 @@ ENV/
.idea/misc.xml
.idea/Python-PS2000B.iml
.idea/Python-PS2000B.iml
.idea/Python-PS2000B.iml
.idea/misc.xml
config.ini
*.ini
log.csv
+1 -1
View File
@@ -2,7 +2,7 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.7 (MagnetEnv)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.7 (venv)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (MagnetEnv)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (venv)" project-jdk-type="Python SDK" />
</project>
+1
View File
@@ -1,3 +1,4 @@
# This file enables control of a connected Arduino microcontroller.
#!/usr/bin/env python
import logging
import itertools
-29
View File
@@ -1,29 +0,0 @@
import time
from Arduino import Arduino
print("Searching for Arduino...")
board = Arduino()
print("Arduino found.")
board.pinMode(15, "Output")
board.pinMode(16, "Output")
board.pinMode(17, "Output")
board.digitalWrite(15, "LOW")
board.digitalWrite(16, "LOW")
board.digitalWrite(17, "LOW")
i = 0
while i <= 1:
print("running: ", i)
for var in [15,16,17]:
board.digitalWrite(var, "HIGH")
time.sleep(0.5)
time.sleep(5)
for var in [15,16,17]:
board.digitalWrite(var, "LOW")
time.sleep(0.5)
time.sleep(2)
i = i + 1
board.close()
-72
View File
@@ -1,72 +0,0 @@
# import platform
import time as t
import numpy as np
import settings as g
import cage_func as func
from pyps2000b import PS2000B
# User Inputs/Configuration----------------------------------
Test1 = 0
Test2 = 1
Test3 = 0
Test4 = 0
# Constants:
g.COIL_CONST = np.array([38.6, 38.45, 37.9]) * 1e-9 # Coil constants [x,y,z] in T/A
g.AMBIENT_FIELD = np.array([80]) * 1e-6 # ambient magnetic field in measurement area, to be cancelled out
g.RESISTANCES = np.array([3.9, 3.9, 1]) # resistance of [x,y,z] circuits
g.MAX_WATTS = np.array([8, 8, 0]) # max. allowed power for [x,y,z] circuits
# COM-Ports for power supply units:
XY_PORT = "COM7"
g.XY_DEVICE = PS2000B.PS2000B(XY_PORT)
g.MAX_AMPS = np.sqrt(g.MAX_WATTS / g.RESISTANCES)
#print(g.MAX_AMPS)
'''def print_status():
print("Output 1:")
func.print_status(g.X_AXIS)
print("Output 2:")
func.print_status(g.Y_AXIS)'''
func.set_to_zero(g.XY_DEVICE)
#print_status()
t.sleep(3)
if Test1 == 1:
print("setting")
g.XY_DEVICE.voltage1 = 5
g.XY_DEVICE.current1 = 1
g.XY_DEVICE.enable_all()
t.sleep(1)
#print_status()
t.sleep(5)
print("setting to zero")
func.set_to_zero(g.XY_DEVICE)
if Test2 == 1:
print("setting current")
g.XY_DEVICE.set_current(0.2, 0)
g.XY_DEVICE.set_voltage(5, 0)
g.XY_DEVICE.enable_all()
t.sleep(1)
#print_status()
t.sleep(5)
func.set_to_zero(g.XY_DEVICE)
if Test4 == 1:
func.set_axis_current(g.XY_DEVICE, 0.2)
t.sleep(1)
print_status()
t.sleep(10)
func.set_to_zero(g.XY_DEVICE)
t.sleep(1)
#print_status()
g.XY_DEVICE.disable_all()
#print_status()
BIN
View File
Binary file not shown.
View File
+15
View File
@@ -0,0 +1,15 @@
Time (s);xField (T);yField (T);zField (T)
0;0,0000145;0,0000255;0,0000195
0,1;-0,000015;0,000025;0,00002
0,2;-0,0000155;-0,0000245;0,0000205
0,3;-0,000016;-0,000024;-0,000021
0,4;0,0000165;-0,0000235;-0,0000215
0,5;0,000017;0,000023;-0,000022
0,6;0,0000175;0,0000225;0,0000225
0,7;-0,000018;-0,000022;0,000023
0,8;0,0000185;-0,0000215;-0,0000235
0,9;-0,000019;0,000021;-0,000024
1;-0,0000195;-0,0000205;-0,0000245
1,1;0,00002;0,00002;0,000025
1,2;-0,0000205;-0,0000195;-0,0000245
1,3;0,000021;0,000019;0,000024
1 Time (s) xField (T) yField (T) zField (T)
2 0 0,0000145 0,0000255 0,0000195
3 0,1 -0,000015 0,000025 0,00002
4 0,2 -0,0000155 -0,0000245 0,0000205
5 0,3 -0,000016 -0,000024 -0,000021
6 0,4 0,0000165 -0,0000235 -0,0000215
7 0,5 0,000017 0,000023 -0,000022
8 0,6 0,0000175 0,0000225 0,0000225
9 0,7 -0,000018 -0,000022 0,000023
10 0,8 0,0000185 -0,0000215 -0,0000235
11 0,9 -0,000019 0,000021 -0,000024
12 1 -0,0000195 -0,0000205 -0,0000245
13 1,1 0,00002 0,00002 0,000025
14 1,2 -0,0000205 -0,0000195 -0,0000245
15 1,3 0,000021 0,000019 0,000024
+15
View File
@@ -0,0 +1,15 @@
Time (s);xField (T);yField (T);zField (T)
0;0,0000145;0,0000255;0,0000195
1;-0,000015;0,000025;0,00002
2;-0,0000155;-0,0000245;0,0000205
3;-0,000016;-0,000024;-0,000021
4;0,0000165;-0,0000235;-0,0000215
5;0,000017;0,000023;-0,000022
6;0,0000175;0,0000225;0,0000225
7;-0,000018;-0,000022;0,000023
8;0,0000185;-0,0000215;-0,0000235
9;-0,000019;0,000021;-0,000024
10;-0,0000195;-0,0000205;-0,0000245
11;0,00002;0,00002;0,000025
12;-0,0000205;-0,0000195;-0,0000245
13;0,000021;0,000019;0,000024
1 Time (s) xField (T) yField (T) zField (T)
2 0 0,0000145 0,0000255 0,0000195
3 1 -0,000015 0,000025 0,00002
4 2 -0,0000155 -0,0000245 0,0000205
5 3 -0,000016 -0,000024 -0,000021
6 4 0,0000165 -0,0000235 -0,0000215
7 5 0,000017 0,000023 -0,000022
8 6 0,0000175 0,0000225 0,0000225
9 7 -0,000018 -0,000022 0,000023
10 8 0,0000185 -0,0000215 -0,0000235
11 9 -0,000019 0,000021 -0,000024
12 10 -0,0000195 -0,0000205 -0,0000245
13 11 0,00002 0,00002 0,000025
14 12 -0,0000205 -0,0000195 -0,0000245
15 13 0,000021 0,000019 0,000024
+12
View File
@@ -0,0 +1,12 @@
Time (s);xField (T);yField (T);zField (T);
0;0,00015;-0,00015;0,00002;150
1;0,00017;-0,00017;0,00002;170
2;0,00018;-0,00018;0,00002;180
3;0,00019;-0,00019;0,00002;190
4;0,0002;-0,0002;0,00002;200
5;0,00021;0,00021;0,00002;210
6;0,00022;-0,00022;0,00002;220
7;0,0002;-0,0002;0,00002;200
8;0,00018;-0,00018;0,00002;180
9;0,00005;-0,00005;0,00002;50
10;-0,00004;0,00004;0,00002;-40
1 Time (s) xField (T) yField (T) zField (T)
2 0 0,00015 -0,00015 0,00002 150
3 1 0,00017 -0,00017 0,00002 170
4 2 0,00018 -0,00018 0,00002 180
5 3 0,00019 -0,00019 0,00002 190
6 4 0,0002 -0,0002 0,00002 200
7 5 0,00021 0,00021 0,00002 210
8 6 0,00022 -0,00022 0,00002 220
9 7 0,0002 -0,0002 0,00002 200
10 8 0,00018 -0,00018 0,00002 180
11 9 0,00005 -0,00005 0,00002 50
12 10 -0,00004 0,00004 0,00002 -40
+1004 -135
View File
File diff suppressed because it is too large Load Diff
+321 -210
View File
@@ -1,34 +1,48 @@
from pyps2000b import PS2000B
from Arduino import Arduino
import settings as g
import pandas
import time
# This file contains all classes and functions directly related to the operation of the helmholtz test stand.
# The two main classes are Axis and ArduinoCtrl, see their definitions for details.
# import packages:
import numpy as np
import serial
import traceback
from tkinter import messagebox
# import other project files
from User_Interface import ui_print
from pyps2000b import PS2000B
from Arduino import Arduino
import config_handling as config
import globals as g
class Axis:
# Main class representing an axis (x,y,z) of the test stand
# contains static and dynamic status information about this axis and methods to control it
def __init__(self, index, device, PSU_channel, arduino_pin):
# static information
self.index = index
self.device = device # power supply object (PS2000B class)
self.channel = PSU_channel # power supply unit channel (1 or 2)
self.index = index # index of this axis, 0->X, 1->Y, 2->Z
self.device = device # power supply object for this axis (PS2000B class)
self.channel = PSU_channel # power supply unit channel (0 or 1)
self.ardPin = arduino_pin # output pin on the arduino for switching polarity on this axis
self.name = g.AXIS_NAMES[index]
self.port = g.ports[index]
self.name = g.AXIS_NAMES[index] # get name of this axis from list in globals.py (e.g. "X-Axis"
self.port = g.PORTS[index] # get serial port of this axis PSU
self.resistance = 0 # [Ohm]
# maximum allowable values to pass through this circuit
self.max_watts = 0 # [W]
self.max_amps = 0 # [A]
self.max_volts = 0 # [V]
# read static information from the configuration object (which has read it from the config file or settings):
self.resistance = float(config.read_from_config(self.name, "resistance", config.CONFIG_OBJECT))
self.max_amps = float(config.read_from_config(self.name, "max_amps", config.CONFIG_OBJECT))
self.max_volts = float(config.read_from_config(self.name, "max_volts", config.CONFIG_OBJECT))
self.coil_constant = 0 # coil constant of this axis [T/A]
self.ambient_field = 0 # ambient field in this axis [T]
# ToDo: get this info from settings file
self.coil_constant = float(config.read_from_config(self.name, "coil_const", config.CONFIG_OBJECT))
self.ambient_field = float(config.read_from_config(self.name, "ambient_field", config.CONFIG_OBJECT))
# dynamic information
max_field = self.max_amps * self.coil_constant # calculate max field reachable in this axis
self.max_field = np.array([-max_field, max_field]) # make array with min/max reachable field (w/o compensation)
# calculate max and min field that can be reached after compensating for the ambient field
self.max_comp_field = np.array([self.ambient_field - max_field, self.ambient_field + max_field]) # [min, max]
# initialize dynamic information, this is updated by self.update_status_info() later
self.connected = "Not Connected"
self.output_active = "Unknown" # power output on the PSU enabled?
self.remote_ctrl_active = "Unknown" # remote control on the PSU enabled?
@@ -46,265 +60,362 @@ class Axis:
if self.device is not None:
self.update_status_info()
def update_status_info(self): # Read out the values of the parameters stored in this class and update them
try:
self.device.update_device_information(self.channel)
device_status = self.device.get_device_status_information(self.channel)
if device_status.output_active:
def update_status_info(self): # Read out the values of the dynamic parameters stored in this object and update them
try: # try to read out the data, this will fail on connection error to PSU
self.device.update_device_information(self.channel) # update the information in the device object
device_status = self.device.get_device_status_information(self.channel) # get object with new status info
if device_status.output_active: # is the power output active?
self.output_active = "Active"
else: self.output_active = "Inactive"
else:
self.output_active = "Inactive"
# is remote control active, allowing the device to be controlled by this program?
if device_status.remote_control_active:
self.remote_ctrl_active = "Active"
else: self.remote_ctrl_active = "Inactive"
else:
self.remote_ctrl_active = "Inactive"
# get currents and voltages:
self.voltage = self.device.get_voltage(self.channel)
self.voltage_setpoint = self.device.get_voltage_setpoint(self.channel)
self.current = self.device.get_current(self.channel)
self.current_setpoint = self.device.get_current_setpoint(self.channel)
except (serial.serialutil.SerialException, IndexError):
# print("Connection Error with %s PSU on %s" % (self.name, self.port))
except (serial.serialutil.SerialException, IndexError): # Connection error, usually the PSU is unplugged
if self.connected == "Connected": # only show error messages if the device was connected before this error
# Show error as print-out in console and as pop-up:
ui_print("Connection Error with %s PSU on %s" % (self.name, self.port))
messagebox.showerror("PSU Error", "Connection Error with %s PSU on %s" % (self.name, self.port))
# set status attributes to connection error status:
self.connected = "Connection Error"
self.output_active = "Unknown"
self.remote_ctrl_active = "Unknown"
else:
self.connected = "Connected"
else: # no communications error
self.connected = "Connected" # PSU is connected
if g.ARDUINO.connected == "Connected":
try:
if g.ARDUINO.digitalRead(self.ardPin): # ToDo: Test if this actually works
self.polarity_switched = "True"
else: self.polarity_switched = "False"
except Exception:
print("Error with Arduino")
self.polarity_switched = "Unknown"
g.ARDUINO.connected = "Connection Error"
else:
g.ARDUINO.connected = "Connected"
def print_status(self): # axis = axis control variable, stored in settings.py
print("%s, %0.2f V, %0.2f A"
% (self.device.get_device_status_information(self.channel),
self.device.get_voltage(self.channel), self.device.get_current(self.channel)))
def print_status(self): # print out the current status of the device (not used at the moment)
ui_print("%s, %0.2f V, %0.2f A"
% (self.device.get_device_status_information(self.channel),
self.device.get_voltage(self.channel), self.device.get_current(self.channel)))
def power_down(self): # temporary powerdown, set outputs to 0 but keep connections enabled
self.target_current = 0
self.target_field = 0
self.target_field_comp = 0
self.device.set_voltage(0, self.channel)
self.device.set_current(0, self.channel)
self.device.disable_output(self.channel)
g.ARDUINO.digitalWrite(self.ardPin, "LOW")
try:
# set class object attributes to 0 to reflect shutdown in status displays, log files etc.
self.target_current = 0
self.target_field = 0
self.target_field_comp = 0
def set_signed_current(self, value): # sets current with correct polarity on this axis
device = self.device
channel = self.channel
ardPin = self.ardPin
# print("Attempting to set current", value, "A")
self.target_current = value
if self.connected == "Connected":
if abs(value) > self.max_amps: # prevent excessive currents
self.power_down() # set output to 0 and deactivate
raise ValueError("Invalid current value. Tried %0.2fA, max. %0.2fA allowed" % (value, self.max_amps))
elif value >= 0: # switch polarity as needed
pass # g.ARDUINO.digitalWrite(ardPin, "LOW") ToDo: reactivate and tie to arduino
elif value < 0:
pass # g.ARDUINO.digitalWrite(ardPin, "HIGH") ToDo: reactivate
else:
raise Exception("This should be impossible.")
maxVoltage = min(max(1.1 * self.max_amps * self.resistance, 8), self.max_volts) # limit voltage#
# print("sending values to device: U =", maxVoltage, "I =", abs(value))
device.set_current(abs(value), channel)
device.set_voltage(maxVoltage, channel)
device.enable_output(channel)
else:
print(self.name, "not connected, can't set current.")
if self.device is not None: # there is a PSU connected for this axis
self.device.set_voltage(0, self.channel) # set voltage on PSU channel to 0
self.device.set_current(0, self.channel) # set current on PSU channel to 0
self.device.disable_output(self.channel) # disable power output on PSU channel
g.ARDUINO.digitalWrite(self.ardPin, "LOW") # set arduino pin for polarity switch relay to unpowered state
except Exception as e: # some error was encountered
# show error message:
ui_print("Error while powering down %s: %s" % (self.name, e))
messagebox.showerror("PSU Error!", "Error while powering down %s: \n%s" % (self.name, e))
def set_signed_current(self, value):
# sets current with correct polarity on this axis, this is the primary way to control the test stand
# ui_print("Attempting to set current", value, "A")
self.target_current = value # show target value in object attribute for status display, logging etc.
if abs(value) > self.max_amps: # prevent excessive currents
self.power_down() # set output to 0 and deactivate
raise ValueError("Invalid current value on %s. Tried %0.2fA, max. %0.2fA allowed"
% (self.name, value, self.max_amps))
elif value >= 0: # switch the e-box relay to change polarity as needed
g.ARDUINO.digitalWrite(self.ardPin, "LOW") # command the output pin on the arduino in the electronics box
elif value < 0:
g.ARDUINO.digitalWrite(self.ardPin, "HIGH") # command the output pin on the arduino in the electronics box
# determine voltage limit to be set on PSU, must be high enough to not limit the current:
# min. 8V, max. max_volts, in-between as needed with current value (+margin to not limit current)
maxVoltage = min(max(1.3 * value * self.resistance, 8), self.max_volts) # limit voltage
if self.connected == "Connected": # only try to command the PSU if its actually connected
self.device.set_current(abs(value), self.channel) # set desired current
self.device.set_voltage(maxVoltage, self.channel) # set voltage limit
self.device.enable_output(self.channel) # activate the power output
else: # the PSU is not connected
ui_print(self.name, "not connected, can't set current.")
def set_field_simple(self, value): # forms magnetic field as specified by value, w/o cancelling ambient field
self.target_field = value
self.target_field_comp = value
current = value / self.coil_constant
self.set_signed_current(current)
self.target_field = value # update object attribute for display
self.target_field_comp = value # same as above, bc no compensation
current = value / self.coil_constant # calculate needed current
self.set_signed_current(current) # command the test stand
def set_field(self, value): # forms magnetic field as specified by value, corrected for ambient field
self.target_field = value
field = value - self.ambient_field
self.target_field_comp = field
current = field / self.coil_constant
self.set_signed_current(current)
self.target_field = value # update object attribute for display
field = value - self.ambient_field # calculate needed field after compensation
self.target_field_comp = field # update object attribute for display
current = field / self.coil_constant # calculate needed current
self.set_signed_current(current) # command the test stand
class ArduinoCtrl(Arduino):
# main class to control the electronics box (which means commanding the arduino inside)
# inherits from the Arduino library
def __init__(self, pins):
self.connected = "Unknown"
self.pins = pins
print("Connecting to Arduino...")
try:
Arduino.__init__(self) # search for connected arduino and connect
def __init__(self):
self.connected = "Unknown" # connection status attribute, nominal "Connected"
self.pins = [0, 0, 0] # initialize list with pins to switch relay of each axis
for i in range(3): # get correct pins from the config
self.pins[i] = int(config.read_from_config(g.AXIS_NAMES[i], "relay_pin", config.CONFIG_OBJECT))
ui_print("\nConnecting to Arduino...")
try: # try to set up the arduino
Arduino.__init__(self) # search for connected arduino and connect by initializing arduino library class
for pin in self.pins:
g.ARDUINO.pinMode(pin, "Output")
g.ARDUINO.digitalWrite(pin, "LOW")
except Exception:
print("Connection to Arduino failed.")
self.pinMode(pin, "Output")
self.digitalWrite(pin, "LOW")
except Exception as e: # some error occurred, usually the arduino is not connected
ui_print("Connection to Arduino failed.", e)
self.connected = "Not Connected"
else:
g.arduino_connected = "Connected"
print("Arduino ready.")
else: # connection was successfully established
self.connected = "Connected"
ui_print("Arduino ready.")
def safe(self): # sets output pins to low and closes serial connection
def update_status_info(self): # update the attributes stored in this class object
if self.connected == "Connected": # only do this if arduino is connected (initialize new instance to reconnect)
try: # try to read the status of the pins from the arduino
for axis in g.AXES: # go through all three axes
if g.ARDUINO.digitalRead(axis.ardPin): # pin is HIGH --> relay is switched
axis.polarity_switched = "True" # set attribute in axis object accordingly
else: # pin is LOW --> relay is not switched
axis.polarity_switched = "False" # set attribute in axis object accordingly
except Exception as e: # some error occurred while trying to read status, usually arduino is disconnected
# show warning messages to alert user
ui_print("Error with Arduino:", e)
messagebox.showerror("Error with Arduino!", "Connection Error with Arduino: \n%s" % e)
for axis in g.AXES: # set polarity switch attributes in axis objects to "Unknown"
axis.polarity_switched = "Unknown"
self.connected = "Connection Error" # update own connection status
else: # no error occurred --> data was read successfully
self.connected = "Connected" # update own connection status
def safe(self): # sets relay switching pins to low to depower most of the electronics box
for pin in self.pins:
g.ARDUINO.digitalWrite(pin, "LOW")
self.digitalWrite(pin, "LOW")
def setup_axes(): # creates device objects for all PSUs and sets their values
g.AXES = []
def value_in_limits(axis, key, value): # Check if value is within safe limits (set in globals.py)
# axis is string with axis name, e.g. "X-Axis"
# key specifies which value to check, e.g. current
max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(axis)] # get max value from dictionary in globals.py
min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(axis)] # get min value from dictionary in globals.py
print("Connecting to XY Device on %s..." % g.XY_PORT)
try:
if g.XY_DEVICE is not None:
print("closing serial connection on XY device")
if float(value) > float(max_value): # value is too high
return 'HIGH'
elif float(value) < float(min_value): # value is too low
return 'LOW'
else: # value is within limits
return 'OK'
def setup_all(): # main test stand initialization function
# creates device objects for all PSUs and Arduino and sets their values
# initializes an object of class Axis for all three axes (x,y,z)
# Setup Arduino:
try: # broad error handling for unforeseen errors, handling in ArduinoCtrl should catch most errors
if g.ARDUINO is not None: # the arduino has been initialized before, so we need to first close its connection
try:
g.ARDUINO.close() # close serial link
except serial.serialutil.SerialException:
pass
# serial.flush() in Arduino.close() fails when reconnecting
# this ignores it and allows serial.close() to execute (I think)
except AttributeError:
pass
# when no Arduino is connected but g.ARDUINO has been initialized then there is nothing to close
# this throws an exception, which can be ignored
g.ARDUINO = ArduinoCtrl() # initialize the arduino object from the control class, connects and sets up
except Exception as e: # some unforeseen error occurred
# show error messages to alert user
ui_print("Arduino setup failed:", e)
ui_print(traceback.print_exc())
messagebox.showerror("Error!", "Arduino setup failed:\n%s \nCheck traceback in console." % e)
# Setup PSUs and axis objects:
g.AXES = [] # initialize global list containing the three axis objects
# get serial ports for the PSUs from config
g.XY_PORT = config.read_from_config("PORTS", "xy_port", config.CONFIG_OBJECT)
g.Z_PORT = config.read_from_config("PORTS", "z_port", config.CONFIG_OBJECT)
g.PORTS = [g.XY_PORT, g.XY_PORT, g.Z_PORT] # write list with PSU port for each axis (X/Y share PSU)
# setup PSU and axis objects for X and Y axes:
ui_print("\nConnecting to XY Device on %s..." % g.XY_PORT)
try: # try to connect to the PSU
if g.XY_DEVICE is not None: # if PSU has previously been connected we need to close the serial link first
ui_print("Closing serial connection on XY device")
g.XY_DEVICE.serial.close()
g.XY_DEVICE = None
g.XY_DEVICE = PS2000B.PS2000B(g.XY_PORT) # setup PSU
print("Connection established.")
g.X_AXIS = Axis(0, g.XY_DEVICE, 0, g.ARDUINO.pins[0]) # create axis objects
ui_print("Connection established.")
g.X_AXIS = Axis(0, g.XY_DEVICE, 0, g.ARDUINO.pins[0]) # create axis objects (index, PSU, channel, relay pin)
g.Y_AXIS = Axis(1, g.XY_DEVICE, 1, g.ARDUINO.pins[1])
except serial.serialutil.SerialException:
g.X_AXIS = Axis(0, None, 0, g.ARDUINO.pins[0]) # create axis objects
except serial.serialutil.SerialException: # communications error, usually PSU is not connected or wrong port set
g.X_AXIS = Axis(0, None, 0, g.ARDUINO.pins[0]) # create axis objects without the PSU
g.Y_AXIS = Axis(1, None, 1, g.ARDUINO.pins[1])
print("XY Device not connected or incorrect port set.")
ui_print("XY Device not connected or incorrect port set.")
print("Connecting to Z Device on %s..." % g.XY_PORT)
# same for the Z axis
ui_print("Connecting to Z Device on %s..." % g.Z_PORT)
try:
if g.Z_DEVICE is not None:
ui_print("Closing serial connection on Z device")
g.Z_DEVICE.serial.close()
g.Z_DEVICE = None
g.Z_DEVICE = PS2000B.PS2000B(g.Z_PORT)
print("Connection established.")
ui_print("Connection established.")
g.Z_AXIS = Axis(2, g.Z_DEVICE, 0, g.ARDUINO.pins[2])
except serial.serialutil.SerialException:
g.Z_AXIS = Axis(2, None, 0, g.ARDUINO.pins[2])
print("Z Device not connected or incorrect port set.")
ui_print("Z Device not connected or incorrect port set.")
# put newly created axis objects into a list for access later
g.AXES.append(g.X_AXIS)
g.AXES.append(g.Y_AXIS)
g.AXES.append(g.Z_AXIS)
i = 0
for axis in g.AXES: # ToDo: move to axis init
axis.resistance = g.RESISTANCES[i]
axis.max_watts = g.MAX_WATTS[i]
axis.max_amps = np.sqrt(axis.max_watts / axis.resistance)
print(axis.name, "max Current:", axis.max_amps)
axis.max_volts = g.MAX_VOLTS[i]
axis.coil_constant = g.COIL_CONST[i]
axis.ambient_field = g.AMBIENT_FIELD[i]
i = i+1
ui_print("") # print new line
def activate_all(): # enables remote control and output on all PSUs and channels
g.XY_DEVICE.enable_all()
g.Z_DEVICE.enable_all()
def deactivate_all(): # disables remote control and output on all PSUs and channels
# ToDo: add check if device is connected
try:
g.XY_DEVICE.disable_all()
except BaseException:
print("XY PSU deactivation unsuccessful.")
else:
print("XY PSU deactivated.")
try:
g.Z_DEVICE.disable_all()
except BaseException:
print("Z PSU deactivation unsuccessful.")
else:
print("Z PSU deactivated.")
def print_status_3():
print("X-Axis:")
g.X_AXIS.print_status()
print("Y-Axis:")
g.Y_AXIS.print_status()
print("Z-Axis:")
g.Z_AXIS.print_status()
def set_to_zero(device): # sets voltages and currents to 0
def set_to_zero(device): # sets voltages and currents to 0 on all channels of a specific PSU
device.voltage1 = 0
device.current1 = 0
device.voltage2 = 0
device.current2 = 0
def power_down_all(): # temporary, set all outputs to 0 but keep connections enabled
set_to_zero(g.XY_DEVICE)
set_to_zero(g.Z_DEVICE)
g.ARDUINO.safe()
def power_down_all(): # on all PSUs set all outputs to 0 but keep connections enabled
for axis in g.AXES:
axis.power_down() # set outputs to 0 and pin to low on this axis
def shut_down_all(): # shutdown at program end or on error, set outputs to 0 and disable connections
# ToDo: better messages, check if things are connected first
print("\nAttempting to safely shut down all devices. Check equipment to confirm.")
try: set_to_zero(g.XY_DEVICE)
except:
print("XY PSU set to 0 unsuccessful.")
def shut_down_all(): # safe shutdown at program end or on error
# set outputs to 0 and disable connections on all devices
ui_print("\nAttempting to safely shut down all devices. Check equipment to confirm.")
# start writing string to later show how shutdown on all devices went in a single info pop-up:
message = "Tried to shut down all devices. Check equipment to confirm."
# Shut down PSUs:
if g.XY_DEVICE is not None: # the PSU has been setup before
try: # try to safe the PSU
set_to_zero(g.XY_DEVICE) # set currents and voltages to 0 for both channels
g.XY_DEVICE.disable_all() # disable power output on both channels
except BaseException as e: # some error occurred, usually device has been disconnected
ui_print("Error while deactivating XY PSU:", e) # print the problem in the console
message += "\nError while deactivating XY PSU: %s" % e # append status to the message to show later
else: # device was successfully deactivated
ui_print("XY PSU deactivated.")
message += "\nXY PSU deactivated." # append message to show later
else: # the device was not connected before
# tell user there was no need/no possibility to deactivate:
ui_print("XY PSU not connected, can't deactivate.")
message += "\nXY PSU not connected, can't deactivate."
# same as above
if g.Z_DEVICE is not None:
try:
set_to_zero(g.Z_DEVICE)
g.Z_DEVICE.disable_all()
except BaseException as e:
ui_print("Error while deactivating Z PSU:", e)
message += "\nError while deactivating Z PSU: %s" % e
else:
ui_print("Z PSU deactivated.")
message += "\nZ PSU deactivated."
else:
print("XY PSU currents and voltages set to 0.")
try: set_to_zero(g.Z_DEVICE)
except:
print("Z PSU set to 0 unsuccessful.")
else:
print("Z PSU currents and voltages set to 0.")
deactivate_all()
try: g.ARDUINO.safe()
except:
print("Arduino safing unsuccessful.")
# else: # commented out bc this throws no exception, even when arduino is not connected
# print("Arduino pins set to LOW.") # ToDo: figure out error handling for this
try: g.ARDUINO.close()
except:
print("Closing Arduino connection failed.")
else:
print("Serial connection to Arduino closed.")
ui_print("Z PSU not connected, can't deactivate.")
message += "\nZ PSU not connected, can't deactivate."
# Shut down Arduino:
try:
g.ARDUINO.safe() # call safe method in ArduinoCtrl class (all relay pins to LOW)
except BaseException as e: # some error occurred
ui_print("Arduino safing unsuccessful:", e)
message += "\nArduino safing unsuccessful: %s" % e # append to the message to show later
# this throws no exception, even when arduino is not connected
# ToDo (optional): figure out error handling for this
try:
g.ARDUINO.close() # close the serial link
except BaseException as e: # something went wrong there
if g.ARDUINO.connected == "Connected": # Arduino was connected, some error occurred
ui_print("Closing Arduino connection failed:", e)
message += "\nClosing Arduino connection failed: %s" % e
else: # Arduino was not connected, so error is expected
ui_print("Arduino not connected, can't close connection.")
message += "\nArduino not connected, can't close connection."
else: # no problems, connection was successfully closed
ui_print("Serial connection to Arduino closed.")
message += "\nSerial connection to Arduino closed."
messagebox.showinfo("Program ended", message) # Show a unified pop-up with how the shutdown on each device went
def set_field_simple(vector): # forms magnetic field as specified by vector, w/o cancelling ambient field
for i in [0, 1, 2]:
g.AXES[i].set_field_simple(vector[i])
try:
g.AXES[i].set_field_simple(vector[i]) # try to set the field on each axis
except ValueError as e: # a limit was violated, usually the needed current was too high
ui_print(e) # let the user know
def set_field(vector): # forms magnetic field as specified by vector, corrected for ambient field
# same as set_field_simple(), but with compensation
for i in [0, 1, 2]:
g.AXES[i].set_field(vector[i])
try:
g.AXES[i].set_field(vector[i])
except ValueError as e:
ui_print(e)
def set_current_vec(vector): # sets needed currents on each axis for given vector
def set_current_vec(vector): # sets currents on each axis according to given vector
i = 0
for axis in g.AXES:
axis.set_signed_current(vector[i])
i = i + 1
try:
axis.target_field = 0 # set target field attribute to 0 to show that current, not field is controlled atm
axis.target_field_comp = 0 # as above
axis.set_signed_current(vector[i]) # command test stand to set the current
except ValueError as e: # current was too high
ui_print(e) # print out the error message
i += 1
def execute_csv(filepath, printing=0): # runs through csv file containing times and desired field vectors
# csv format: time (s); xField (T); yField (T); zField (T)
# decimal commas
print("Reading File:", filepath)
file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file
array = file.to_numpy() # convert csv to array
t_zero = time.time()
t_ref = t_zero
i = 0
print("Starting Execution...")
while i < len(array):
t = time.time() - t_zero
if t >= array[i, 0]:
field_vec = array[i, 1:4]
print("t = %0.2f s, target field vector = " % (array[i, 0]), field_vec)
set_field(field_vec)
i = i + 1
if t - t_ref >= 1 and printing == 1: # print status every second
print_status_3()
t_ref = t
print("File executed, powering down channels.")
power_down_all() # set currents and voltages to 0, set arduino pins to low
def devices_ok(xy_off=False, z_off=False, arduino_off=False):
# check if all devices are connected, return True if yes
# checks for individual devices can be disabled by parameters above (default not disabled)
try: # handle errors while checking connections
if not xy_off: # if check for this device is not disabled
if g.XY_DEVICE is not None: # has the handle for this device been set?
g.X_AXIS.update_status_info() # update info --> this actually communicates with the device
if g.X_AXIS.connected != "Connected": # if not connected
return False # return and exit function
else: # if handle has not been set the device is inactive --> not ok
return False
if not z_off: # same as above
if g.Z_DEVICE is not None:
g.Z_AXIS.update_status_info()
if g.Z_AXIS.connected != "Connected":
return False
else:
return False
if not arduino_off: # check not disabled
g.ARDUINO.update_status_info() # update status info --> attempts communication
if g.ARDUINO.connected != "Connected":
return False
except Exception as e: # if an error is encountered while checking the devices
messagebox.showerror("Error!", "Error while checking devices: \n%s" % e) # show error pop-up
return False # clearly something is not ok
else: # if nothing has triggered so far all devices are ok --> return True
return True
+131
View File
@@ -0,0 +1,131 @@
# This file contains functions and variables related to reading and writing configuration files.
# The configparser module is used for processing. Config files are of type .ini
# import packages:
from configparser import ConfigParser
from tkinter import messagebox
# import other project files:
import globals as g
import cage_func as func
# noinspection PyPep8Naming
import User_Interface as ui
global CONFIG_FILE # string storing the path of the used config file
global CONFIG_OBJECT # object of type ConfigParser(), storing all configuration information
# CONFIG_OBJECT is what is mostly read/written by the program
# CONFIG_FILE is only used to export/import to/from a file
def get_config_from_file(file): # read a config file to a config_object
config_object = ConfigParser() # initialize config parser
config_object.read(file) # open config file
return config_object # return config object, that contains all info from the file
def write_config_to_file(config_object): # write contents of config object to a config file
with open(CONFIG_FILE, 'w') as conf: # Write changes to config file
config_object.write(conf)
def read_from_config(section, key, config_object): # read a specific value from a config object
try:
section_obj = config_object[section] # get relevant section
value = section_obj[key] # get relevant value in the section
return value
except KeyError as e: # a section or key was used, that does not exist
ui.ui_print("Error while reading config file:", e)
raise KeyError("Could not find key", key, "in config file.")
def edit_config(section, key, value, override=False): # edit a specific value in the CONFIG_OBJECT
# section: Section of the config, e.g. "X-Axis" or "PORTS"
# key: name of the value in the section, e.g. max_amps
# value: new value to be written into the config
# override: Bool to allow user to force writing a value into the config, even if it exceeds the safe limit
global CONFIG_OBJECT # get the global config object to edit it
# ToDo (optional): add check for data types, e.g. int for arduino ports
# Check if value to write is within acceptable limits (set in dictionary in globals.py):
try:
value_ok = 'OK'
if section in g.AXIS_NAMES and not override: # only check values in axis sections and not if check overridden
value_ok = func.value_in_limits(section, key, value) # check if value is ok, too high or too low
if value_ok == 'HIGH': # value is too high
max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(section)] # get max value for message printing
message = "Prevented writing too high value for {s} {k} to config file:\n" \
"{v}, max. {mv} allowed. Erroneous values may damage equipment!" \
.format(s=section, k=key, v=value, mv=max_value)
raise ValueError(message) # return an error with the message attached
elif value_ok == 'LOW': # value is too low
min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] # get min value for message printing
message = "Prevented writing too low value for {s} {k} to config file:\n" \
"{v}, max. {mv} allowed. Erroneous values may damage equipment!" \
.format(s=section, k=key, v=value, mv=min_value)
raise ValueError(message) # return an error with the message attached
if value_ok == 'OK' or override: # value is within limits or user has overridden the checks
try:
section_obj = CONFIG_OBJECT[section] # get relevant section in the config
except KeyError: # there is no such section
ui.ui_print("Could not find section", section, "in config file, creating new.")
CONFIG_OBJECT.add_section(section) # create the missing section
section_obj = CONFIG_OBJECT[section] # get the object of the section
try:
section_obj[key] = str(value) # set value for correct entry in the section
except KeyError: # there is no entry with this key
ui.ui_print("Could not find key", key, "in config file, creating new.")
CONFIG_OBJECT.set(section, key, str(value)) # create the entry and set the value
except KeyError as e: # key for section or specific value does not exist in the dictionary for max/min values
ui.ui_print("Error while editing config file:", e)
raise KeyError("Could not find key", key, "in config file.") # return an error
def check_config(config_object): # check all numeric values in the config and see if they are within safe limits
ui.ui_print("Checking config file for values exceeding limits:")
concerns = {} # initialize dictionary for found problems
problem_counter = 0 # count the number of values that exceed limits
i = 0
for axis in g.AXIS_NAMES: # go through all 3 axes
concerns[axis] = [] # create dictionary entry for this axis
for key in g.default_arrays.keys(): # go over entries in this axis
value = float(read_from_config(axis, key, config_object)) # read value to check from config file
max_value = g.default_arrays[key][1][i] # get max value
min_value = g.default_arrays[key][2][i] # get min value
if not min_value <= value <= max_value: # value is not in safe limits
concerns[axis].append(key) # add this entry to the problem dictionary
problem_counter += 1
if len(concerns[axis]) == 0: # no problems were found for this axis
concerns[axis].append("No problems detected.")
ui.ui_print(axis, ":", *concerns[axis]) # print out results for this axis
i += 1
if problem_counter > 0: # some values are not ok
# shop pup-up warning message:
messagebox.showwarning("Warning!", "Found %i value(s) exceeding limits in config file. Check values "
"to ensure correct operation and avoid equipment damage!" % problem_counter)
g.app.show_frame(ui.Configuration) # open configuration window so user can check values
def reset_config_to_default(): # reset values in config object to defaults (set in globals.py)
config = ConfigParser() # reinitialize empty config object
global CONFIG_OBJECT # get the global config object
CONFIG_OBJECT = config # reset it to the empty object
i = 0
for axis_name in g.AXIS_NAMES: # go through axes
config.add_section(axis_name) # add section for this axis
for key in g.default_arrays.keys(): # go through dictionary with default values
config.set(axis_name, key, str(g.default_arrays[key][0][i])) # set values
i += 1
config.add_section("PORTS") # add section for PSU serial ports
for key in g.default_ports.keys(): # go through dictionary of default serial ports
config.set("PORTS", key, str(g.default_ports[key])) # set the value for each axis
+99
View File
@@ -0,0 +1,99 @@
# This file contains functions related to logging data from the program to a CSV file.
# They are mainly but not only called by the ConfigureLogging class in User_Interface.py.
# import packages
import pandas as pd
import globals as g
from datetime import datetime
import os
from tkinter import filedialog
from tkinter import messagebox
# import other project files
import User_Interface as ui
log_data = pd.DataFrame() # pandas data frame containing the logged data, in-program representation of csv/excel data
unsaved_data = False # Bool to indicate if there is unsaved data, set to True each time a datapoint is logged
zero_time = datetime.now() # set reference for timestamps in log file, reset when log_data is cleared and restarted
# create dictionary with all value handles that could be logged
# Key: String that is displayed in UI and column headers. Also serves as handle to access dictionary elements.
# Keys are the same as the rows in the status display ToDo (optional): use this for the status display
# Content: name of the corresponding attribute in the Axis class (in cage_func.py).
# Important: attribute handle must match definition in Axis class exactly, used with axis.getattr() to get values.
axis_data_dict = {
'PSU Status': 'connected',
'Voltage Setpoint': 'voltage_setpoint',
'Actual Voltage': 'voltage',
'Current Setpoint': 'current_setpoint',
'Actual Current': 'current',
'Target Field': 'target_field_comp',
'Trgt. Field Raw': 'target_field_comp',
'Target Current': 'target_current',
'Inverted': 'polarity_switched'
}
def triple_list(key_list): # creates list with each entry of key_list tripled with axis names before it
new_list = [] # initialize list
for key in key_list: # go through the given list
for axis_name in ['X', 'Y', 'Z']: # per given list entry create three, one for each axis
new_list.append(' '.join((axis_name, key))) # put axis_name before the given entry and append to new list
return new_list
def log_datapoint(key_list): # logs a single row of data into the log_data DataFrame
# key_list determines what data is logged
global log_data # get global dataframe with logged data
global unsaved_data # get global variable that indicates if there is unsaved data
date = datetime.now().date() # get current date
time = datetime.now().strftime("%H:%M:%S,%f") # get string with current time in correct format
t = (datetime.now() - zero_time).total_seconds() # calculate timestamp relative to the start of the logging
data = [[date, time, t]] # initialize new data row with timestamps
for key in key_list: # go through the list telling us what data to log
for axis in g.AXES: # log this data for each axis
# get relevant value from the correct AXIS object and append to new data row:
data[0].append(getattr(axis, axis_data_dict[key]))
column_names = ["Date", "Time", "t (s)", *triple_list(key_list)] # create list with the correct column headers
new_row = pd.DataFrame(data, columns=column_names) # create data frame containing the new row
log_data = log_data.append(new_row, ignore_index=True) # append the new data frame to the logged data
unsaved_data = True # tell other program parts that there is now unsaved data
def select_file(): # select a file to write logs to
directory = os.path.abspath(os.getcwd()) # get project directory
# open file selection dialogue and save path of selected file
filepath = filedialog.asksaveasfilename(initialdir=directory, title="Set log file",
filetypes=([("Comma Separated Values", "*.csv*")]),
defaultextension=[("Comma Separated Values", "*.csv*")])
if filepath == '': # this happens when file selection window is closed without selecting a file
ui.ui_print("No file selected, can not save logged data.")
return None
else: # a valid file name was entered
return filepath
def write_to_file(dataframe, filepath):
# get global variables for use in this function:
global unsaved_data
if filepath is not None: # user has selected a file and no errors occurred
ui.ui_print("Writing logged data to file", filepath)
try:
# write data collected in log_data DataFrame to csv file in german excel format:
dataframe.to_csv(filepath, index=False, sep=';', decimal=',')
except PermissionError:
message = "No permission to write to: \n%s. \nFile may be open in another program." % filepath
messagebox.showerror("Permission Error", message)
except BaseException as e:
message = "Error while trying to write to file \n%s.\n%s" % (filepath, e)
messagebox.showerror("Error!", message)
else: # no exceptions occurred
unsaved_data = False # tell everything that there is no unsaved data remaining
ui.ui_print("Log data saved to", filepath)
def clear_logged_data(): # clears all logged data from data frame
global log_data # get global variable
log_data = pd.DataFrame() # reset to an empty data frame, i.e. clearing all logged data
+166
View File
@@ -0,0 +1,166 @@
# tThis file contains code for executing a sequence of magnetic fields from a csv file.
# To do this without crashing the UI it has to run in a separate thread using the threading module.
# import packages:
import time
import pandas
from threading import *
from tkinter import messagebox
import matplotlib.pyplot as plt
# import other project files:
import User_Interface as ui
import cage_func as func
import globals as g
class ExecCSVThread(Thread):
# main class for executing a CSV sequence
# it inherits the threading.Thread class, enabling sequence execution in a separate thread
def __init__(self, array, parent, controller):
Thread.__init__(self)
self.array = array # numpy array containing data from csv to be executed
self.parent = parent # object from which this class is called, here the ExecuteCSVMode object of the UI
self.controller = controller # object on which mainloop() is running, usually the main UI window
self.__stop_event = Event() # event which can be set to stop the thread execution if needed
def run(self): # called to start the execution of the thread
ui.ui_print("Starting Sequence Execution...")
self.execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence
# when the sequence has ended, reset buttons on the UI:
if not g.exitFlag: # main window is open
self.parent.select_file_button["state"] = "normal"
self.parent.execute_button["state"] = "normal"
self.parent.stop_button["state"] = "disabled"
self.parent.reinit_button["state"] = "normal"
# setup ability to interrupt thread (https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread)
def stop(self): # stop thread execution, can be called from another thread to kill this one
self.__stop_event.set()
def stopped(self): # returns true if the thread has been stopped, used to check if a run should continue
return self.__stop_event.is_set()
def execute_sequence(self, array, delay, parent, controller):
# main execution method of the class
# runs through array with times and desired fields and commands test stand accordingly
# array format: [time (s), xField (T), yField (T), zField (T)]
func.power_down_all() # sets outputs on PSUs to 0 and Arduino pins to LOW before starting
t_zero = time.time() # set reference time for start of run
# Check if everything is properly connected:
all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(),
parent.arduino_override.get())
# True or False depending on devices status, checks for some devices may be overridden by user
i = 0 # index of the current array row
while i < len(array) and all_connected and not self.stopped() and not g.exitFlag:
# while array is not finished, devices are connected, user has not cancelled and application is running
t = time.time() - t_zero # get time relative to start of run
if t >= array[i, 0]: # time for this row has come
g.threadLock.acquire() # execute all lines until threadLock.release() before going back to main thread
# check if everything is still connected before sending commands:
all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(),
parent.arduino_override.get())
if all_connected:
field_vec = array[i, 1:4] # extract desired field vector
ui.ui_print("%0.5f s: t = %0.2f s, target field vector ="
% (time.time() - t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT")
func.set_field(field_vec) # send field vector to test stand
controller.StatusDisplay.update_labels() # update status display after change
# log change to the log file if user has selected event logging in the Configure Logging window
logger = controller.pages[ui.ConfigureLogging] # get object of logging configurator
if logger.event_logging: # data should be logged when test stand is commanded
logger.log_datapoint() # log data
i = i + 1 # next row
g.threadLock.release() # allow going back to main thread now
elif t <= array[i, 0] - delay - 0.02: # is there enough time to sleep before the next row?
time.sleep(delay) # sleep to give other threads time to run
if not self.stopped() and not g.exitFlag and all_connected: # sequence ended without interruption
ui.ui_print("Sequence executed, powering down channels.")
elif all_connected: # interrupted by user
ui.ui_print("Sequence cancelled, powering down channels.")
elif not all_connected: # interrupted by device error
ui.ui_print("Error with at least one device, sequence aborted.")
messagebox.showwarning("Device Error!", "Error with at least one device, sequence aborted.")
else: # if this happens there is a mistake in the logic above, it really should not
# tell the user something weird happened:
ui.ui_print("Encountered unexpected sequence end state:"
"\nThread Stopped:", self.stopped(), ", Application Closed:", g.exitFlag,
", Devices connected:", all_connected)
messagebox.showwarning("Unexpected state",
"Encountered unexpected sequence end state, see console output for details.")
func.power_down_all() # set currents and voltages to 0, set arduino pins to low
def read_csv_to_array(filepath): # convert a given csv file to a numpy array
# csv format: time (s); xField (T); yField (T); zField (T) (german excel)
# decimal commas
file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file without column headers
array = file.to_numpy() # convert csv to array
return array
def check_array_ok(array):
# check if any magnetic fields in an array exceed the test stand limits and if so display a warning message
values_ok = True
for i in [0, 1, 2]: # go through axes/columns
max_val = g.AXES[i].max_comp_field[1] # get limits the test stand can do
min_val = g.AXES[i].max_comp_field[0]
data = array[:, i + 1] # extract data for this axis from array
# noinspection PyTypeChecker
if any(data > max_val) or any(data < min_val): # if any datapoint is out of bounds
values_ok = False
if not values_ok: # show warning pop-up if values are exceeding limits
messagebox.showwarning("Value Limits Warning!", "Found field values exceeding limits of test stand."
"\nSee plot and check values in csv.")
def plot_field_sequence(array, width, height): # create plot of fixed size (pixels) from array
# ToDo (optional): polar plots, plots of angle...
# ToDo (optional): show graphs as steps (as performed by test stand)
fig_dpi = 100 # set figure resolution (dots per inch)
px = 1/fig_dpi # get pixel to inch size conversion
figure = plt.Figure(figsize=(width*px, height*px), dpi=fig_dpi) # create figure with correct size
# noinspection PyTypeChecker,SpellCheckingInspection
axes = figure.subplots(3, sharex=True, sharey=True, gridspec_kw={'hspace': 0.4}) # create subplots with shared axes
figure.suptitle("Magnetic Field Sequence") # set figure title
t = array[:, 0] # extract time column
for i in [0, 1, 2]: # go through all three axes
data = array[:, i + 1] * 1e6 # extract field column of this axis and convert to microtesla
max_val = g.AXES[i].max_comp_field[1] * 1e6 # get limits of achievable field
min_val = g.AXES[i].max_comp_field[0] * 1e6
plot = axes[i] # get appropriate subplot
plot.plot(t, data, linestyle='solid', marker='.') # plot data
if any(data > max_val): # if any value is higher than the maximum
plot.axhline(y=max_val, linestyle='dashed', color='r') # plot horizontal line to show maximum
# add label to line:
plot.text(t[-1], max_val, "max", horizontalalignment='center', verticalalignment='top', color='r')
if any(data < min_val): # same as above
plot.axhline(y=min_val, linestyle='dashed', color='r')
plot.text(t[-1], min_val, "min", horizontalalignment='center', color='r')
plot.set_title(g.AXIS_NAMES[i], size=10) # set subplot title (e.g. "X-Axis")
# set shared axis labels:
axes[2].set_xlabel("Time (s)")
axes[1].set_ylabel("Magnetic Field (\u03BCT)")
return figure # return the created figure to be inserted somewhere
+49
View File
@@ -0,0 +1,49 @@
# This file is used to hold global variables that are used by more than one file of the program.
# Instead of always passing all variables to functions, this file can simply be imported to get them.
import numpy as np
XY_DEVICE = None # XY PSU object will be stored here (class PS2000B)
Z_DEVICE = None # Z PSU object will be stored here (class PS2000B)
ARDUINO = None # Arduino object will be stored here (class ArduinoCtrl)
# Axis objects will be stored here (class Axis)
X_AXIS = None
Y_AXIS = None
Z_AXIS = None
AXES = None # list containing [X_AXIS, Y_AXIS, Z_AXIS]
app = None # Main Tkinter application object will be stored here (class HelmholtzGUI)
AXIS_NAMES = ["X-Axis", "Y-Axis", "Z-Axis"] # list with the names of each axis, used mainly for printing functions
global XY_PORT # serial port for XY PSU will be stored here (string)
global Z_PORT # serial port for Z PSU will be stored here (string)
global PORTS # list containing [XY_PORT, XY_PORT, Z_PORT], used in loops where info on each axis is needed
global threadLock # thread locking object, used to force threads to perform actions in a certain order (threading.Lock)
exitFlag = True # False when main window is open, True otherwise
# Create dictionaries with default Constants and maximum/minimum values
# Used to create default configs and to check if user inputs are within safe limits
# ToDo: check actual maximum ratings (or refine after testing)
# ToDo: put this into a config file
# Dictionary for numerical values:
# format: key: [default values], [maximum values], [minimum values]
default_arrays = {
"coil_const": np.array([[38.6, 38.45, 37.9], [50, 50, 50], [0, 0, 0]]) * 1e-6, # Coil constants [x,y,z] [T/A]
"ambient_field": np.array([[30, 30, 30], [200, 200, 200], [-200, -200, -200]]) * 1e-6, # ambient magnetic field [T]
"resistance": np.array([[1.7, 1.7, 1.7], [5, 5, 5], [1, 1, 1]], dtype=float), # resistance of circuits [Ohm]
"max_volts": np.array([[14, 14, 14], [16, 16, 16], [0, 0, 0]], dtype=float), # max. voltage, limited to 16V by used diodes! [V]
"max_amps": np.array([[4.5, 4.5, 4.5], [6, 6, 6], [0, 0, 0]], dtype=float), # max. allowed current (A)
"relay_pin": [[15, 16, 17], [15, 16, 17], [15, 16, 17]] # pins on the arduino for reversing [x,y,z] polarity
}
# Dictionary for PSU serial ports:
default_ports = {
"xy_port": "COM1", # Default serial port where PSU for X- and Y-Axes is connected
"z_port": "COM2", # Default serial port where PSU for Z-Axis is connected
}
+69 -19
View File
@@ -1,28 +1,78 @@
import User_Interface as ui
import cage_func as func
# Main file of the program. Run this file to start the application.
# import packages:
from os.path import exists
import traceback
import settings as g
from tkinter import messagebox
# import other project files:
import cage_func as func
from User_Interface import HelmholtzGUI
from User_Interface import ui_print
import User_Interface as ui
import globals as g
import config_handling as config
import csv_logging as log
def program_end(): # called on exception or when user closes application
# safely shuts everything down and saves any unsaved data
g.exitFlag = True # tell everything else the application has been closed
if g.app is not None: # the main Tkinter app object has been initialized before
if g.app.pages[ui.ExecuteCSVMode].csv_thread is not None: # check if a thread for executing CSVs exists
g.app.pages[ui.ExecuteCSVMode].csv_thread.stop() # stop the thread
func.shut_down_all() # shut down devices
if log.unsaved_data: # Check if there is logged data that has not been saved yet
# open pop-up to ask user if he wants to save the data:
save_log = messagebox.askquestion("Save log data?", "There seems to be unsaved logging data. "
"Do you wish to write it to a file now?")
if save_log == 'yes': # user has chosen yes
filepath = log.select_file() # let user select a file to write to
log.write_to_file(log.log_data, filepath) # write the data to the chosen file
if g.app is not None:
g.app.destroy() # close application
try: # start normal operations
# Connect to Arduino:
g.ARDUINO = func.ArduinoCtrl(g.RELAY_PINS)
print("Connecting to PSUs...")
func.setup_axes() # initiate communication, set handles
config.CONFIG_FILE = 'config.ini' # set the config file path
# ToDo: remember what the last config file was
if not exists(config.CONFIG_FILE): # config file does not exist yet
print("Config file not found, creating new from defaults.")
config.reset_config_to_default() # create configuration object from defaults
config.write_config_to_file(config.CONFIG_OBJECT) # write the configuration object to a new file
config.CONFIG_OBJECT = config.get_config_from_file(config.CONFIG_FILE) # read configuration data from config file
print("Starting setup...")
func.setup_all() # initiate communication with devices and initialize all major program objects
print("\nOpening User Interface...")
'''g.TestValuesX = ui.TestValues()
g.TestValuesY = ui.TestValues()
#g.TestValuesZ = ui.TestValues()
g.TestValues = [g.TestValuesX, g.TestValuesY]#, g.TestValuesZ]'''
application = ui.HelmholtzGUI()
application.mainloop()
g.app = HelmholtzGUI() # initialize user interface
g.exitFlag = False # tell all functions that the user interface is now running
g.app.state('zoomed') # open UI in maximized window
g.app.StatusDisplay.continuous_label_update(g.app, 500) # initiate regular Status Display updates (ms)
ui_print("Program Initialized")
config.check_config(config.CONFIG_OBJECT) # check config for values exceeding limits
except BaseException as e: # if there is an error, print what happened
ui_print("\nStarting setup...") # do setup again, so it is printed in the UI console ToDo: do it only once
func.setup_all() # initiate communication with devices and initialize all major program objects
g.app.protocol("WM_DELETE_WINDOW", program_end) # call program_end function if user closes the application
g.app.mainloop() # start main program loop
except Exception as e: # An error has occurred somewhere in the program
print("\nAn error occurred, Shutting down.")
print(e)
print(traceback.print_exc())
finally: # safely shut everything down at the end
func.shut_down_all()
# shop pup-up error message:
message = "%s.\nSee python console traceback for more details. " \
"\nShutting down devices, check equipment to confirm." % e
messagebox.showerror("Error!", message)
print(traceback.print_exc()) # print error traceback in the python console
program_end() # safely close everything and shut down devices
-36
View File
@@ -1,36 +0,0 @@
import numpy as np
import settings as g
import cage_func as func
# from pyps2000b import PS2000B
from Arduino import Arduino
# User Inputs/Configuration----------------------------------
# Desired output:
mag_vec1 = np.array([10, 10, 5])*1e-6
# Constants:
g.COIL_CONST = np.array([38.6, 38.45, 37.9]) * 1e-9 # Coil constants [x,y,z] in T/A
g.AMBIENT_FIELD = np.array([80]) * 1e-6 # ambient magnetic field in measurement area, to be cancelled out
g.RESISTANCES = np.array([3.9, 1, 1]) # resistance of [x,y,z] circuits
g.MAX_WATTS = np.array([8, 0, 0]) # max. allowed power for [x,y,z] circuits
g.MAX_VOLTS = 16 # max. allowed voltage, limited to 16V by used diodes!
# COM-Ports for power supply units:
g.XY_PORT = "COM1" # placeholders
g.Z_PORT = "COM2"
# Code starts here------------------------------------------
g.MAX_AMPS = np.sqrt(g.MAX_WATTS / g.RESISTANCES) # calculate maximum currents in each axis
print("Connecting to PSUs...")
func.setup_axes() # initiate communication, set handles
print("Connecting to Arduino...")
g.ARDUINO = Arduino() # search for connected arduino and set handle
print("Arduino found, configuring pins.")
func.setup_arduino()
print("Activating PSU outputs...")
func.activate_all() # activate remote control and outputs on PSUs
func.set_field_simple(mag_vec1)
func.deactivate_all()
View File
+2
View File
@@ -1,3 +1,5 @@
# This file enables communication with PS2000B Power Supply Units.
#!/usr/bin/env python3
# coding=utf-8
# Python access to Elektro Automatik PS 2000 B devices via USB/serial
+2 -1
View File
@@ -1,4 +1,5 @@
numpy==1.19.3 # bug in numpy 1.19.4, 1.19.3 used as workaround
pyserial~=3.5
future~=0.18.2
pandas
pandas~=1.1.5
matplotlib~=3.3.2
-35
View File
@@ -1,35 +0,0 @@
import numpy as np
XY_DEVICE = None
Z_DEVICE = None
X_AXIS = None # object structure: (device, channel, arduino pin, axis index)
Y_AXIS = None
Z_AXIS = None
AXES = None # list containing [X_AXIS, Y_AXIS, Z_AXIS]
# Constants:
COIL_CONST = np.array([38.6, 38.45, 37.9]) * 1e-6 # Coil constants [x,y,z] in T/A
AMBIENT_FIELD = np.array([80, 80, 80]) * 1e-6 # ambient magnetic field in measurement area, to be cancelled out
RESISTANCES = np.array([4.5, 8, 1]) # resistance of [x,y,z] circuits
MAX_WATTS = np.array([8, 25, 0]) # max. allowed power for [x,y,z] circuits
MAX_VOLTS = [16, 16, 16] # max. allowed voltage, limited to 16V by used diodes!
# COM-Ports for power supply units:
XY_PORT = "COM7" # placeholders
Z_PORT = "COM11"
AXIS_NAMES = ["X-Axis", "Y-Axis", "Z-Axis"]
ports = [XY_PORT, XY_PORT, Z_PORT]
global ARDUINO
RELAY_PINS = [15, 16, 17] # pin on the Arduino for switching relay of each axis [x,y,z]
# ToDo: make proper settings file to read from and write to
global TestValuesX
global TestValuesY
global TestValuesZ
global TestValues