forked from zietzm/Helmholtz_Test_Bench
Merge pull request 'Development_zietz' (#1) from Development_zietz into master
Reviewed-on: zietzm/Magnetfeldteststand#1
This commit is contained in:
@@ -98,3 +98,8 @@ ENV/
|
|||||||
.idea/misc.xml
|
.idea/misc.xml
|
||||||
.idea/Python-PS2000B.iml
|
.idea/Python-PS2000B.iml
|
||||||
.idea/Python-PS2000B.iml
|
.idea/Python-PS2000B.iml
|
||||||
|
.idea/Python-PS2000B.iml
|
||||||
|
.idea/misc.xml
|
||||||
|
config.ini
|
||||||
|
*.ini
|
||||||
|
log.csv
|
||||||
|
|||||||
Generated
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<module type="PYTHON_MODULE" version="4">
|
<module type="PYTHON_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$" />
|
<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" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PyDocumentationSettings">
|
<component name="PyDocumentationSettings">
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<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>
|
</project>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# This file enables control of a connected Arduino microcontroller.
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import logging
|
import logging
|
||||||
import itertools
|
import itertools
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
Binary file not shown.
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
+1004
-135
File diff suppressed because it is too large
Load Diff
+321
-210
@@ -1,34 +1,48 @@
|
|||||||
from pyps2000b import PS2000B
|
# This file contains all classes and functions directly related to the operation of the helmholtz test stand.
|
||||||
from Arduino import Arduino
|
# The two main classes are Axis and ArduinoCtrl, see their definitions for details.
|
||||||
import settings as g
|
|
||||||
import pandas
|
# import packages:
|
||||||
import time
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import serial
|
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:
|
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):
|
def __init__(self, index, device, PSU_channel, arduino_pin):
|
||||||
# static information
|
# static information
|
||||||
self.index = index
|
self.index = index # index of this axis, 0->X, 1->Y, 2->Z
|
||||||
self.device = device # power supply object (PS2000B class)
|
self.device = device # power supply object for this axis (PS2000B class)
|
||||||
self.channel = PSU_channel # power supply unit channel (1 or 2)
|
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.ardPin = arduino_pin # output pin on the arduino for switching polarity on this axis
|
||||||
|
|
||||||
self.name = g.AXIS_NAMES[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]
|
self.port = g.PORTS[index] # get serial port of this axis PSU
|
||||||
|
|
||||||
self.resistance = 0 # [Ohm]
|
# read static information from the configuration object (which has read it from the config file or settings):
|
||||||
# maximum allowable values to pass through this circuit
|
self.resistance = float(config.read_from_config(self.name, "resistance", config.CONFIG_OBJECT))
|
||||||
self.max_watts = 0 # [W]
|
self.max_amps = float(config.read_from_config(self.name, "max_amps", config.CONFIG_OBJECT))
|
||||||
self.max_amps = 0 # [A]
|
self.max_volts = float(config.read_from_config(self.name, "max_volts", config.CONFIG_OBJECT))
|
||||||
self.max_volts = 0 # [V]
|
|
||||||
|
|
||||||
self.coil_constant = 0 # coil constant of this axis [T/A]
|
self.coil_constant = float(config.read_from_config(self.name, "coil_const", config.CONFIG_OBJECT))
|
||||||
self.ambient_field = 0 # ambient field in this axis [T]
|
self.ambient_field = float(config.read_from_config(self.name, "ambient_field", config.CONFIG_OBJECT))
|
||||||
# ToDo: get this info from settings file
|
|
||||||
|
|
||||||
# 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.connected = "Not Connected"
|
||||||
self.output_active = "Unknown" # power output on the PSU enabled?
|
self.output_active = "Unknown" # power output on the PSU enabled?
|
||||||
self.remote_ctrl_active = "Unknown" # remote control 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:
|
if self.device is not None:
|
||||||
self.update_status_info()
|
self.update_status_info()
|
||||||
|
|
||||||
def update_status_info(self): # Read out the values of the parameters stored in this class and update them
|
def update_status_info(self): # Read out the values of the dynamic parameters stored in this object and update them
|
||||||
try:
|
try: # try to read out the data, this will fail on connection error to PSU
|
||||||
self.device.update_device_information(self.channel)
|
self.device.update_device_information(self.channel) # update the information in the device object
|
||||||
device_status = self.device.get_device_status_information(self.channel)
|
device_status = self.device.get_device_status_information(self.channel) # get object with new status info
|
||||||
if device_status.output_active:
|
|
||||||
|
if device_status.output_active: # is the power output active?
|
||||||
self.output_active = "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:
|
if device_status.remote_control_active:
|
||||||
self.remote_ctrl_active = "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 = self.device.get_voltage(self.channel)
|
||||||
self.voltage_setpoint = self.device.get_voltage_setpoint(self.channel)
|
self.voltage_setpoint = self.device.get_voltage_setpoint(self.channel)
|
||||||
self.current = self.device.get_current(self.channel)
|
self.current = self.device.get_current(self.channel)
|
||||||
self.current_setpoint = self.device.get_current_setpoint(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.connected = "Connection Error"
|
||||||
self.output_active = "Unknown"
|
self.output_active = "Unknown"
|
||||||
self.remote_ctrl_active = "Unknown"
|
self.remote_ctrl_active = "Unknown"
|
||||||
else:
|
else: # no communications error
|
||||||
self.connected = "Connected"
|
self.connected = "Connected" # PSU is connected
|
||||||
|
|
||||||
if g.ARDUINO.connected == "Connected":
|
def print_status(self): # print out the current status of the device (not used at the moment)
|
||||||
try:
|
ui_print("%s, %0.2f V, %0.2f A"
|
||||||
if g.ARDUINO.digitalRead(self.ardPin): # ToDo: Test if this actually works
|
% (self.device.get_device_status_information(self.channel),
|
||||||
self.polarity_switched = "True"
|
self.device.get_voltage(self.channel), self.device.get_current(self.channel)))
|
||||||
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 power_down(self): # temporary powerdown, set outputs to 0 but keep connections enabled
|
def power_down(self): # temporary powerdown, set outputs to 0 but keep connections enabled
|
||||||
self.target_current = 0
|
try:
|
||||||
self.target_field = 0
|
# set class object attributes to 0 to reflect shutdown in status displays, log files etc.
|
||||||
self.target_field_comp = 0
|
self.target_current = 0
|
||||||
self.device.set_voltage(0, self.channel)
|
self.target_field = 0
|
||||||
self.device.set_current(0, self.channel)
|
self.target_field_comp = 0
|
||||||
self.device.disable_output(self.channel)
|
|
||||||
g.ARDUINO.digitalWrite(self.ardPin, "LOW")
|
|
||||||
|
|
||||||
def set_signed_current(self, value): # sets current with correct polarity on this axis
|
if self.device is not None: # there is a PSU connected for this axis
|
||||||
device = self.device
|
self.device.set_voltage(0, self.channel) # set voltage on PSU channel to 0
|
||||||
channel = self.channel
|
self.device.set_current(0, self.channel) # set current on PSU channel to 0
|
||||||
ardPin = self.ardPin
|
self.device.disable_output(self.channel) # disable power output on PSU channel
|
||||||
# print("Attempting to set current", value, "A")
|
g.ARDUINO.digitalWrite(self.ardPin, "LOW") # set arduino pin for polarity switch relay to unpowered state
|
||||||
self.target_current = value
|
except Exception as e: # some error was encountered
|
||||||
if self.connected == "Connected":
|
# show error message:
|
||||||
if abs(value) > self.max_amps: # prevent excessive currents
|
ui_print("Error while powering down %s: %s" % (self.name, e))
|
||||||
self.power_down() # set output to 0 and deactivate
|
messagebox.showerror("PSU Error!", "Error while powering down %s: \n%s" % (self.name, e))
|
||||||
raise ValueError("Invalid current value. Tried %0.2fA, max. %0.2fA allowed" % (value, self.max_amps))
|
|
||||||
elif value >= 0: # switch polarity as needed
|
def set_signed_current(self, value):
|
||||||
pass # g.ARDUINO.digitalWrite(ardPin, "LOW") ToDo: reactivate and tie to arduino
|
# sets current with correct polarity on this axis, this is the primary way to control the test stand
|
||||||
elif value < 0:
|
|
||||||
pass # g.ARDUINO.digitalWrite(ardPin, "HIGH") ToDo: reactivate
|
# ui_print("Attempting to set current", value, "A")
|
||||||
else:
|
self.target_current = value # show target value in object attribute for status display, logging etc.
|
||||||
raise Exception("This should be impossible.")
|
|
||||||
maxVoltage = min(max(1.1 * self.max_amps * self.resistance, 8), self.max_volts) # limit voltage#
|
if abs(value) > self.max_amps: # prevent excessive currents
|
||||||
# print("sending values to device: U =", maxVoltage, "I =", abs(value))
|
self.power_down() # set output to 0 and deactivate
|
||||||
device.set_current(abs(value), channel)
|
raise ValueError("Invalid current value on %s. Tried %0.2fA, max. %0.2fA allowed"
|
||||||
device.set_voltage(maxVoltage, channel)
|
% (self.name, value, self.max_amps))
|
||||||
device.enable_output(channel)
|
|
||||||
else:
|
elif value >= 0: # switch the e-box relay to change polarity as needed
|
||||||
print(self.name, "not connected, can't set current.")
|
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
|
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 = value # update object attribute for display
|
||||||
self.target_field_comp = value
|
self.target_field_comp = value # same as above, bc no compensation
|
||||||
current = value / self.coil_constant
|
current = value / self.coil_constant # calculate needed current
|
||||||
self.set_signed_current(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
|
def set_field(self, value): # forms magnetic field as specified by value, corrected for ambient field
|
||||||
self.target_field = value
|
self.target_field = value # update object attribute for display
|
||||||
field = value - self.ambient_field
|
field = value - self.ambient_field # calculate needed field after compensation
|
||||||
self.target_field_comp = field
|
self.target_field_comp = field # update object attribute for display
|
||||||
current = field / self.coil_constant
|
current = field / self.coil_constant # calculate needed current
|
||||||
self.set_signed_current(current)
|
self.set_signed_current(current) # command the test stand
|
||||||
|
|
||||||
|
|
||||||
class ArduinoCtrl(Arduino):
|
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):
|
def __init__(self):
|
||||||
self.connected = "Unknown"
|
self.connected = "Unknown" # connection status attribute, nominal "Connected"
|
||||||
self.pins = pins
|
self.pins = [0, 0, 0] # initialize list with pins to switch relay of each axis
|
||||||
print("Connecting to Arduino...")
|
for i in range(3): # get correct pins from the config
|
||||||
try:
|
self.pins[i] = int(config.read_from_config(g.AXIS_NAMES[i], "relay_pin", config.CONFIG_OBJECT))
|
||||||
Arduino.__init__(self) # search for connected arduino and connect
|
|
||||||
|
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:
|
for pin in self.pins:
|
||||||
g.ARDUINO.pinMode(pin, "Output")
|
self.pinMode(pin, "Output")
|
||||||
g.ARDUINO.digitalWrite(pin, "LOW")
|
self.digitalWrite(pin, "LOW")
|
||||||
except Exception:
|
except Exception as e: # some error occurred, usually the arduino is not connected
|
||||||
print("Connection to Arduino failed.")
|
ui_print("Connection to Arduino failed.", e)
|
||||||
self.connected = "Not Connected"
|
self.connected = "Not Connected"
|
||||||
else:
|
else: # connection was successfully established
|
||||||
g.arduino_connected = "Connected"
|
self.connected = "Connected"
|
||||||
print("Arduino ready.")
|
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:
|
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
|
def value_in_limits(axis, key, value): # Check if value is within safe limits (set in globals.py)
|
||||||
g.AXES = []
|
# 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)
|
if float(value) > float(max_value): # value is too high
|
||||||
try:
|
return 'HIGH'
|
||||||
if g.XY_DEVICE is not None:
|
elif float(value) < float(min_value): # value is too low
|
||||||
print("closing serial connection on XY device")
|
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.serial.close()
|
||||||
g.XY_DEVICE = None
|
g.XY_DEVICE = None
|
||||||
g.XY_DEVICE = PS2000B.PS2000B(g.XY_PORT) # setup PSU
|
g.XY_DEVICE = PS2000B.PS2000B(g.XY_PORT) # setup PSU
|
||||||
print("Connection established.")
|
ui_print("Connection established.")
|
||||||
g.X_AXIS = Axis(0, g.XY_DEVICE, 0, g.ARDUINO.pins[0]) # create axis objects
|
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])
|
g.Y_AXIS = Axis(1, g.XY_DEVICE, 1, g.ARDUINO.pins[1])
|
||||||
except serial.serialutil.SerialException:
|
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
|
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])
|
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:
|
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)
|
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])
|
g.Z_AXIS = Axis(2, g.Z_DEVICE, 0, g.ARDUINO.pins[2])
|
||||||
except serial.serialutil.SerialException:
|
except serial.serialutil.SerialException:
|
||||||
g.Z_AXIS = Axis(2, None, 0, g.ARDUINO.pins[2])
|
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.X_AXIS)
|
||||||
g.AXES.append(g.Y_AXIS)
|
g.AXES.append(g.Y_AXIS)
|
||||||
g.AXES.append(g.Z_AXIS)
|
g.AXES.append(g.Z_AXIS)
|
||||||
|
|
||||||
i = 0
|
ui_print("") # print new line
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def activate_all(): # enables remote control and output on all PSUs and channels
|
def set_to_zero(device): # sets voltages and currents to 0 on all channels of a specific PSU
|
||||||
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
|
|
||||||
device.voltage1 = 0
|
device.voltage1 = 0
|
||||||
device.current1 = 0
|
device.current1 = 0
|
||||||
device.voltage2 = 0
|
device.voltage2 = 0
|
||||||
device.current2 = 0
|
device.current2 = 0
|
||||||
|
|
||||||
|
|
||||||
def power_down_all(): # temporary, set all outputs to 0 but keep connections enabled
|
def power_down_all(): # on all PSUs set all outputs to 0 but keep connections enabled
|
||||||
set_to_zero(g.XY_DEVICE)
|
for axis in g.AXES:
|
||||||
set_to_zero(g.Z_DEVICE)
|
axis.power_down() # set outputs to 0 and pin to low on this axis
|
||||||
g.ARDUINO.safe()
|
|
||||||
|
|
||||||
|
|
||||||
def shut_down_all(): # shutdown at program end or on error, set outputs to 0 and disable connections
|
def shut_down_all(): # safe shutdown at program end or on error
|
||||||
# ToDo: better messages, check if things are connected first
|
# set outputs to 0 and disable connections on all devices
|
||||||
print("\nAttempting to safely shut down all devices. Check equipment to confirm.")
|
|
||||||
try: set_to_zero(g.XY_DEVICE)
|
ui_print("\nAttempting to safely shut down all devices. Check equipment to confirm.")
|
||||||
except:
|
# start writing string to later show how shutdown on all devices went in a single info pop-up:
|
||||||
print("XY PSU set to 0 unsuccessful.")
|
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:
|
else:
|
||||||
print("XY PSU currents and voltages set to 0.")
|
ui_print("Z PSU not connected, can't deactivate.")
|
||||||
try: set_to_zero(g.Z_DEVICE)
|
message += "\nZ PSU not connected, can't deactivate."
|
||||||
except:
|
|
||||||
print("Z PSU set to 0 unsuccessful.")
|
# Shut down Arduino:
|
||||||
else:
|
try:
|
||||||
print("Z PSU currents and voltages set to 0.")
|
g.ARDUINO.safe() # call safe method in ArduinoCtrl class (all relay pins to LOW)
|
||||||
deactivate_all()
|
except BaseException as e: # some error occurred
|
||||||
try: g.ARDUINO.safe()
|
ui_print("Arduino safing unsuccessful:", e)
|
||||||
except:
|
message += "\nArduino safing unsuccessful: %s" % e # append to the message to show later
|
||||||
print("Arduino safing unsuccessful.")
|
# this throws no exception, even when arduino is not connected
|
||||||
# else: # commented out bc this throws no exception, even when arduino is not connected
|
# ToDo (optional): figure out error handling for this
|
||||||
# print("Arduino pins set to LOW.") # ToDo: figure out error handling for this
|
try:
|
||||||
try: g.ARDUINO.close()
|
g.ARDUINO.close() # close the serial link
|
||||||
except:
|
except BaseException as e: # something went wrong there
|
||||||
print("Closing Arduino connection failed.")
|
if g.ARDUINO.connected == "Connected": # Arduino was connected, some error occurred
|
||||||
else:
|
ui_print("Closing Arduino connection failed:", e)
|
||||||
print("Serial connection to Arduino closed.")
|
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
|
def set_field_simple(vector): # forms magnetic field as specified by vector, w/o cancelling ambient field
|
||||||
for i in [0, 1, 2]:
|
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
|
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]:
|
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
|
i = 0
|
||||||
for axis in g.AXES:
|
for axis in g.AXES:
|
||||||
axis.set_signed_current(vector[i])
|
try:
|
||||||
i = i + 1
|
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
|
def devices_ok(xy_off=False, z_off=False, arduino_off=False):
|
||||||
# csv format: time (s); xField (T); yField (T); zField (T)
|
# check if all devices are connected, return True if yes
|
||||||
# decimal commas
|
# checks for individual devices can be disabled by parameters above (default not disabled)
|
||||||
print("Reading File:", filepath)
|
try: # handle errors while checking connections
|
||||||
file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file
|
if not xy_off: # if check for this device is not disabled
|
||||||
array = file.to_numpy() # convert csv to array
|
if g.XY_DEVICE is not None: # has the handle for this device been set?
|
||||||
t_zero = time.time()
|
g.X_AXIS.update_status_info() # update info --> this actually communicates with the device
|
||||||
t_ref = t_zero
|
if g.X_AXIS.connected != "Connected": # if not connected
|
||||||
i = 0
|
return False # return and exit function
|
||||||
print("Starting Execution...")
|
else: # if handle has not been set the device is inactive --> not ok
|
||||||
while i < len(array):
|
return False
|
||||||
t = time.time() - t_zero
|
if not z_off: # same as above
|
||||||
if t >= array[i, 0]:
|
if g.Z_DEVICE is not None:
|
||||||
field_vec = array[i, 1:4]
|
g.Z_AXIS.update_status_info()
|
||||||
print("t = %0.2f s, target field vector = " % (array[i, 0]), field_vec)
|
if g.Z_AXIS.connected != "Connected":
|
||||||
set_field(field_vec)
|
return False
|
||||||
i = i + 1
|
else:
|
||||||
if t - t_ref >= 1 and printing == 1: # print status every second
|
return False
|
||||||
print_status_3()
|
|
||||||
t_ref = t
|
if not arduino_off: # check not disabled
|
||||||
print("File executed, powering down channels.")
|
g.ARDUINO.update_status_info() # update status info --> attempts communication
|
||||||
power_down_all() # set currents and voltages to 0, set arduino pins to low
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
@@ -1,28 +1,78 @@
|
|||||||
import User_Interface as ui
|
# Main file of the program. Run this file to start the application.
|
||||||
import cage_func as func
|
|
||||||
|
# import packages:
|
||||||
|
from os.path import exists
|
||||||
import traceback
|
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
|
try: # start normal operations
|
||||||
# Connect to Arduino:
|
|
||||||
g.ARDUINO = func.ArduinoCtrl(g.RELAY_PINS)
|
|
||||||
|
|
||||||
print("Connecting to PSUs...")
|
config.CONFIG_FILE = 'config.ini' # set the config file path
|
||||||
func.setup_axes() # initiate communication, set handles
|
# 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...")
|
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()
|
g.app = HelmholtzGUI() # initialize user interface
|
||||||
application.mainloop()
|
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("\nAn error occurred, Shutting down.")
|
||||||
print(e)
|
# shop pup-up error message:
|
||||||
print(traceback.print_exc())
|
message = "%s.\nSee python console traceback for more details. " \
|
||||||
|
"\nShutting down devices, check equipment to confirm." % e
|
||||||
finally: # safely shut everything down at the end
|
messagebox.showerror("Error!", message)
|
||||||
func.shut_down_all()
|
print(traceback.print_exc()) # print error traceback in the python console
|
||||||
|
program_end() # safely close everything and shut down devices
|
||||||
|
|||||||
-36
@@ -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()
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# This file enables communication with PS2000B Power Supply Units.
|
||||||
|
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
# Python access to Elektro Automatik PS 2000 B devices via USB/serial
|
# Python access to Elektro Automatik PS 2000 B devices via USB/serial
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
numpy==1.19.3 # bug in numpy 1.19.4, 1.19.3 used as workaround
|
numpy==1.19.3 # bug in numpy 1.19.4, 1.19.3 used as workaround
|
||||||
pyserial~=3.5
|
pyserial~=3.5
|
||||||
future~=0.18.2
|
future~=0.18.2
|
||||||
pandas
|
pandas~=1.1.5
|
||||||
|
matplotlib~=3.3.2
|
||||||
-35
@@ -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
|
|
||||||
Reference in New Issue
Block a user