forked from zietzm/Helmholtz_Test_Bench
182 lines
24 KiB
TeX
182 lines
24 KiB
TeX
\chapter{Software Implementation}\label{chp:software}
|
|
\section{Program Structure}
|
|
\glsunset{psu}
|
|
\glsunset{irs}
|
|
To operate the test bench, a Python software with graphical \gls{ui} was developed. It controls the used PS2000B Power Supply Units (\gls{psu}) as well as the Arduino microcontroller inside the switch box. This chapter focuses on the overall implementation. More detailed information is provided in the form of comments in the source code, which is available on the \gls{irs} git server.\footnote{\url{https://egit.irs.uni-stuttgart.de/zietzm/Helmholtz_Test_Bench.git}} A users guide can be found in Section \ref{sec:software_guide}.\\
|
|
Software development and testing were done in Windows 10 and Python 3.7. Some aspects may need to be adapted to use the software on a different operating system or Python version. The code was tested with a \gls{psu} or the switch box individually. However, integrated verification with both \gls{psu}s and the switch box Arduino connected simultaneously was not possible up to this point, as some of the equipment was located inside the \gls{irs} cleanroom.\\
|
|
The program file architecture is shown in Figure \ref{fig:softwarelayout}. This is meant to give an overview of the structure, therefore it does not show all interactions between the files.
|
|
\begin{figure}[hb]
|
|
\centering
|
|
\includegraphics[width=0.8\textwidth]{media/Software layout}
|
|
\caption{Software file architecture}
|
|
\label{fig:softwarelayout}
|
|
\end{figure}
|
|
|
|
Upon execution of \code{main.py}, the test bench devices (\gls{psu}s and switch box Arduino), program objects and variables are initialized. Next, the \gls{ui} (controlled by \code{User\_Interface.py}) is set up and displayed. Program elements needed for more complex functionalities are coded in \code{csv\_logging.py}, \code{config\_handling.py} and \code{csv\_threading.py}.\\
|
|
All program elements use the classes in the \code{cage\_func.py} file to control and read data from the test bench. Communication with the actual hardware (power supply units and switch box Arduino) is achieved via the libraries in \code{PS2000B.py} and \code{Arduino.py}, which were taken from online sources \cite{PS2000B_lib, Arduino_lib} with only minor modifications.\\
|
|
\code{globals.py} is used to easily pass frequently used variables between the different program files. It contains mainly variable initializations, that can be accessed and changed by the other program elements after importing it.\\
|
|
The application is able to operate with one or all test bench devices disconnected. For example, it is possible to generate a magnetic field in only one test bench axis with just a single \gls{psu}. The program also provides error handling for a variety of device disconnects and user mistakes. This includes protection against the commanding of excessive currents and voltages, as well as the display of warnings when attempting to enter a potentially dangerous value in the settings.
|
|
|
|
\section{Program Files}
|
|
\subsection{Main Executable \code{main.py}}
|
|
The main executable file contains the framework code for start and end of the program.\\
|
|
During initialization the information from the configuration file is read out to set the necessary constants. Then the test bench is prepared using the \code{setup\_all} function (see Section \ref{sec:cage_func}), the \gls{ui} is initialized and its \code{mainloop} started.\\
|
|
The \code{program\_end} function in this file ensures a safe shutdown of all equipment at the end of the program. It is immediately called when the user closes the window. The function stops any other operational threads (see Section \ref{sec:csv_exec}) and powers down the \gls{psu}s and Arduino. It also asks the user to store possible unsaved log data and then destroys the application window.\\
|
|
For exceptions that were not caught by lower level error handling, the main file also includes a global error protection. In such a case an error message is displayed and the \code{program\_end} function called. This is to ensure a safe shutdown, regardless of the current program state.
|
|
|
|
\subsection{Global Variables File \code{globals.py}}
|
|
This file holds global variables and constants that are used often and by more than one module.\\
|
|
Examples of these are \code{PS2000B} and \code{ArduinoCtrl} objects that represent the test bench devices, \code{Axis} objects for each spatial axis but also status indicators like \code{exitFlag}, which signals the end of the program to all modules.\\
|
|
The file also contains the default values for all constants used to run the test bench, like coil constants, resistances and maximum currents. These are stored in a hard-coded dictionary, which can be used to generate a default configuration. The dictionary also includes the minimum and maximum safe value for each constant. These limits are used to warn users if they attempt to set a value that may damage equipment. In the future, it may be advantageous to store this information in a separate configuration file to allow easier modification.
|
|
|
|
\subsection{Test Bench Control File \code{cage\_func.py}}\label{sec:cage_func}
|
|
This file contains all classes and functions directly related to the operation of the test bench. It includes the two main control classes \code{Axis} and \code{ArduinoCtrl}, functions for initialization and shutdown as well as for controlling more than one axis at a time.
|
|
|
|
\myparagraph{Axis Control Class \code{Axis}}
|
|
This is the main class representing an axis (x,y,z) of the test bench. It contains static and dynamic status information about the axis in its attributes and the means to command the axis in its methods. During program initialization an object of this class is created for each of the three axes and stored in \code{globals.py} \\
|
|
Apart from methods to get status information from the appropriate \gls{psu}, the class contains the primary way to command the test bench in form of the \code{set\_signed\_current} method. Its parameter is a positive or negative current value in Ampere. The method first checks if this value exceeds the safe limits defined in the program configuration. If not, the appropriate \gls{psu} channel and the switch box Arduino are commanded to supply the desired current and to actuate the polarity change relay as needed.\\
|
|
To generate a specific magnetic field, the \code{set\_field} method can be called. It takes a given field value $B$ in Tesla and calculates the needed current:
|
|
\begin{align}
|
|
I=\frac{B-B_0}{K}
|
|
\label{eq:field_current}
|
|
\end{align}
|
|
$B_0$ is the background magnetic field in the measurement area for this axis in Tesla and $K$ is the coil constant in [\si{\tesla\per\ampere}]. The calculated value $I$ is passed to the \code{set\_signed\_current} method to command the test bench. The \code{set\_field\_simple} method works the same way, but without subtracting the ambient field.\\
|
|
Similarly the minimum and maximum achievable field values $B_{min}$ and $B_{max}$ are calculated in the class initialization, mainly in order to provide this information to the user:
|
|
\begin{align}
|
|
B_{max} &= B_0 + I_{max}*K\\
|
|
B_{min} &= B_0 - I_{max}*K
|
|
\end{align}
|
|
Here $I_{max}$ is the maximum allowed current, as defined in the configuration page of the \gls{ui}.
|
|
|
|
\myparagraph{Arduino Control Class \code{ArduinoCtrl}}
|
|
This is the main class used to control the Arduino microcontroller inside the switch box. It inherits the \code{Arduino} class from the Arduino command \gls{api} \cite{Arduino_lib}. This provides a way to pass commands directly through the class object, which is created during program initialization and stored in \code{globals.py}. \\
|
|
Most commands to the switch box are passed directly through the inherited \code{Arduino} class. For example, the \code{digitalWrite()} method is called to energize a pin that actuates a specific relay. The purpose of the \code{ArduinoCtrl} class is mainly to provide supporting functionality directly related to the specific use case on the test bench, including:
|
|
\begin{itemize}
|
|
\item{Configuration of output pins used to trigger polarity switch relays}
|
|
\item{Checking Arduino connection and output pin status}
|
|
\item{\code{safe} method to set all used output pins to "LOW" for safe shutdown}
|
|
\end{itemize}
|
|
|
|
\myparagraph{Setup Function \code{setup\_all}}
|
|
This is the main initialization function. It is used at application start-up and during the runtime whenever the user updates settings or (re)connects a device. Its main tasks are:
|
|
\begin{itemize}
|
|
\item Read the latest information from the configuration object (see Section \ref{sec:config_handling})
|
|
\item Initiate communication with switch box Arduino and create object of \code{ArduinoCtrl} class
|
|
\item Initiate communication with \gls{psu}s
|
|
\item Create object of class \code{Axis} for each axis (x,y,z)
|
|
\end{itemize}
|
|
During these tasks it also handles different error cases, like disconnected devices or wrong settings in the configuration object.
|
|
|
|
\myparagraph{Shutdown Function \code{shut\_down\_all}}
|
|
This function is used to safely shut down all devices at the end of the program or if an error occurs. It is called in the \code{program\_end} function of \code{main.py}. Its primary tasks are:
|
|
\begin{itemize}
|
|
\item Secure all connected \gls{psu}s
|
|
\begin{itemize}
|
|
\item Set voltages and currents on both channels to \SI{0}{\volt} and \SI{0}{\ampere}
|
|
\item Disable power outputs on both channels
|
|
\end{itemize}
|
|
\item Secure Arduino
|
|
\begin{itemize}
|
|
\item Set all used output pins to "LOW"
|
|
\item Close serial connection
|
|
\end{itemize}
|
|
\item Show message to user with shutdown status of all devices and any errors encountered
|
|
\end{itemize}
|
|
During this process different error cases, like disconnected devices, are handled and the user is informed.
|
|
|
|
\myparagraph{Other Functions}
|
|
In addition to those discussed above, the file also contains functions to:
|
|
\begin{itemize}
|
|
\item Command currents or magnetic fields on all axes, based on a given vector
|
|
\item Check if all devices are still connected
|
|
\item Check if a given value is within the safe limits defined in \code{globals.py}
|
|
\end{itemize}
|
|
\newpage
|
|
\subsection{Graphical User Interface \code{User\_Interface.py}}
|
|
This file is used to construct the \gls{ui}, that the user interacts with to control the test bench. It contains several classes, each with code to build a specific section of the \gls{ui} as well as methods to perform the actions that are triggered by different user inputs in that section.\\
|
|
|
|
\begin{figure}[h]
|
|
\centering
|
|
\includegraphics[width=\linewidth]{media/csv_mode_full}
|
|
\caption{User interface (example state)}
|
|
\label{fig:ui_overview}
|
|
\end{figure}
|
|
|
|
The general layout of the application window is shown in Figure \ref{fig:ui_overview}. At the bottom left is a status display, that is constantly updated with the current state of all connected devices. To its right is a text area that can be used to print out information to the user, similar to a basic console output. The top half is the main area, used to display the interactive elements. Its content changes depending on which mode the user has selected. At the very top of the window a drop-down menu is used to switch between the different modes. For more detailed usage instructions please refer to Section \ref{sec:software_guide}.\\
|
|
The interface is setup through a central object of class \code{HelmholtzGUI}, which inherits the \code{Tk} main application class from the \code{tkinter} library. The other \gls{ui} element classes (except the top menu) inherit from \code{tkinter.Frame} and are placed in the layout by the main object. This structure is based on a proposal by H. Kinsley \cite{Tkinter_tutorial}. Each class is briefly described below.
|
|
|
|
\myparagraph{Main Application Class \code{HelmholtzGUI}}
|
|
An instance of this class represents the application window. The program \code{mainloop} is running on this object. The major \gls{ui} elements are initialized here and placed at their appropriate positions. This class's only method is \code{show\_frame}, which is used to switch between the different operating modes by displaying their respective frame in the main area.
|
|
|
|
\myparagraph{Menu Bar Class \code{TopMenu}}
|
|
The menu bar at the very top of the application window is constructed by this class. It contains methods to implement each option of the menu. At this time, it is only used to switch between modes, but more functionalities could be added in the future if needed.
|
|
|
|
\myparagraph{Static Manual Input Mode Class \code{ManualMode}}
|
|
The manual input mode interface is used to manually command static values of magnetic fields or currents on the test bench. It is placed in the main area of the application window, the layout can be seen in Figure \ref{fig:manualmodepure}. The methods provided in this class interface mainly with the \code{cage\_func.py} file to command the devices.
|
|
|
|
\myparagraph{CSV Sequence Execution Mode Class \code{ExecuteCSVMode}}
|
|
This class constructs and operates the interface for executing magnetic field sequences from a \gls{csv} file. It is placed in the application's main area, its layout is shown in Figure \ref{fig:csvmodepure}.\\
|
|
Apart from a method to load \gls{csv} files, the class manages the separate thread that needs to be created every time a sequence is run. For this purpose, it interfaces mainly with the \code{csv\_threading.py} file to initialize, start and (if needed) stop the thread object. More details on the multithreading functionality is provided in Section \ref{sec:csv_exec}.
|
|
|
|
\myparagraph{Settings Page Class \code{Configuration}}
|
|
The program settings page is constructed in this class. It also controls the reading and writing of configuration files. Like the actual test bench control modes, the settings page is placed in the application's main area. Its layout can be seen in Figure \ref{fig:settingspure}.\\
|
|
The class generates and reads out all entry fields the user can use to set program constants like used serial ports, resistances and current limits. It interfaces with the \code{config\_handling.py} file to store this information in a \code{ConfigParser} object and to read and write it from and to configuration files (see Section \ref{sec:config_handling}).\\
|
|
Individual entry fields can be highlighted to point out values that exceed the safe limits defined in \code{globals.py} to the user.
|
|
|
|
\myparagraph{Data Logging Configuration Page Class \code{ConfigureLogging}}
|
|
This class generates the page for configuring the logging of test bench data to a \gls{csv} file. Similarly to the settings page it is placed in the main area, its layout is shown in Figure \ref{fig:loggingpure}.\\
|
|
The page provides a number of checkboxes used to select what specific data is to be logged. This information is stored in a list of string keys. Whenever a row of data needs to be logged, a method in the class calls the \code{log\_datapoint} function in \code{csv\_logging.py}. To indicate what data to log, the string key list is passed as the function parameter (see Section \ref{sec:logging}). \\
|
|
There are two options to control when and how often data is logged: in regular intervals and on event. These can also be used at the same time. The information, which of these is enabled, is stored in two boolean attributes of the class object. For logging in regular intervals a method in the \code{ConfigureLogging} class object periodically calls itself. \\
|
|
For logging on event, a data point is generated whenever a significant change on the test bench is commanded. As of now, this is simply done by inserting a piece of code in every function where it was seen as appropriate. However, this is somewhat inconsistent and carries the risk of missing some state changes. A better balance between logging consistency and excessive data generation should be devised in a future update.
|
|
|
|
\myparagraph{Status Display Class \code{StatusDisplay}}
|
|
The status display (bottom left in Figure \ref{fig:ui_overview}) is used to monitor the state of the test bench devices. The individual values are displayed and updated through labels tied to variables of type \code{tkinter.StringVar()}, which are stored in a dictionary. The \code{update\_labels} method then polls the current values from the \code{Axis} and \code{ArduinoCtrl} objects described in Section \ref{sec:cage_func} and updates the label variables. This is done both periodically and on major test bench commands.
|
|
|
|
\myparagraph{Output Console Class \code{OutputConsole}}
|
|
The output console in the bottom right of the \gls{ui} (see Figure \ref{fig:ui_overview}) is generated in this class. The main body is a \code{tkinter.Text} widget. Printing of information to it is done via the custom \code{ui\_print} function, which can be used exactly like the built-in \code{print}.
|
|
|
|
\subsection{Sequence Execution File \code{csv\_threading.py}}\label{sec:csv_exec}
|
|
This file contains code for executing a timed sequence of magnetic field vectors from a \gls{csv} file. To do this without interfering with the \gls{ui}, it must run in a separate thread.\\
|
|
The main class for this is \code{ExecCSVThread}. It inherits the \code{Thread} class from the \code{threading} library, so each of its instances represents a unique thread. Its main method, apart from those needed to start and stop it, is \code{execute\_sequence}. It takes the desired sequence in the form of a \code{numpy} array and commands the test bench at the appropriate times. When a new field vector is set, all related actions need to be performed before the scheduler returns to the main thread. A \code{threading.Lock} object is used to ensure this. The method also continuously checks that all devices are still connected and that the run has not been aborted by user input or closing of the main application window.\\
|
|
Apart from the main class this file contains functions to read data from a \gls{csv} file to a \code{numpy} array and check that no values in it exceed the test bench limits. There is also a function to generate a plot of the data for display in the \gls{ui}. The line plot is modified to reflect the discrete and nearly instantaneous change of fields in the test bench (see result in Figure \ref{fig:ui_overview}).
|
|
\newpage
|
|
\subsection{Configuration Handling File \code{config\_handling.py}}\label{sec:config_handling}
|
|
This file contains functions and variables for reading and writing configuration files. The processing is done using the \code{configparser} library. \\
|
|
The current program configuration is stored in \code{CONFIG\_OBJECT}, an instance of \code{ConfigParser}. It contains all configurable information and can be stored in its entirety in a .ini file. The \code{write\_config\_to\_file} and \code{get\_config\_from\_file} functions are used to read/write the entire \code{CONFIG\_OBJECT} from/to such a file. An example is provided in Appendix \ref{app:example_files}.\\
|
|
To access or change specific values the \code{read\_from\_config} and \code{edit\_config} functions are provided. The latter also checks if a value is within the safe limits defined in \code{globals.py}. If it is not, the user is warned and asked to confirm, before the value is written to \code{CONFIG\_OBJECT}.\\
|
|
The \code{check\_config} function does this check for all values of a provided configuration object. If excessive values are found, a warning message is displayed and the configuration \gls{ui} page opened, where they are highlighted.\\
|
|
The last function of the file is \code{reset\_config\_to\_default}, which overwrites the entire \code{CONFIG\_OBJECT} with the default values defined in \code{globals.py}.
|
|
|
|
\subsection{Data Logging Handling File \code{csv\_logging.py}}\label{sec:logging}
|
|
This file handles the logging of test bench status data to a \gls{csv} file, using the \code{pandas} library. Within the program the logged data is stored in a \code{pandas.DataFrame} object, which represents a table with column headers and data rows.\\
|
|
The information about what data can be logged and how to access the specific values is stored in a central dictionary. Its keys are the names of the values as they are displayed in the \gls{ui}, e.g. "Voltage Setpoint" or "Actual Current". The keys are used as handles for accessing the dictionary elements and as labels for the checkboxes in the logging configuration \gls{ui} page (see Figure \ref{fig:loggingpure}). The \code{ConfigureLogging} class of the \gls{ui} generates a list containing the keys belonging to the ticked checkboxes, which is then passed to the logging functions to indicate what data should be logged.
|
|
The keys are intentionally identical to the labels used for the status display. This makes it easier for the user to know what each value means. It may also allow unifying some of the code for these two functionalities in the future.\\
|
|
The dictionary elements themselves are a string representation of the respective attribute names in the \code{Axis} class. They are used with the \code{getattr} command to get the values of these attributes from the individual axis objects.\\
|
|
To log a data point, the mentioned list of keys is passed to the \code{log\_datapoint} function. For each of its elements, the data from all three axes is read out using \code{getattr} as described above. To form the correct number of column headers, the key list is expanded to include each item three times, for example \code{["Target Field"]} becomes \code{["Target Field X", "Target Field Y", "Target Field Z"]}. Together with the values this forms a new \code{DataFrame}, which is then appended to the previously logged data.\\
|
|
When the logging is finished, the entire \code{DataFrame} can then be saved to a \gls{csv} file using the \code{write\_to\_file} function. The file also provides functions to allow the user to choose a filename and clear the logged data.
|
|
\newpage
|
|
\subsection{Hardware Control Libraries}
|
|
\myparagraph{PS2000B \gls{psu} Control Library \code{PS2000B.py}}
|
|
The code necessary to control the power supply units was adapted from S. Spr{\"o}ßig \cite{PS2000B_lib} with only minor modifications. The library provides the class \code{PS2000B} and some supporting functions. Each object of the class represents a physical \gls{psu}. These objects can be used to access status information and command the device, for example to set currents and voltages. More information can be found in the readme file provided by the original author.\\
|
|
The main modification to this library done as part of this thesis, was to implement independent commanding of both \gls{psu} channels. For this an additional parameter \code{channel} (integer type, 0 or 1) was added to all methods of the \code{PS2000B} class that interact with the device.
|
|
Additionally the properties \code{PS2000B.output1}, \code{PS2000B.voltage1} and \code{PS2000B.current1} were duplicated to support the second channel (e.g. \code{PS2000B.output2}).
|
|
|
|
\myparagraph{Arduino Command \gls{api} \code{arduino.py}}
|
|
To control the Arduino microcontroller the online library from \cite{Arduino_lib} was integrated without major modifications. To use it, the provided \code{prototype.ino} file needs to be transferred to the Arduino. An object of class \code{Arduino} can then be used to control the board (connected via \gls{usb}) with methods that closely resemble the ones used in the standard Arduino programming language. More details can be found in the readme file provided by the original author \cite{Arduino_lib}.
|
|
|
|
\section{Conversion to Executable}
|
|
The program is compiled to a .exe executable file to enable simple use without a Python installation. For this, the "Auto Py to Exe" tool developed by B. Vollebregt \cite{auto_py_to_exe} is used. The resulting files are distributed in a separate repository on the \gls{irs} git server.\footnote{\url{https://egit.irs.uni-stuttgart.de/zietzm/Helmholtz_Test_Bench_Releases}} When a new software version is ready for publication, the following procedure should be used to release it:
|
|
\begin{enumerate}
|
|
\item Run "auto-py-to-exe.exe" (provided in main development repository)
|
|
\item In the Auto Py to Exe \gls{ui}, select \code{main.py} file as "Script Location"
|
|
\item Select options "One File" and "Window Based"
|
|
\item Select file "Helmholtz.ico" as the "Icon"
|
|
\item Select "Releases" in the development repository as the output folder
|
|
\item Execute conversion
|
|
\item Rename resulting "main.exe" file to "Helmholtz Cage Control.exe"
|
|
\item Verify correct program operation from created executable
|
|
\item Copy new executable to the release repository\footnotemark[\value{footnote}] and replace previous version
|
|
\item Commit, merge and create a new release in the git web interface
|
|
\item Record changes in release notes and changelog and update the documentation
|
|
\end{enumerate}
|