Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 310a92fe08 | |||
| ffc956554d | |||
| dbd393e661 | |||
| 960ae27f6b | |||
| 6568515957 | |||
| a6e1b43d49 | |||
| 982a7d7a20 | |||
| 03eaf8330c | |||
| 1354aed150 | |||
| 56b8ffcd5e | |||
| 298852f6c8 | |||
| 82b0d1cfd5 | |||
| c678332b45 | |||
| f13389c06d | |||
| 3262d30b9a | |||
| e107d15cf5 | |||
| c638f5a278 | |||
| 827af90bdc | |||
| dfd0fd8ecc | |||
| 842739b4b3 | |||
| 00a1669f39 | |||
| 3b4bbc91da | |||
| cd79c993f2 | |||
| 380c8c9664 | |||
| c704886717 | |||
| c5f8bb9414 | |||
| 3fe39b0cf9 | |||
| d5c9879eae | |||
| effcc72966 | |||
| 283ef5fd24 | |||
| 4021aa5383 | |||
| 95984329c1 | |||
| 2bce7ffd85 | |||
| 998b325ca9 | |||
| 9e046e25ab | |||
| 5ad5e5ebff | |||
| b626ef3ae8 | |||
| 88f85607be | |||
| 6b1a27d9ae | |||
| 157f3dc9e3 | |||
| 1703e99684 | |||
| 1ccd1e8c63 | |||
| 53d198a064 | |||
| 952200c730 | |||
| 3bd29bf011 | |||
| 8f0facb461 | |||
| 1fc059a656 | |||
| 6ab3778692 | |||
| e1e7db29a9 | |||
| ac609b242f | |||
| 8da80d949b | |||
| ba05d67a81 | |||
| dbc750be7d | |||
| 7c3c8aa420 | |||
| dc5952caf8 | |||
| 286874f4d9 | |||
| 95965f0931 | |||
| addb064f71 | |||
| 1144e5fae9 | |||
| 33331e829a | |||
| 9e56937252 | |||
| 5a3625021e | |||
| f2c33f69fa | |||
| d7ade91f80 | |||
| 747e424871 | |||
| a08ba26384 | |||
| de4e54e7e0 | |||
| d02bde9631 | |||
| 4bb6536a84 | |||
| bea7f94550 | |||
| 151081a3c3 | |||
| 4bb1bad731 | |||
| bcadc3f273 | |||
| 244aaa8e89 | |||
| 4d1f5a7437 | |||
| 8177402ba1 | |||
| 3a526ec26d | |||
| 89ed805411 | |||
| 3596733843 | |||
| 8f70f85c84 | |||
| 11b5ea4e36 | |||
| 4827d07b6d | |||
| 80f036060a | |||
| fa6d50d04c | |||
| 38f3793c27 | |||
| bcfe4808c0 | |||
| c7e793a420 | |||
| e5da0c87ed | |||
| faa913fcd7 | |||
| 4c6164c41f | |||
| 41bb8b0885 | |||
| cad62899c0 | |||
| 7da0efd9f0 | |||
| 796c3df94b | |||
| e0104b5604 | |||
| f5bf9cbf68 | |||
| 929e0ed50a | |||
| d5ca80d612 | |||
| 3e8490d353 | |||
| 606cfa1f40 | |||
| 9475657865 | |||
| eeb6a230e6 | |||
| d3ffe4d642 | |||
| 640d99c9e2 | |||
| 12de3c3587 |
@@ -101,5 +101,7 @@ ENV/
|
||||
.idea/Python-PS2000B.iml
|
||||
.idea/misc.xml
|
||||
config.ini
|
||||
*.ini
|
||||
log.csv
|
||||
.idea/misc.xml
|
||||
.idea/misc.xml
|
||||
.idea/Python-PS2000B.iml
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.7 (venv)" jdkType="Python SDK" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.9 (Helmholtz_Test_Bench)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (venv)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (Helmholtz_Test_Bench)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
@@ -0,0 +1,19 @@
|
||||
*.aux
|
||||
*.bbl
|
||||
*.bcf
|
||||
*.blg
|
||||
*.log
|
||||
*.lof
|
||||
*.lot
|
||||
*.out
|
||||
*.gz
|
||||
*.xml
|
||||
*.glg
|
||||
*.glo
|
||||
*.gls
|
||||
*.ist
|
||||
*.toc
|
||||
*.txss
|
||||
*.aux
|
||||
*.fls
|
||||
*.fdb_latexmk
|
||||
@@ -0,0 +1,177 @@
|
||||
% please capatalize all abbreviations
|
||||
\newacronym{irs}{IRS}{Institute of Space Systems at the University of Stuttgart}
|
||||
\newacronym{flp}{FLP}{Flying Laptop}
|
||||
\newacronym{ksat}{KSat}{Small Satellite Student Society at the University of Stuttgart}
|
||||
\newacronym{acs}{ACS}{Attitude Control System}
|
||||
\newacronym{obdh}{OBDH}{onboard data handling}
|
||||
\newacronym{source}{SOURCE}{Stuttgart Operated University Research CubeSat for Evaluation and Education}
|
||||
\newacronym{iras}{IRAS}{Integrated Research Platform for Affordable Satellites}
|
||||
\newacronym{dlr}{DLR}{German Aerospace Center}
|
||||
\newacronym{esa}{ESA}{European Space Agency}
|
||||
\newacronym{eps}{EPS}{electrical power system}
|
||||
\newacronym{com}{COM}{Communication Port}
|
||||
\newacronym{st}{S\&T}{structure and thermal control system}
|
||||
\newacronym{pl}{PL}{payload}
|
||||
\newacronym{sim}{SIM}{simulation and test bed}
|
||||
\newacronym{pr}{PR}{public relations}
|
||||
\newacronym{prf}{PRF}{power reduction factor}
|
||||
\newacronym{trl}{TRL}{Technology Readiness Level}
|
||||
\newacronym{ilr}{ILR}{Institut für Luft- und Raumfahrt}
|
||||
\newacronym{leo}{LEO}{Low Earth Orbit}
|
||||
\newacronym{meo}{MEO}{Medium Earth Orbit}
|
||||
\newacronym{geo}{GEO}{Geostationary Orbit}
|
||||
\newacronym{pcdu}{PCDU}{Power Control and Distribution Unit}
|
||||
\newacronym{pwm}{PWM}{Pulse Width Modulation}
|
||||
\newacronym{opa}{OpAmp}{Operational Amplifier}
|
||||
\newacronym{adc}{ADC}{Analogue to Digital Converter}
|
||||
\newacronym{iss}{ISS}{International Space Station}
|
||||
\newacronym{gsoc}{GSOC}{German Space Operation Centre}
|
||||
\newacronym{tps}{TPS}{Thermal Protection System}
|
||||
\newacronym{acu}{ACU}{Address Control Unit}
|
||||
\newacronym{p-pod}{P-POD}{Poly Picosatellite Orbital Deployer}
|
||||
\newacronym{csp}{CSP}{?}
|
||||
\newacronym{gosh}{GOSH}{?}
|
||||
\newacronym{pdu}{PDU}{Power Distribution Unit}
|
||||
\newacronym{tfm}{TFM}{?}
|
||||
\newacronym{tml}{TML}{Total Mass Loss}
|
||||
\newacronym{cvcm}{CVCM}{collected volatile condensed material}
|
||||
\newacronym{bpx}{BPX}{NanoPower BPX}
|
||||
\newacronym{obc}{OBC}{Onboard Computer}
|
||||
\newacronym{pe}{PE}{Port Expander}
|
||||
\newacronym{obsw}{OBSW}{Onboard Software}
|
||||
\newacronym{soc}{SoC}{State of Charge}
|
||||
\newacronym{dod}{DoD}{Depth of Discharge}
|
||||
\newacronym{ipa}{IPA}{Institute for Manufacturing Engineering and Automation}
|
||||
\newacronym{sso}{SSO}{Sun Synchronous Orbit}
|
||||
\newacronym{ksc}{KSC}{Kennedy Space Center}
|
||||
\newacronym{csg}{CSG}{Centre Spatial Guyanais}
|
||||
\newacronym{ploc}{PLOC}{Payload Onboard Computer}
|
||||
\newacronym{uhf}{UHF}{ultra high frequency}
|
||||
\newacronym{ccsds}{CCSDS}{Consultative Committee for Space Data Systems}
|
||||
\newacronym{ecss}{ECSS}{European Cooperation for Space Standardization}
|
||||
\newacronym{fsfw}{FSFW}{Flight Software Framework}
|
||||
\newacronym{obcp}{OBCP}{On-board control procedure}
|
||||
\newacronym{apid}{APID}{Application Process ID}
|
||||
\newacronym{soa}{SOA}{Service Oriented Architecture}
|
||||
\newacronym{pus}{PUS}{Packet Utilisation Standard}
|
||||
\newacronym{stem}{STEM}{Science, technology, engineering, and mathematics}
|
||||
\newacronym{pcb}{PCB}{Printed Circuit Board}
|
||||
\newacronym{fipex}{FIPEX}{Flux Phi Probe Experiment}
|
||||
\newacronym{spi}{SPI}{Serial Peripheral Interface}
|
||||
\newacronym{i2c}{I2C}{Inter-Integrated Circuit}
|
||||
\newacronym{rtd}{RTD}{Resistance Temperature Detector}
|
||||
\newacronym{fdir}{FDIR}{Failure Detection, Isolation, and Recovery}
|
||||
\newacronym{mcs}{MCS}{Mission Control Software}
|
||||
\newacronym{vm}{VM}{Virtuell Maschine}
|
||||
\newacronym{tmtc}{TM/TC}{Telemetry and Telecommand}
|
||||
\newacronym{oop}{OOP}{Object-oriented Programming}
|
||||
\newacronym{mgt}{MGT}{Magnetorquer}
|
||||
\newacronym{sus}{SuS}{Sun Sensors}
|
||||
\newacronym{mgm}{MGM}{Magnetometer}
|
||||
\newacronym{gps}{GPS}{Global Positioning System}
|
||||
\newacronym{prima}{PRIma}{PR Imager}
|
||||
\newacronym{meshcam}{MeSHCam}{Meteor, Star and Horizon tracking Camera}
|
||||
\newacronym{mib}{MIB}{Mission Information Base}
|
||||
\newacronym{scos}{SCOS-2000}{Satellite Control and Operation System 2000}
|
||||
\newacronym{leop}{LEOP}{Launch and Early Orbit Phase}
|
||||
\newacronym{cots}{COTS}{commercial off-the-shelf}
|
||||
\newacronym{em}{EM}{Engineering Model}
|
||||
\newacronym{fm}{FM}{Flight Model}
|
||||
\newacronym{qm}{QM}{Qualification Model}
|
||||
\newacronym{bbm}{BBM}{Breadboard Model}
|
||||
\newacronym{uart}{UART}{Universal Asynchronous Receiver-Transmitter}
|
||||
\newacronym{fpga}{FPGA}{Field-Programmable Gate Array}
|
||||
\newacronym{lsb}{LSB}{least significant bit}
|
||||
\newacronym{tc}{TC}{telecommand}
|
||||
\newacronym{tm}{TM}{telemetry}
|
||||
\newacronym{itu}{ITU}{International Telecommunication Union}
|
||||
\newacronym{isl}{ISL}{intersatellite link}
|
||||
\newacronym{fov}{FOV}{field of view}
|
||||
\newacronym{gsd}{GSD}{ground sampling distance}
|
||||
\newacronym{snr}{SNR}{signal to noise ratio}
|
||||
\newacronym{dsp}{DSP}{Digital Signal Processing}
|
||||
\newacronym{fifo}{FIFO}{First In First Out}
|
||||
\newacronym{fec}{FEC}{Forward Error Correction}
|
||||
\newacronym{bpsk}{BPSK}{Binary Phase Shift Keying}
|
||||
\newacronym{qpsk}{QPSK}{Quaternary Phase-Shift Keying}
|
||||
\newacronym{ber}{BER}{Bit Error Rate}
|
||||
\newacronym{hpc}{HPC}{high priority command}
|
||||
\newacronym{imu}{IMU}{inertial measurement unit}
|
||||
\newacronym{imus}{IMU's}{inertial measurement units}
|
||||
\newacronym{bch}{BCH}{Bose-Chaudhuri-Hocquenghem}
|
||||
\newacronym{il}{IL}{interleaving depth}
|
||||
\newacronym{mems}{MEMS}{Micro-Electro-Mechanical Systems}
|
||||
\newacronym{hefdig}{HEFDiG}{High Enthalpy Flow Diagnostics Group}
|
||||
\newacronym{nisi}{NISI}{Noninteger System Identification}
|
||||
\newacronym{pwt}{PWT}{IRS Plasma Wind Tunnel}
|
||||
\newacronym{jtag}{JTAG}{Joint Test Action Group}
|
||||
\newacronym{dcsa}{DCSA}{Deployable CubeSat Solar Array}
|
||||
\newacronym{pvch}{PVCH}{Photovoltaic Characterization Hardware}
|
||||
\newacronym{srn}{SRN}{Switchable Resistor Network}
|
||||
\newacronym{pdr}{PDR}{Preliminary Design Review}
|
||||
\newacronym{fys}{FYS}{Fly Your Satellite}
|
||||
\newacronym{nga}{NGA}{National Geospatial-Intelligence Agency}
|
||||
\newacronym{dgc}{DGC}{Defence Geographic Centre}
|
||||
\newacronym{fdm}{FDM}{Fused Deposition Modeling}
|
||||
\newacronym{osi}{OSI}{Open Systems Interconnection}
|
||||
\newacronym{vhdl}{VHDL}{Very High Speed Integrated Circuit Hardware Description Language}
|
||||
\newacronym{mss}{MSS}{mobile satellite communications systems}
|
||||
\newacronym{nasa}{Nasa}{National Aeronautics and Space Administration}
|
||||
\newacronym{rr}{RR}{Radio Regulations}
|
||||
\newacronym{cdr}{CRD}{Critical Design Review}
|
||||
\newacronym{lna}{LNA}{low noise amplifier}
|
||||
\newacronym{pa}{PA}{power amplifier}
|
||||
\newacronym{sti}{STI}{SpaceTech GmbH Immenstaad}
|
||||
\newacronym{esd}{ESD}{Electrostatic Discharge}
|
||||
\newacronym{hil}{HiL}{Hardware in the Loop}
|
||||
\newacronym{sil}{SiL}{Software in the Loop}
|
||||
\newacronym[description=Application Programming Interface]{api}{API}{application programming interface}
|
||||
\newacronym{sdi}{SDI}{Serial Digital Interface}
|
||||
\newacronym{aoi}{AOI}{Area Of Interest}
|
||||
\newacronym{dr}{DR}{Dynamic Range}
|
||||
\newacronym{iag}{IAG}{Institute of Aerodynamics and Gas Dynamics}
|
||||
\newacronym{dsmc}{DSMC}{Direct Simulation Monte Carlo}
|
||||
\newacronym{afwa}{AFWA}{Air Force Weather Agency}
|
||||
\newacronym{pslv}{PSLV}{Polar Satellite Launch Vehicle}
|
||||
\newacronym{isro}{ISRO}{Indian Space Research Organisation}
|
||||
\newacronym{sdsc}{SDSC}{Satish Dhawan Space Centre}
|
||||
\newacronym{simcard}{SIM card}{Subscriber Identity Module card}
|
||||
\newacronym{emc}{EMC}{electromagnetic compability}
|
||||
\newacronym{sbd}{SBD}{Short Burst Data}
|
||||
\newacronym{rudics}{RUDICS}{ Router based Unrestricted Digital Interworking Connectivity Solution}
|
||||
\newacronym{sms}{SMS}{Short Message Service}
|
||||
\newacronym{rhcp}{RHCP}{Right-Hand Circular Polarization}
|
||||
\newacronym{fa}{FA}{Field Application}
|
||||
\newacronym{isu}{ISU}{Iridium Subscriber Unit}
|
||||
\newacronym{peek}{PEEK}{Polyether ether ketone}
|
||||
\newacronym{cf}{CF}{Carbon fiber}
|
||||
\newacronym{mosfet}{MOSFET}{Metal–Oxide–Semiconductor Field-Effect Transistor}
|
||||
\newacronym{plv}{PlV}{Payload Processor Vorago}
|
||||
\newacronym{plp}{PlP}{Payload Processor PIC24}
|
||||
\newacronym{edac}{EDAC}{Error Detectionn And Correction}
|
||||
\newacronym{gpio}{GPIO}{General Purpose Input/Output}
|
||||
\newacronym{tfsp}{TFSP}{Thin Film Solar Panel}
|
||||
\newacronym{mgse}{MGSE}{Mechanical Ground Support Equipment}
|
||||
\newacronym{egse}{EGSE}{Electrical Ground Support Equipment}
|
||||
\newacronym{ogse}{OGSE}{Optical Ground Support Equipment}
|
||||
\newacronym{gse}{GSE}{Ground Support Equipment}
|
||||
\newacronym{tid}{TID}{total ionizing dose}
|
||||
\newacronym{hdm}{HdM}{Hochschule der Medien}
|
||||
\newacronym{syoc}{SoC}{System on a Chip}
|
||||
\newacronym{cskb}{CSKB}{CubeSat Kit Bus}
|
||||
\newacronym{str}{STR}{Star Tracker}
|
||||
\newacronym{htr}{HTR}{Horizon Tracker}
|
||||
\newacronym{wmm}{WMM}{World Magnetic Model}
|
||||
\newacronym{igrf}{IGRF}{International Geomagnetic Reference Field}
|
||||
\newacronym{gafe}{GAFE}{Generic AOCS GNC Techniques and Design Framework for FDIR}
|
||||
\newacronym{eive}{EIVE}{Extraplanetary In-Orbit Verification of an E-/W-Link}
|
||||
\newacronym{psu}{PSU}{Power Supply Unit}
|
||||
\newacronym[description=User Interface]{ui}{UI}{user interface}
|
||||
\newacronym[description=Comma Separated Values file]{csv}{CSV}{Comma Separated Values}
|
||||
\newacronym{usb}{USB}{Universal Serial Bus}
|
||||
\newacronym[description=Light-Emitting Diode]{led}{LED}{light-emitting diode}
|
||||
\newacronym{pla}{PLA}{Polylactic Acid, a thermoplastic polyester}
|
||||
\newacronym{pc}{PC}{Personal Computer}
|
||||
\newacronym{tbd}{TBD}{To Be Determined}
|
||||
\newacronym{mpi}{MPI}{Message Passing Interface}
|
||||
\newacronym{dut}{DUT}{Device Under Test}
|
||||
@@ -0,0 +1,10 @@
|
||||
\chapter{Example Program Auxiliary Files} \label{app:example_files}
|
||||
\textbf{Example Configuration File}
|
||||
\begin{small}
|
||||
\lstinputlisting{Appendix/example_config.ini}
|
||||
\end{small}
|
||||
\bigskip
|
||||
\textbf{Example Log File}
|
||||
\begin{small}
|
||||
\lstinputlisting{Appendix/logfile_example.csv}
|
||||
\end{small}
|
||||
@@ -0,0 +1,30 @@
|
||||
[X-Axis]
|
||||
coil_const = 3.883e-05
|
||||
ambient_field = 0.0
|
||||
resistance = 3.131
|
||||
max_volts = 15.0
|
||||
max_amps = 5.0
|
||||
relay_pin = 15
|
||||
|
||||
[Y-Axis]
|
||||
coil_const = 3.865e-05
|
||||
ambient_field = 0.0
|
||||
resistance = 3.107
|
||||
max_volts = 15.0
|
||||
max_amps = 5.0
|
||||
relay_pin = 16
|
||||
|
||||
[Z-Axis]
|
||||
coil_const = 3.73e-05
|
||||
ambient_field = 0.0
|
||||
resistance = 3.129
|
||||
max_volts = 15.0
|
||||
max_amps = 5.0
|
||||
relay_pin = 17
|
||||
|
||||
[Supplies]
|
||||
supply_model = ql355tp
|
||||
arduino_port = COM1
|
||||
xy_port = COM2
|
||||
z_port = COM3
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
Date;Time;t (s);X Target Field;Y Target Field;Z Target Field
|
||||
2021-03-08;16:18:34,583986;0,0;0,0;0,0;0,0
|
||||
2021-03-08;16:18:39,595109;5,011123;0,0;0,0;0,0
|
||||
2021-03-08;16:18:44,217410;9,633424;-4,499999996e-05;-4,999999996e-05;0,0
|
||||
2021-03-08;16:18:44,597378;10,013392;-4,499999996e-05;-4,999999996e-05;0,0
|
||||
2021-03-08;16:18:49,604153;15,020167;-4,499999996e-05;-4,999999996e-05;0,0
|
||||
2021-03-08;16:18:54,619011;20,035025;-4,499999996e-05;-4,999999996e-05;0,0
|
||||
2021-03-08;16:18:57,713520;23,129534;-4,499999996e-05;4,999999996e-05;0,0
|
||||
2021-03-08;16:18:59,635428;25,051442;-4,499999996e-05;4,999999996e-05;0,0
|
||||
2021-03-08;16:19:00,759706;26,17572;0,0;0,0;0,0
|
||||
|
@@ -0,0 +1,352 @@
|
||||
\chapter{Software Implementation}\label{chp:software}
|
||||
\section{Program Structure}
|
||||
\glsunset{psu}
|
||||
\glsunset{irs}
|
||||
To operate the test bench, a Python software with a graphical \gls{ui} was developed.
|
||||
It manages the test bench hardware, such as the PS2000B and QL355TP Power Supply Units (\gls{psu}), the polarity switch box, and magnetometers.
|
||||
This chapter focuses on the overall software implementation.
|
||||
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/eive/Helmholtz_Test_Bench.git}}.
|
||||
For a user manual, please refer to Section \ref{sec:software_guide}.
|
||||
|
||||
Software development and testing were done in Windows 10 and Python 3.9.
|
||||
Some aspects may need to be adapted to use the software on a different operating system or Python version.
|
||||
The software in its current state (26. October 2021) has been fully tested with the complete hardware suite.
|
||||
For the complete system test, a FGM3D magnetometer, QL355TP \gls{psu}, and the polarity switcher were used and are thus recommended as a reference configuration.
|
||||
|
||||
\begin{figure}[h]
|
||||
\centering
|
||||
\includegraphics[width=\textwidth]{media/complete_software_architecture}
|
||||
\caption{Software file layout and architecture}
|
||||
\label{fig:softwarelayout}
|
||||
\end{figure}
|
||||
|
||||
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.
|
||||
|
||||
Upon execution of \code{main.py}, a test bench proxy object is initialized that acts as a wrapper around the \gls{psu} and Arduino interfaces and calls.
|
||||
Next, the \gls{ui} (controlled by \code{user\_interface.py}) is set up and displayed.
|
||||
Advanced software functionality is split into separate files and components that are called from the \gls{ui} code.
|
||||
This includes the CSV and calibration functionality for example.
|
||||
All program elements use the global \code{HelmholtzCageDevice} object instance defined in the \code{helmholtz\_cage\_device.py} file to control and read data from the test bench.
|
||||
Communication with some of the hardware is achieved via modified third-party libraries such as \code{PS2000B.py} and \code{Arduino.py}, which were taken from online sources \cite{PS2000B_lib, Arduino_lib}.
|
||||
\code{globals.py} is used to easily pass frequently used variables between the different program files.
|
||||
It contains mainly constants and defaults as well as some global instances, 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, if one \gls{psu} were to be disconnected, commands and fields could still be applied to the axes accessible on the other \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 code to start (\code{program\_start}) and stop (\code{program\_end}) the program.
|
||||
During program initialization the information from the default configuration file is read out to set the necessary constants.
|
||||
The startup defaults may be modified according to the currently used hardware setup.
|
||||
Afterwards, the global Helmholtz cage and magnetometer proxy device instances are created.
|
||||
These instances are requisite for the following \gls{ui} initialization, but do not yet need to be connected to hardware devices.
|
||||
Afterwards, the graphical and TCP interfaces are created, and the hardware initialization is triggered in a non-blocking, asynchronous function call.
|
||||
Finally, the UI's \code{mainloop} is 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 levels, the main file also includes application-level error handling.
|
||||
In such a case an error message is displayed and the \code{program\_end} function called 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 shared between modules.
|
||||
Examples of these are the \code{CAGE\_DEVICE} and \code{MAGNETOMETER} objects that represent hardware devices, but also status indicators like \code{exit\_flag}, which signals the end of the program to all components.
|
||||
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 is also used to regenerate the default configuration.
|
||||
The dictionary also includes the minimum and maximum safe value for each constant, which 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{helmholtz\_cage\_device.py}}
|
||||
\label{sec:cage_func}
|
||||
This file contains all classes directly related to the operation of the test bench.
|
||||
It includes the main control class \code{HelmholtzCageDevice}, its proxy \code{HelmholtzCageProxy}, as well as the \code{Axis} adapter object used by both.
|
||||
Magnetometer interfacing is handled in the separate \code{magnetometer.py} file.
|
||||
The application contains a single global \code{HelmholtzCageDevice} instance, contained in the \code{CAGE\_DEVICE} variable.
|
||||
|
||||
\myparagraph{Main Control Class \code{HelmholtzCageDevice}}
|
||||
This class provides the following important functionality:
|
||||
\begin{itemize}
|
||||
\item Asynchronous connection and disconnection of hardware devices (\code{connect\_hardware\_async} and \code{shutdown}).
|
||||
These functions may be called at any time during program execution, and are used as the primary means of reloading configuration variables and connecting new hardware at runtime.
|
||||
\item Set currents, raw fields, and compensated fields in the test bench coils.
|
||||
Uses \code{\_set\_signed\_currents}, \code{\_set\_field\_raw}, and \code{\_set\_field\_compensated}.
|
||||
These functions are called internally when the corresponding command is received in the \code{\_cmd\_exec\_thread}.
|
||||
\item Hardware status and control is mediated by an array of \code{Axis} adapter objects.
|
||||
Each axis stores its own specific configuration data and related status information.
|
||||
\item Components may provide callbacks to the \code{subscribe\_status\_updates} function, to receive periodic status information about the test bench.
|
||||
This is more sensible than polling, since this operation is slow.
|
||||
\end{itemize}
|
||||
Device access is limited to one application component at once by means of a proxy pattern.
|
||||
With the \code{request\_proxy} and \code{release\_proxy} functions, up to one \code{HelmholtzCageProxy} instance may be obtained if none are currently active.
|
||||
This proxy must be released with the latter function, to allow for another component to request a proxy instance.
|
||||
All commands to the test bench are set by means of the proxy object, except for startup and shutdown commands, which must always be available to the application.
|
||||
|
||||
The hardware connection sequence goes as follows:
|
||||
\begin{itemize}
|
||||
\item Read the current config variables (see Section \ref{sec:config_handling}).
|
||||
\item Attempt to connect to Arduino on specified com port.
|
||||
\item Select \gls{psu} object type based on configuration variable.
|
||||
\item Attempt to create \gls{psu} object instances.
|
||||
\item Axis specific config variables are reloaded.
|
||||
\item All \gls{psu} channels are zeroed and activated. The Arduino relay pins are set to their default ``low'' state.
|
||||
\end{itemize}
|
||||
|
||||
The disconnection sequence:
|
||||
\begin{itemize}
|
||||
\item Zero and disable all \gls{psu} output channels. The \gls{psu} serial device is disconnected.
|
||||
\item The Arduino relay pins are set to low, and the serial device is closed.
|
||||
\item Information regarding the shutdown process is presented to the user.
|
||||
\end{itemize}
|
||||
|
||||
\myparagraph{Axis Adapter Class \code{Axis}}
|
||||
This class is an adapter object representing a single 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 the global \code{HelmholtzCageDevice} instance.
|
||||
The class contains the primary way to command the test bench hardware 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\_compensated} 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\_raw} 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{Value checking function \code{value\_in\_limits}}
|
||||
This function checks whether a value is within the safe limits defined in \code{globals.py}.
|
||||
|
||||
\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{globals.CAGE\_DEVICE} instance 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}.
|
||||
Buttons are provided to load and run \gls{csv} sequences.
|
||||
To execute the sequence without blocking the \gls{ui}, a separate execution thread defined in \code{csv\_threading.py} is created and managed by this class.
|
||||
Hardware control is acquired upon creation of the thread, or an exception is returned otherwise.
|
||||
More details on the execution thread is provided in Section \ref{sec:csv_exec}.
|
||||
|
||||
\myparagraph{Magnetometer Calibration Mode Class \code{CalibrateMagnetometer}}
|
||||
This class constructs magnetometer calibration interface.
|
||||
It is placed in the application's main area, its layout is shown in Figure \ref{fig:magcalibrationpure}.
|
||||
This class creates and manages a separate execution thread defined in \code{calibration.py} to preform the calibration without blocking the \gls{ui}.
|
||||
Hardware control is acquired upon creation of the thread, or an exception is returned otherwise.
|
||||
More details on the calibration thread is provided in Section \ref{sec:calibration_processes}.
|
||||
Communication with the running thread is preformed by means of the \code{view\_mpi\_queue} queue variable, which is regularly polled in \code{update\_view}.
|
||||
This is where different \gls{mpi} commands are delegated to their respective handlers.
|
||||
For example, the live completion status and final calibration results are handled here.
|
||||
It should be of note, that the calibration threads do not store any results or information, instead these are contained in variables defined near the top of \code{\_\_init\_\_}.
|
||||
|
||||
\myparagraph{Ambient Field Calibration Mode Class \code{CalibrateAmbientField}}
|
||||
This class consists of an identical structure to the Magnetometer Calibration View Class.
|
||||
The only major difference is that this view provides an interface for two calibration procedures; The ambient field and the coil constants.
|
||||
All relevant calibration worker threads can also be found in \code{calibration.py}.
|
||||
|
||||
\myparagraph{Settings Page Class \code{Configuration}}
|
||||
The program settings page is constructed in this class.
|
||||
It provides a \gls{ui} to edit application settings and also controls the reading and writing of configuration files.
|
||||
Like the 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}.
|
||||
It interfaces with the \code{config\_handling.py} file to store this information in a \code{ConfigParser} object.
|
||||
The \code{ConfigParser} object can then also be read and written to and from configuration files (see Section \ref{sec:config_handling}).
|
||||
Individual entry fields are automatically highlighted when values exceed the safe limits defined in \code{globals.py} to notify the user.
|
||||
Usage instructions and additional information can be found in Section \ref{sec:settings_guide}.
|
||||
|
||||
\myparagraph{Data Logging Configuration Page Class \code{ConfigureLogging}}
|
||||
This class generates the page for configuring and controlling 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 dictionary of log options.
|
||||
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 config dictionary 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.
|
||||
\code{StatusDisplay} subscribes to status updates from \code{HelmholtzCageDevice}, by registering a callback that queues the new information.
|
||||
The \code{update\_labels} method is called with the new status information and updates the label variables.
|
||||
A shared queue and regular polling for new status information in the main thread is used to avoid calling Tkinter code outside the main thread.
|
||||
Updates are received from the cage object at a polling rate of \SI{1}{\second}.
|
||||
|
||||
\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 \code{ui\_print} function defined in \code{utility.py}, 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.
|
||||
The method also continuously polls 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 visually reflect the discrete and nearly instantaneous change of fields in the test bench (see result in Figure \ref{fig:ui_overview}).
|
||||
|
||||
\subsection{Calibration Procedures \code{calibration.py}}
|
||||
\label{sec:calibration_processes}
|
||||
This file contains the worker thread objects \code{AmbientFieldCalibration}, \code{CoilConstantCalibration}, \code{MagnetometerCalibrationSimple}, and \code{MagnetometerCalibrationComplete}.
|
||||
All of these threads start by checking for and acquiring hardware during instantiation, while still in the main thread.
|
||||
Then upon running, the main code in \code{calibration\_procedure} is executed; This function may freely block and make use of sleep commands.
|
||||
Typically, calibration procedures should save two data detail levels: Processed data that is displayed as a final result, and detailed, raw data points that can be exported by the user to apply custom algorithms or verify the applications function.
|
||||
|
||||
The calibration procedures communicate with the main thread via a \gls{mpi} queue.
|
||||
Depending on whether the procedure completes successfully or not, a \code{finished} or \code{failed} status signal is sent.
|
||||
In addition, a \code{calibration\_data} message is sent together with ``\code{finished}'' to convey the final results to the main thread.
|
||||
Other signals may also be used, such as \code{progress} to indicate the current completion status to the main thread.
|
||||
Data that should be passed to the calibration procedure from the main thread, is done so through the constructor.
|
||||
|
||||
\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.
|
||||
|
||||
A \code{logging\_selection\_options} dict, contains a key-value list of logging options and their display labels, which are used to generate the \gls{ui}.
|
||||
The \code{ConfigureLogging} class of the \gls{ui} then later 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.
|
||||
To log a data point, this option dict is passed to the \code{log\_datapoint} function.
|
||||
According to the options set, data is aggregated from the \code{CAGE\_DEVICE} and \code{MAGNETOMETER} components and assembled into a key-value dictionary.
|
||||
Since no unified set of keys covering all of the logged parameters exist, new, understandable keys were selected.
|
||||
The dict of log values is appended to the end of a Python list that collects every data point.
|
||||
When the logging is finished, a \gls{csv} file is assembled and saved using the \code{write\_to\_file} function.
|
||||
Assembly consists of first prepending two headers to the data rows, one containing the dictionary keys, and one containing the user-readable long names.
|
||||
The long names can be obtained from the \code{get\_long\_column\_header} function.
|
||||
|
||||
The file also provides functions to allow the user to choose a filename and clear the logged data.
|
||||
|
||||
\subsection{TCP Remote Control \code{socket\_control.py}}
|
||||
This class is responsible for enabling TCP control of the Helmholtz cage hardware.
|
||||
In addition, it is the sole data path for magnetometer data to ingress into the control software.
|
||||
As with all other software components, the command processing thread accesses the hardware using the global \code{g.CAGE\_DEVICE} and \code{g.MAGNETOMETER} instances, and uses the proxy model to prevent control collisions.
|
||||
|
||||
The \code{socket\_control.py} file defines the \code{SocketInterfaceThread} class, which provides a listener thread to accept incoming TCP connections.
|
||||
The listener thread instance is defined at application start-up in \code{main.py}.
|
||||
For each established connection, a \code{ClientConnectionThread} instance is created.
|
||||
All the main logic and command parsing is handled the client connection/session threads.
|
||||
The \gls{api} is documented in Chapter \ref{sec:tcp_api} and also in the application source code.
|
||||
|
||||
Note: Following an application crash, the application may present an error in acquiring the TCP port.
|
||||
This may be fixed by waiting a few minutes, or restarting the computer.
|
||||
Perhaps this special case can be fixed with a more rigorous exit handler in the future.
|
||||
|
||||
\subsection{Hardware Control Libraries}
|
||||
|
||||
\myparagraph{``\code{PSUDevice}'' Abstract Class and PSU Drivers \code{psu\_device.py} / \code{PS2000B.py}}
|
||||
|
||||
An abstract class \code{PSUDevice} describing any compatible power supply object is used to allow for quick and easy addition of new device support.
|
||||
This improvement was implemented since power supplies may be replaced or upgraded on a moderately frequent basis.
|
||||
The abstract class exposes functions to set currents and voltages, and also to query the current device status.
|
||||
QL355TP devices are natively supported with the \code{PSUDeviceQL355TP} object, that is also defined in \code{psu\_device.py}.
|
||||
|
||||
Since the code was at some point migrated to require a consistent interface for PSU objects, the original PS2000B library had to be put in a wrapper class (\code{PSUDevicePS2000B}).
|
||||
The external library provides the class \code{PS2000B} and some supporting functions, and was adapted from S. Sprößig \cite{PS2000B_lib} with only minor modifications.
|
||||
More information can be found in the read me file provided by the original author.
|
||||
The library objects provide effectively the same set of functions advertised by the abstract class, as such the wrapper is quite trivial.
|
||||
In the future, it may make sense to migrate the library code directly into the \code{PSUDevicePS2000B} class.
|
||||
|
||||
The main modification to the PS2000B 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} / \code{arduino\_device.py}}
|
||||
To control the Arduino microcontroller, an interface and wrapper class is used.
|
||||
First, the online library from \cite{Arduino_lib} is integrated without major modifications to expose a hardware control \gls{api}.
|
||||
It communicates with the provided \code{prototype.ino} Arduino program to allow toggling and control of its pins with methods that closely resemble the ones used in the standard Arduino programming language.
|
||||
More details can be found in the read me file provided by the original author \cite{Arduino_lib}.
|
||||
|
||||
The \code{Arduino} class is then wrapped in the \code{ArduinoDevice} class defined in \code{arduino\_device.py}, to expose a more polarity-switcher-relevant \gls{api}.
|
||||
This simplifies its use in the already complex \code{HelmholtzCageDevice} function.
|
||||
|
||||
\myparagraph{Magnetometer Device \code{magnetometer.py}}
|
||||
The magnetometer object is hardware-agnostic, and merely mirrors the state of an external physical device.
|
||||
To enable quick and optimal compatibility with new magnetometer devices, data is ingested by means of the TCP port and the application functions purely as a listener.
|
||||
External magnetometer interface scripts will first push their data according to the \gls{api} defined in \code{socker\_control.py}, which will then be forwarded to the global \code{MagnetometerProxy} instance.
|
||||
This means the magnetometer proxy object is purely passive, and functions like a simple data container.
|
||||
|
||||
The magnetometer object mirrors the state of the TCP connection, which is presented as a connected/disconnected state.
|
||||
|
||||
\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 \code{pip install auto-py-to-exe} in the project's virtual environment.
|
||||
\item Execute "auto-py-to-exe" in a console. To make sure the right version is executed, don't install auto-py-to-exe in your system environment.
|
||||
\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}
|
||||
@@ -0,0 +1,621 @@
|
||||
\chapter{Operating Instructions}\label{chp:users_guide}
|
||||
\section{Test Bench Assembly Instructions}
|
||||
Because of the limited space available in the \gls{irs} cleanroom, the test bench may need to be disassembled and reassembled in the future. Instructions for this are provided here. Mentioned position numbers relate to those shown in Figure \ref{fig:assembly}.
|
||||
|
||||
\begin{figure}[h]
|
||||
% preliminary
|
||||
\sbox\twosubbox{%
|
||||
\resizebox{\dimexpr.99\textwidth-1em}{!}{%
|
||||
\includegraphics[trim={3 0 3 0}, clip, height=4cm]{media/Frame_iso.pdf}%
|
||||
\includegraphics[height=4cm]{media/Cable Routing.png}%
|
||||
}%
|
||||
}
|
||||
\setlength{\twosubht}{\ht\twosubbox}
|
||||
\centering
|
||||
\subcaptionbox{Structural frame \cite{Blessing_TestBench} \label{fig:frame_iso2}}{%
|
||||
\includegraphics[height=\twosubht]{media/Frame_iso.pdf}%
|
||||
}\quad
|
||||
\subcaptionbox{Full test bench and cable routing\protect\footnotemark \label{fig:cable_routing2}}{%
|
||||
\includegraphics[height=\twosubht]{media/Cable Routing.png}%
|
||||
}
|
||||
\caption{Test bench assembly drawings}
|
||||
\label{fig:assembly}
|
||||
\end{figure}
|
||||
\footnotetext{Base drawing from \cite{Blessing_TestBench}}
|
||||
|
||||
\subsection{Disassembly Procedure}\label{sec:disassembly}
|
||||
\myparagraph{Notes:}
|
||||
The coils must always be supported on two sides during handling to avoid bending.\\
|
||||
The used slot nuts are wedged in the structural profiles and will stay in place even when their screw is removed. In most cases this is desirable, to mark the correct positions for reassembly. If one needs to be removed, loosen the attached screw and gently tap its head with a hammer.\\
|
||||
\textbf{All removed screws must be stored orderly, as they are needed for future reassembly!}
|
||||
|
||||
\paragraph{Procedure:}
|
||||
\begin{enumerate}
|
||||
\item Undo cabling
|
||||
\begin{enumerate}
|
||||
\item Disconnect switch box from main cable and \gls{psu}s
|
||||
\item Disconnect coil cables from central screw terminal (Fig. \ref{fig:main_connector})
|
||||
\item Disconnect screw terminals between coils, leave terminal on one side
|
||||
\item Untie wires from their guiding structures
|
||||
\end{enumerate}
|
||||
\item Remove Z-axis coils
|
||||
\begin{enumerate}
|
||||
\item Unscrew upper coil brackets (20) from profiles (08), but leave them on the coil
|
||||
\item Lift off upper coil (A6)
|
||||
\item Remove upper coil support profiles (08), leave angle pieces on main structure
|
||||
\item Repeat procedure for lower coil
|
||||
\end{enumerate}
|
||||
\item Remove Y and X-axis coils
|
||||
\begin{enumerate}
|
||||
\item Unscrew coil brackets (19) for one Y-axis coil (A5) from frame profiles (07), but leave them on the coil
|
||||
\item Lift off coil (A5)
|
||||
\item Repeat procedure for second coil (A5) and X-axis coils (A4)
|
||||
\end{enumerate}
|
||||
\item Remove mounting plate (21)
|
||||
\item Lay remaining frame on its side, mounting plate profiles (09) should be on top and bottom (orientation shown in Figure \ref{fig:assembly_rectangle})
|
||||
\item Separate "upper" rectangular frame section (highlighted yellow in Figure \ref{fig:assembly_rectangle})
|
||||
\begin{enumerate}
|
||||
\item Remove bolts of angles connecting rectangle to cross-member profiles (07), angle pieces should remain on "upper" rectangle
|
||||
\item Lift off entire rectangle section
|
||||
\end{enumerate}
|
||||
\item Remove cross-member profiles (07) from "lower" rectangle section, angles stay attached
|
||||
\end{enumerate}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\begin{minipage}{0.49\textwidth}
|
||||
\includegraphics[width=\textwidth]{media/wire_collector}
|
||||
\caption{Main connector (X-cables temp.)}
|
||||
\label{fig:main_connector}
|
||||
\end{minipage}
|
||||
\begin{minipage}{0.49\textwidth}
|
||||
\includegraphics[width=\textwidth]{media/disassembly_frame1}
|
||||
\caption{Intermediate (dis-)assembly step}
|
||||
\label{fig:assembly_rectangle}
|
||||
\end{minipage}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Reassembly Procedure}
|
||||
\myparagraph{Notes:}
|
||||
These instructions assume a disassembly according to Section \ref{sec:disassembly}. Most slot nuts should still be in place and simplify positioning the parts. However, the angle pieces do allow some tolerance, so care should still be taken to get the correct alignments.
|
||||
|
||||
\paragraph{Procedure:}
|
||||
\begin{enumerate}
|
||||
\item Lay one main frame rectangle on the floor as shown in Figure \ref{fig:assembly_rectangle}
|
||||
\item Attach cross-member profiles (07)
|
||||
\item Attach second frame rectangle on top, state should now be as shown in Figure \ref{fig:assembly_rectangle}.
|
||||
\item Lift frame upright and adjust foot heights for solid stand
|
||||
\item Attach mounting plate (21)
|
||||
\item Install X and Y-axis coils
|
||||
\begin{enumerate}
|
||||
\item Lift +X coil (A4) onto cross-member profiles (07); mind wire exit position (Fig. \ref{fig:cable_routing2})
|
||||
\item If needed, adjust cross-member profile height; coil should be supported equally on upper and lower profiles without bending
|
||||
\item Centre coil and secure with 3D-printed brackets (19)
|
||||
\item Repeat for -X (A4)and Y-axis coils (A5)
|
||||
\end{enumerate}
|
||||
\item Install Z-axis coils (A6)
|
||||
\begin{enumerate}
|
||||
\item Attach lower coil support profiles (08)
|
||||
\item Lift -Z coil over X/Y coils onto support profiles; mind wire exit position (Fig. \ref{fig:cable_routing2})
|
||||
\item Centre coil and secure with 3D-printed brackets (20)
|
||||
\item Repeat for upper support profiles and +Z coil
|
||||
\end{enumerate}
|
||||
\item Make electrical connections
|
||||
\begin{enumerate}
|
||||
\item Connect screw terminals between coils (unmarked wire ends)
|
||||
\item Route and tie down wires as shown in Figure \ref{fig:cable_routing2}
|
||||
\item Check that there are no short circuits between coil wires and frames
|
||||
\item Connect main screw terminal as shown in Figure \ref{fig:main_connector}
|
||||
\item Connect main cable to switch box
|
||||
\item Verify contact on all axes (multimeter at switch box inputs), resistances approx. {\SI{3.1}{\ohm}}
|
||||
\item Connect switch box to \gls{psu}s: X-axis to \gls{psu} 1 channel 1, Y-axis to \gls{psu} 1 channel 2, Z-axis to \gls{psu} 2 channel 1
|
||||
\end{enumerate}
|
||||
\item Setup laptop and initialize control program as described in Section \ref{sec:software_init}
|
||||
\item Verify correct polarity on all axes through magnetic field measurements at different currents
|
||||
\item Calibrate test bench (exact procedure to be developed)
|
||||
\end{enumerate}
|
||||
|
||||
\newpage
|
||||
\section{Software Users Guide}\label{sec:software_guide}
|
||||
\subsection{Installation}
|
||||
\begin{enumerate}
|
||||
\item Download latest release: \url{https://egit.irs.uni-stuttgart.de/eive/Helmholtz_Test_Bench/releases}
|
||||
\item Unpack ZIP-folder and run "Release{\textbackslash}Helmholtz Control.exe"
|
||||
\item Setup hardware and program according to Section \ref{sec:software_init}
|
||||
\end{enumerate}
|
||||
|
||||
\subsection{User Interface Elements}
|
||||
The general \gls{ui} layout is shown in Figure \ref{fig:ui_overview}. The upper area contains the interactive elements of each operating mode and settings page. Switching between these pages is done using the top "Menu" bar. The lower half of the \gls{ui} contains the status display and a console output.
|
||||
|
||||
\subsubsection*{Status Display}
|
||||
The status display shows the current state of the test bench devices. Explanations of the different values are given in Table \ref{tab:status_contents}.
|
||||
|
||||
\small
|
||||
\begin{longtable}{lp{12.5cm}}
|
||||
\caption{Status display entries}\\
|
||||
\hline
|
||||
\textbf{Entry} & \textbf{Explanation}\\ \hline
|
||||
\endfirsthead
|
||||
\multicolumn{2}{c}%
|
||||
{\tablename\ \thetable\ -- \textit{Continued from previous page}} \\
|
||||
\hline
|
||||
\textbf{Entry} & \textbf{Explanation}\\ \hline
|
||||
\endhead
|
||||
\hline \multicolumn{2}{r}{\textit{Continued on next page}} \\
|
||||
\endfoot
|
||||
\hline
|
||||
\endlastfoot
|
||||
\gls{psu} Serial Port: & Serial \gls{com} port used to connect to the \gls{psu} for this axis. \\[8pt]
|
||||
\gls{psu} Channel: & \gls{psu} output channel for this axis (0$\rightarrow$channel 1, 1$\rightarrow$channel 2). \\[8pt]
|
||||
\gls{psu} Status: & Connection status of \gls{psu} for this axis. Possible states: \vspace{-1.5mm}
|
||||
\begin{itemize}
|
||||
\setlength\itemsep{-0.2em}
|
||||
\item \textit{Connected:} communication nominal
|
||||
\item \textit{Not Connected:} \gls{psu} not found during initialization
|
||||
\item \textit{Connection Error:} \gls{psu} was connected but then an error occurred, e.g. it was disconnected
|
||||
\end{itemize} \\[-4pt]
|
||||
Arduino Status: & Connection status of switch box Arduino (identical for all axes). Possible states: \vspace{-1.5mm}
|
||||
\begin{itemize}
|
||||
\setlength\itemsep{-0.2em}
|
||||
\item \textit{Connected:} communication nominal
|
||||
\item \textit{Not Connected:} Arduino not found during initialization
|
||||
\item \textit{Connection Error:} Arduino was connected but then an error occurred, e.g. it was disconnected
|
||||
\end{itemize} \\[-4pt]
|
||||
Output: & Status of \gls{psu} output channel. Possible states: \vspace{-1.5mm}
|
||||
\begin{itemize}
|
||||
\setlength\itemsep{-0.2em}
|
||||
\item \textit{Active:} set current or voltage is applied to the output jacks
|
||||
\item \textit{Inactive:} output jacks are unpowered
|
||||
\item \textit{Unknown:} no connection to \gls{psu}
|
||||
\end{itemize} \\[-4pt]
|
||||
Remote Control: & Status of \gls{psu} channel remote interface. Possible states: \vspace{-1.5mm}
|
||||
\begin{itemize}
|
||||
\setlength\itemsep{-0.2em}
|
||||
\item \textit{Active:} channel can be controlled remotely
|
||||
\item \textit{Inactive:} channel can not be controlled remotely
|
||||
\item \textit{Unknown:} no connection to \gls{psu}
|
||||
\end{itemize}\\[-4pt]
|
||||
Voltage Setpoint: & Maximum voltage \gls{psu} is set to supply. \\[4pt]
|
||||
Actual Voltage: & Voltage across \gls{psu} channel output jacks. Usually lower than "Voltage Setpoint", as voltage is throttled to achieve the desired current. \\[16pt]
|
||||
Current Setpoint: & Current the \gls{psu} output channel is set to supply \\[8pt]
|
||||
Actual Current: & Current flowing through the \gls{psu} output channel. \\[8pt]
|
||||
Target Field: & Desired magnetic flux density in the measurement area. \\[8pt]
|
||||
Trgt. Field Raw: & Flux density used to calculate needed current (after ambient field compensation). \\[8pt]
|
||||
Target Current: & Desired current to flow through the coils. Negative values mean reversed polarity. \\[8pt]
|
||||
Inverted: & Status of polarity change relay for this axis inside the switch box. Possible states:\vspace{-1.5mm}
|
||||
\begin{itemize}
|
||||
\setlength\itemsep{-0.2em}
|
||||
\item \textit{True:} (Arduino pin "HIGH"$\rightarrow$relay switched$\rightarrow$polarity inverted, status \gls{led} should be illuminated)
|
||||
\item \textit{False:} pin "LOW"$\rightarrow$opposite of "True" state
|
||||
\item \textit{Unknown:} no connection to Arduino
|
||||
\end{itemize}\\[-20pt]
|
||||
\label{tab:status_contents}
|
||||
\end{longtable}
|
||||
\normalsize
|
||||
|
||||
\subsubsection*{Manual Mode}\label{sec:manual_mode_guide}
|
||||
The manual input mode is used to set static currents or magnetic fields on the test bench. Its layout is shown in Figure \ref{fig:manualmodepure}. The main \gls{ui} elements are listed below.
|
||||
|
||||
\begin{figure}[h]
|
||||
\centering
|
||||
\includegraphics[width=0.5\linewidth]{media/manual_mode_pure}
|
||||
\caption{Manual input mode user interface}
|
||||
\label{fig:manualmodepure}
|
||||
\end{figure}
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{"Select Input Mode" drop-down}: Switches between setting currents or magnetic fields
|
||||
\item \textbf{Value entry fields:} Enter values to be set on the test bench here
|
||||
\begin{itemize}
|
||||
\item Entries must be numeric (decimal point)
|
||||
\item Entries must be in indicated safe range (may be changed in the settings page)
|
||||
\end{itemize}
|
||||
\item \textbf{"Compensate ambient field" checkbox:} When ticked, the ambient magnetic field (set in settings page) is subtracted from entered values before commanding the test bench (inactive for "Current" input mode)
|
||||
\item \textbf{"Execute" button:} Implements values from the entry fields
|
||||
\begin{itemize}
|
||||
\item Commands currents on \gls{psu}s and polarity on switch box
|
||||
\item If a device is not connected, the remaining ones are still commanded
|
||||
\item Values beyond safe limits (indicated to the right of entry fields) are rejected
|
||||
\item Device status and any errors are displayed in status display and console
|
||||
\end{itemize}
|
||||
\item \textbf{"Power Down All" button}: "Panic button", sets currents on both \gls{psu}s to 0, deactivates outputs and sets switch box relay pins to inactive state
|
||||
\item \textbf{"Reinitialize" button:} Reruns program initialization
|
||||
\begin{itemize}
|
||||
\item Reinitializes connection to \gls{psu}s and switch box Arduino
|
||||
\item Press after (re)connecting a device to let program establish communications
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
|
||||
\textbf{To command a field vector or currents on the test bench:}
|
||||
\begin{enumerate}
|
||||
\item Select needed input mode, using the drop-down menu
|
||||
\item Enter desired values in entry fields
|
||||
\item For magnetic fields, choose whether ambient field should be compensated by (un)ticking the checkbox
|
||||
\item Press "Execute" button, devices will now implement set values
|
||||
\item Check console output to see if any errors occurred
|
||||
\item Monitor behavior in status display and on devices
|
||||
\item When finished, press "Power Down All" button to remove currents from the test bench
|
||||
\end{enumerate}
|
||||
|
||||
\subsubsection*{CSV Sequence Execution Mode}\label{sec:csv_mode_guide}
|
||||
This mode is used to run timed sequences of magnetic fields. These have to be defined in a \gls{csv} file of the following form:
|
||||
\begin{itemize}
|
||||
\item \textit{Column separator:} Semicolon (;)
|
||||
\item \textit{Decimal:} Comma (,) or Period (.)
|
||||
\item \textit{Line terminator:} Tested with Windows standard (\code{\textbackslash r\textbackslash n}), other options may work as well
|
||||
\item \textit{Columns:} Time in seconds; X-axis, Y-axis and Z-axis flux density in Tesla
|
||||
\end{itemize}
|
||||
An example for the \gls{csv} file structure is given below:
|
||||
%[caption=Example \gls{csv} file]
|
||||
\begin{lstlisting}
|
||||
Time (s);xField (T);yField (T);zField (T)
|
||||
0,5;0,000015;0,000025;0,00002
|
||||
1;0,0000155;0,0000245;0,0000205
|
||||
\end{lstlisting}
|
||||
When loading such a file, the application will check that there are no values exceeding the safe test bench limits in the sequence. If any are found, a warning message is shown and the limits displayed in the sequence graph. It is still possible to execute such a sequence, however the excessive values will not be commanded. For those time stamps, \SI{0}{\ampere} is set instead. The limits may be adjusted in the program settings page, but care should be taken to avoid equipment damage.\\
|
||||
\begin{figure}[h]
|
||||
\centering
|
||||
\includegraphics[width=\textwidth]{media/csv_mode_pure}
|
||||
\caption{CSV sequence mode user interface}
|
||||
\label{fig:csvmodepure}
|
||||
\end{figure}
|
||||
|
||||
The \gls{ui} layout is shown in Figure \ref{fig:csvmodepure}, its main elements are listed below.
|
||||
\begin{itemize}
|
||||
\item \textbf{"Select csv file..." button:} Opens a file dialog to let user choose a \gls{csv} file to execute
|
||||
\item \textbf{"Run Sequence" button} Starts executing the sequence from the chosen \gls{csv} file
|
||||
\item \textbf{"Stop Run" button:} Aborts sequence execution
|
||||
\item \textbf{"Reinitialize devices" button:} Reruns program initialization
|
||||
\begin{itemize}
|
||||
\item Reinitializes connection to \gls{psu}s and switch box Arduino
|
||||
\item Press after (re)connecting a device to let program establish communications
|
||||
\end{itemize}
|
||||
\item \textbf{"Disable device connection checks" checkboxes:} Disable connection check for specific devices, allowing sequence execution with some equipment missing
|
||||
\item \textbf{Graph display:} Displays graph of field sequence, as it will be executed by the test bench
|
||||
\end{itemize}
|
||||
\newpage
|
||||
\textbf{To execute a field sequence:}
|
||||
\begin{enumerate}
|
||||
\item Ensure correct configuration of program and test bench in the settings page
|
||||
\item Prepare sequence in \gls{csv} file with correct format (external program)
|
||||
\item Press "Select csv file..." button, select prepared file and open
|
||||
\item Check plots to confirm sequence was loaded correctly and no values exceed safe limits
|
||||
\item If not all devices are used, check boxes of unused devices to disable connection check
|
||||
\item Press "Run Sequence" button
|
||||
\item Monitor execution in console, status display and on devices
|
||||
\end{enumerate}
|
||||
|
||||
\subsubsection*{Ambient Field and Coil Constant Calibration}
|
||||
\label{sec:ambient_field_calibration}
|
||||
The application offers calibration tools to determine the exact ambient field with the intention of cancelling it, as well for measuring the test bench's coil constants.
|
||||
These are both integrated into one application view, depicted in Figure \ref{fig:ambientcalibrationpure}.
|
||||
All the calibration tools require access to the complete Helmholtz cage hardware, as well as a magnetometer.
|
||||
These must be connected before starting the test.
|
||||
It is important that the magnetometer is centered as well as possible to achieve accurate results.
|
||||
For these tests, where the magnetometer itself is not the \gls{dut}, it is recommended to use the IRS's FGM3D reference magnetometer.
|
||||
Further, an adapter script (\code{fgm3d\_adapter.py}) already exists for this sensor.
|
||||
For more details on writing adapter scripts and connecting magnetometers, please refer to Section \ref{sec:tcp_api}.
|
||||
|
||||
\begin{figure}[h]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth]{media/ambient_calibration_pure}
|
||||
\caption{Ambient field and coil constant calibration view.}
|
||||
\label{fig:ambientcalibrationpure}
|
||||
\end{figure}
|
||||
|
||||
After setting up the magnetometer, operation is simple: Click on the buttons to ``Calibrate Ambient Field'' or ``Calibrate Coil Constants''.
|
||||
This will cause the calibration procedure to start.
|
||||
The current status will be shown in the progress bar underneath.
|
||||
After completing, the results of the calibration procedure will be saved into the data fields on the right hand side of Figure \ref{fig:ambientcalibrationpure}.
|
||||
At this point, the buttons underneath the respective will become available.
|
||||
These two calibrations are primarily used to determine important application config parameters, namely the ambient field and coil constant as their names imply.
|
||||
To automatically apply the newly measured values, click ``Save and apply''.
|
||||
The ``Copy to clipboard'' will put the results table as shown in the \gls{ui} into the system clipboard, from which it can be pasted into software such as Microsoft Excel and LibreOffice Calc.
|
||||
If semi-raw experiment data is required to verify the program functioning or to apply custom algorithms, it can be exported with the ``Export raw CSV'' button.
|
||||
|
||||
\vspace{5mm}
|
||||
\textbf{Ambient Field Calibration Method}
|
||||
This calibration uses a P-controller (implemented as PI-controller with $I=0$) to attempt to reach zero as a set point for the magnetometer.
|
||||
Updates are preformed approximately every half second by default and the calibration executes for 45 seconds.
|
||||
The procedure consistently achieves zero offsets below \SI{10}{\nano\tesla}.
|
||||
The ambient field exported by this calibration has been processed by multiplying the current that was required with the current coil constants, thus it is strongly recommended to first preform the coil constant calibration if both are to be executed.
|
||||
|
||||
\vspace{5mm}
|
||||
\textbf{Coil Constant Calibration Method}
|
||||
The coil consant calibration uses a linear distribution of currents in each axis individually to collect coil consant data.
|
||||
Each setpoint is held for 3 seconds before being measured, and by default 8 points distributed across \SI{-3}{\ampere} to \SI{3}{\ampere}.
|
||||
To calculate the individual setpoints corresponding to each setpoint, first the field magnitude compared to the inital conditions is calculated, second the sign is estimated and reapplied, third the field is divided by the applied current.
|
||||
The final result is the average of all constants.
|
||||
In addition to the primary coil constant measurement, the maximum setpoints are also sampled once for each axis to calculate the angles between the coils.
|
||||
This is simply done by calculating the angle between the measured vectors.
|
||||
|
||||
To understand the calibration methods in all detail, it is recommended to look at the \code{calibration.py} source file.
|
||||
|
||||
\subsubsection*{Magnetometer Calibration}
|
||||
\label{sec:magnetometer_calibration}
|
||||
|
||||
The helmholtz control software supports calibrating magnetometers.
|
||||
In preparation, a calibration of the ambient field using the reference magnetometer (FGM3D) should be conducted beforehand to achieve accurate results.
|
||||
Afterwards, the magnetometer must be replaced with the \gls{dut}.
|
||||
Since the software only supports one magnetometer at once, the reference magnetometer (meaning: its adapter script) should be disabled beforehand.
|
||||
Tip: To mount CubeSat magnetometers a purpose built PC104 holder can be found in accompanyment of the Helmholtz cage.
|
||||
|
||||
As the coordinate system of the Helmholtz Cage and the magnetometer may not match, there are two options that available.
|
||||
First, the calibration can be conducted as-is, and the calibration results will reflect the unexpected coordinate system by often yielding negative sensitivities and untypical sensor axis angles.
|
||||
These values are not invalid, but describe a sensor correction that would transform it into the Helmholtz coordinate system.
|
||||
With some work, the desired calibration parameters could be extracted mathematically by changing signs and adding or subtracting \SI{90}{\degree} increments.
|
||||
The second option would be to change the sensors orientation in software in the adapter script that is used.
|
||||
This yields a more expected set of calibration parameters, but these now do not describe the initial axes anymore.
|
||||
In this case, the parameters must be correlated back to their initial axes and angles must be negated if the axis was flipped.
|
||||
This common issue should be solved robustly in the control/calibration software in the future.
|
||||
|
||||
\begin{figure}[h]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth]{media/magnetometer_calibration_pure}
|
||||
\caption{Magnetometer calibration view.}
|
||||
\label{fig:magcalibrationpure}
|
||||
\end{figure}
|
||||
The procedure to start the magnetometer can be done as follows:
|
||||
\begin{itemize}
|
||||
\item Activate the FGMM3D box, start the \textit{FGM3D TD Application} software and check if the device was recognissed automatically (Port: FGM3D TD (000125).
|
||||
If the device was not recognized, click on \textit{File -> Rescan} devices, else restart the computer.
|
||||
\item Click on \textit{connct} and \textit{start}. Telemetry should now be displayed in the application. Use the \textit{Live Streaming} console and stream to \textbf{COM10}
|
||||
\item Start the \textit{Helmholtz Control.exe} software and navigate to e.g. \textit{Menu -> Ambient Filed Calibration}
|
||||
Start the \textit{fgm3dcadapter.py} script that forwards the data live stream from \textit{COM10} on the Sensys app to \textbf{COM11} of the Helmholtz application.
|
||||
%\item Start the data live stream in the FGM3D software by first starting the data acquisition and then live streaming function. Check if the console of the applocation says "Magnetometer State: active"
|
||||
\item The test bench should be ready to run e.g. the Magnetometer Calibrtation script.
|
||||
\end{itemize}
|
||||
|
||||
|
||||
The calibration method used (Zikmund \cite{ref:calibration_procedure_magnetometer_helmholtz_cage}) relies on a simplified error model and the Helmholtz test bench.
|
||||
After either measuring the local geomagnetic field or cancelling it, the magnetometer-under-test runs through a sequence of magnetic fields generated by the Helmholtz coils, which supplies sufficient data to solve a system of equations containing the coefficients of interest.
|
||||
The non-linear system is constructed with the equation below on a per-axis basis, with one row for every sample.
|
||||
|
||||
\begin{equation*}
|
||||
B_{meas} = S \left( B_E \sin{\alpha_E} + B_x \cos{\alpha} \cos{\beta} + B_y \cos{\alpha} \sin{\beta} + B_z \sin{\alpha} \right)
|
||||
\end{equation*}
|
||||
|
||||
The calibration procedure makes use of nearly equidistantly distributed vectors in all directions as test points.
|
||||
The number of these points, as well as the settle time before taking a measurement, can be set in the \gls{ui}.
|
||||
Generally, a high number of points, such as larger than 8, is recommended to achieve both an accurate result and also to get useful residual data.
|
||||
The meaning of the individual calibration coefficients can be derived from Zikmund \cite{ref:calibration_procedure_magnetometer_helmholtz_cage} or also the thesis accompanying the Helmholtz test bench \cite{ref:leons_test_bench}.
|
||||
|
||||
After completing, the results of the calibration procedure will be saved into the data fields on the right hand side of Figure \ref{fig:magcalibrationpure}.
|
||||
This also enables the save buttons underneath.
|
||||
The ``Copy to clipboard'' will put the results table as shown in the \gls{ui} into the system clipboard, from which it can be pasted into software such as Microsoft Excel and LibreOffice Calc.
|
||||
If semi-raw experiment data is required to verify the program functioning or to apply custom algorithms, it can be exported with the ``Export raw CSV'' button.
|
||||
|
||||
\subsubsection*{Data Logging Configuration Page}\label{sec:logging_guide}
|
||||
The application has the ability to log test bench data to a \gls{csv} file.
|
||||
The data is temporarily stored internally and must be saved to an external file by user request.
|
||||
The logging output is highly customizable using the options shown in Figure \ref{fig:loggingpure}.
|
||||
|
||||
An example of a log file is given in Appendix \ref{app:example_files}.
|
||||
Unless explicitly disabled, the first three columns are time stamps: date, system time and time since the start of logging in seconds.
|
||||
All dynamic values from the status display as well as the magnetometer status can be logged, see Table \ref{tab:status_contents} for explanations.
|
||||
Each type of data is logged for all three axes by default, but this can also be specified.
|
||||
The units for numerical values are Volt, Ampere and Tesla.
|
||||
|
||||
There are two options as for when and how often data is logged.
|
||||
The first option logs a row of data in a regular time interval specified by the user.
|
||||
The second option logs, whenever a significant command is sent to the test bench, for example when a new field vector is commanded.
|
||||
Both options can be used simultaneously.
|
||||
|
||||
The logging configuration \gls{ui} is shown in Figure \ref{fig:loggingpure}.
|
||||
Its elements are listed below.
|
||||
|
||||
\begin{figure}[h]
|
||||
\centering
|
||||
\includegraphics[width=0.6\linewidth]{media/logging_mode_pure}
|
||||
\caption{Data logging configuration page}
|
||||
\label{fig:loggingpure}
|
||||
\end{figure}
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{"Start Logging" button:} Starts the logging of data, as configured in the other elements
|
||||
\item \textbf{"Stop Logging" button:} Stops the logging of data
|
||||
\item \textbf{"Write data to file" button:} Lets user save the logged data to a file
|
||||
\begin{itemize}
|
||||
\item Opens dialogue to let user select a file path (chosen file name must be *.csv)
|
||||
\item Saves logged data to the selected file
|
||||
\end{itemize}
|
||||
\item \textbf{"Clear logged data" button:} Deletes all data logged internally by the program, user will be asked if data should be saved to a file first
|
||||
\item \textbf{"Datapoints logged" counter:} Displays the current number of logged data rows
|
||||
\item \textbf{"Log in regular intervals" controls:} Enable checkbox to periodically log data, set interval (in seconds) in the entry field to the right
|
||||
\item \textbf{"Log whenever test bench is commanded" checkbox:} Enable, to log data on significant changes to the test bench (e.g. a new field vector is commanded)
|
||||
\item \textbf{Data selection checkboxes:} Select what data to log, explanations for most options are given in Table \ref{tab:status_contents}.
|
||||
In addition, the Log X/Y/Z-Axis Data checkboxes specify whether the selected logging variables will be included for the respective axis.
|
||||
\end{itemize}
|
||||
\textbf{To collect and save log data:}
|
||||
\begin{enumerate}
|
||||
\item Select when to log data (both options can be used simultaneously)
|
||||
\begin{itemize}
|
||||
\item For regular logging, enable "Log in regular intervals" checkbox and set desired interval
|
||||
\item For logging on test bench command, enable second checkbox
|
||||
\end{itemize}
|
||||
\item Select what data to log, using the lower checkboxes
|
||||
\item Press "Start Logging" button
|
||||
\item When finished, press "Stop Logging" button
|
||||
\item Press "Write data to file" button
|
||||
\item Choose file location and name (must be *.csv, e.g. my\_log.csv), and press save
|
||||
\item Press "Clear logged data" button (optional)
|
||||
\end{enumerate}
|
||||
\newpage
|
||||
\subsubsection*{Settings Page}\label{sec:settings_guide}
|
||||
This is the main page for configuring the program. The settings can be stored to and loaded from *.ini configuration files.\\
|
||||
The different program constants are set through a series of entry fields. All imputs must be numerical values (decimal points). Safe limits for each value are set inside the program. The constants and limits are listed in Table \ref{tab:settings_entries}. If a value exceeding those boundaries is entered, a warning message is displayed and the respective entry field marked in red. \textbf{It is not recommended to ignore these warnings, as incorrect settings can damage equipment on the test bench!}
|
||||
|
||||
\begin{figure}[h]
|
||||
\centering
|
||||
\includegraphics[width=0.8\linewidth]{media/config_mode_pure}
|
||||
\caption{Program settings page}
|
||||
\label{fig:settingspure}
|
||||
\end{figure}
|
||||
Figure \ref{fig:settingspure} shows a screenshot of the \gls{ui} layout with the default settings. The main elements are listed below.
|
||||
\begin{itemize}
|
||||
\item \textbf{"Load config file..." button:} Imports an existing configuration file
|
||||
\begin{itemize}
|
||||
\item Opens file dialogue for selecting configuration file
|
||||
\item Loads settings from selected file
|
||||
\item Checks if any settings exceed safe limits and, if so, displays warning messages
|
||||
\item Reinitializes test bench devices with new settings
|
||||
\end{itemize}
|
||||
\item \textbf{"Save current config" button:} Writes settings to the currently selected file and reinitializes test bench devices with new settings
|
||||
\item \textbf{"Save current config as..." button:} Writes settings to a new file
|
||||
\begin{itemize}
|
||||
\item Opens dialogue to let user choose new file path and name (must be *.ini)
|
||||
\item Reinitializes test bench devices with current settings
|
||||
\end{itemize}
|
||||
\item \textbf{"Serial Port" entries:} Input \gls{com} ports for both \gls{psu}s and the switch box here
|
||||
\begin{itemize}
|
||||
\item Use Windows device manager to find correct port names (connect devices separately to differentiate between devices)
|
||||
\item Test bench X- and Y-axes need to be connected to channel 1 and 2 of one \gls{psu}, Z-axis to channel 1 of the other
|
||||
\end{itemize}
|
||||
\item \textbf{Program constant entry fields:} Set constants here, details are listed in Table \ref{tab:settings_entries}
|
||||
\item \textbf{"Update and Reinitialize" button:} Implements any changed settings in the program and on test bench devices, needs to be pressed for changes to take effect
|
||||
\item \textbf{"Restore Defaults" button:} Restores default settings
|
||||
\end{itemize}
|
||||
|
||||
\begin{table}[ht]
|
||||
\small
|
||||
\caption{Settable program constants}
|
||||
\begin{tabular}{llp{10.2cm}}
|
||||
\hline
|
||||
\textbf{Entry} & \textbf{Limits} & \textbf{Explanation}\\ \hline
|
||||
\rule{0pt}{12pt} Coil Constants: & 0 to \SI{50}{\micro\tesla\per\ampere} & Magnetic field generated per applied current \vspace{-1.5mm}
|
||||
\begin{itemize}
|
||||
\setlength\itemsep{-0.2em}
|
||||
\item Used to calculate current needed to achieve desired field
|
||||
\item Must be measured and tuned before test campaigns
|
||||
\end{itemize}\\[-4pt]
|
||||
Ambient Field: & -200 to \SI{200}{\micro\tesla} & Background magnetic field in the measurement area \vspace{-1.5mm}
|
||||
\begin{itemize}
|
||||
\setlength\itemsep{-0.2em}
|
||||
\item Subtracted from desired field to compensate ambient field
|
||||
\item Must be measured and tuned before test campaigns
|
||||
\end{itemize}\\[-4pt]
|
||||
Resistances: & 1 to \SI{5}{\ohm} & Electrical resistance of each axis, measure from \gls{psu} connectors \\[8pt]
|
||||
Max. Current: & 0 to 6 \si{\ampere} & Current limit for each axis \vspace{-1.5mm}
|
||||
\begin{itemize}
|
||||
\setlength\itemsep{-0.2em}
|
||||
\item Program will block commanding of higher values on \gls{psu}
|
||||
\item Maximum safe permanent current: $I_{max}=\SI{5.5}{\ampere}$ \cite{Blessing_TestBench}
|
||||
\end{itemize}\\[-4pt]
|
||||
Max. Voltage: & 0 to \SI{16}{\volt} & Voltage limit for each axis \vspace{-1.5mm}
|
||||
\begin{itemize}
|
||||
\setlength\itemsep{-0.2em}
|
||||
\item Program will block commanding of higher values on \gls{psu}
|
||||
\item Maximum safe voltage, limited by diodes inside switch box: $U_{max}=\SI{16}{\volt}$
|
||||
\end{itemize}\\[-4pt]
|
||||
Arduino Pins: & 15,16,17 & Output pins on switch box Arduino for inverting polarity on each axis (hard wired inside switch box, should not need to be changed) \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\label{tab:settings_entries}
|
||||
\end{table}
|
||||
|
||||
\clearpage
|
||||
\subsection{Hardware Connections and Program Setup}\label{sec:software_init}
|
||||
\begin{enumerate}
|
||||
\item Connect hardware
|
||||
\begin{enumerate}
|
||||
\item Connect switch box to test bench main cable bundle (connector on the back)
|
||||
\item Connect switch box to \gls{psu}s: \textbf{X-axis to \gls{psu} 1 channel 1, Y-axis to \gls{psu} 1 channel 2, Z-axis to \gls{psu} 2 channel 1}
|
||||
\item Connect switch box power supply (\SI{12}{\volt} DC)
|
||||
\item Connect switch box \gls{usb} port to \gls{pc}
|
||||
\item Connect \gls{psu} \gls{usb} ports to \gls{pc}
|
||||
\end{enumerate}
|
||||
\item Configure interfaces to hardware
|
||||
\begin{enumerate}
|
||||
\item Start program (run "Helmholtz Cage Control.exe")
|
||||
\item Go to settings page (Menu$\rightarrow$Settings...)
|
||||
\end{enumerate}
|
||||
\begin{itemize}
|
||||
\item If program has not been configured before:
|
||||
\begin{enumerate}
|
||||
\setcounter{enumii}{2}
|
||||
\item Use Windows device manager to find correct serial \gls{com} ports for \gls{psu}s and Arduino (connect /disconnect in turn to differentiate between devices)
|
||||
\item Enter \gls{com} port names in application (switch box should be found automatically)
|
||||
\item Press "Update and Reinitialize" button
|
||||
\end{enumerate}
|
||||
\item If a previous configuration exists:
|
||||
\begin{enumerate}
|
||||
\setcounter{enumii}{2}
|
||||
\item Press "Load config file..." button
|
||||
\item Select configuration file (e.g. "myconfig.ini") and open
|
||||
\item Check that the settings were loaded correctly and no values violate the safe limits (fields highlighted in red)
|
||||
\end{enumerate}
|
||||
\end{itemize}
|
||||
\begin{enumerate}
|
||||
\setcounter{enumii}{5}
|
||||
\item Check console print to see if all devices were found, otherwise check physical connections and \gls{com} port settings
|
||||
\end{enumerate}
|
||||
\item If using a magnetometer, the listening port is open and the adapter script can now be started
|
||||
\item Test configuration
|
||||
\begin{enumerate}
|
||||
\item Go to manual mode (Menu$\rightarrow$Static Manual Input)
|
||||
\item Switch input mode to "Current" (see Section \ref{sec:manual_mode_guide})
|
||||
\item Set different currents and check device response:
|
||||
\begin{itemize}
|
||||
\item Current should be activated on correct \gls{psu} channel
|
||||
\item For negative currents, corresponding status \gls{led} on switch box should light up and relay actuation be audible as clicking sound
|
||||
\end{itemize}
|
||||
\item If using a magnetometer: Check the device output in either of the calibration-views
|
||||
\end{enumerate}
|
||||
\item Go back to the settings page (Menu$\rightarrow$Settings...)
|
||||
\item Change program constants as needed (e.g. enter measured ambient field), see Section \ref{sec:settings_guide}
|
||||
\item Save configuration:
|
||||
\begin{itemize}
|
||||
\item Press "Save current config" button to update the current configuration file
|
||||
\item Press "Save current config as..." button to save to a new configuration file, set new file name in file dialogue (e.g. "myconfig.ini")
|
||||
\end{itemize}
|
||||
\end{enumerate}
|
||||
|
||||
\subsection{TCP Remote Control}
|
||||
\label{sec:tcp_api}
|
||||
|
||||
The Helmholtz Control Software offers a TCP automation interface that is opened upon application start-up.
|
||||
To find out which port is being listened on, look for the corresponding information in the application console, but typically, it will be port 6677.
|
||||
The commands that are accepted by the TCP interface are documented in Table \ref{tab:tcp_comamnds} and also in the \code{socket\_control.py} source file.
|
||||
All field commands will implicitly preform the usual safety checks to ensure safe operation.
|
||||
The commands that are shown must all be terminated with a single {\textbackslash}n (newline) char.
|
||||
Commands may be split across multiple packets if desired.
|
||||
Important Note: Before useful commands can be sent, \code{declare\_api\_version} must be called.
|
||||
|
||||
The \code{tools} folder in the application git repository contains an example implementation (\code{fgm3d\_adapter.py}) of a magnetometer interface using the TCP socket.
|
||||
|
||||
%\small
|
||||
\begin{longtable}{lp{8.5cm}}
|
||||
\caption{TCP Remote Control Commands}\\
|
||||
\hline
|
||||
\textbf{Command} & \textbf{Description}\\ \hline
|
||||
\code{set\_raw\_field} [X] [Y] [Z] & Returns: 0 or 1 for success.
|
||||
Accepts decimal point formatted floats, with or without scientific notation. The float() cast must understand it.
|
||||
The field units are Tesla.
|
||||
This causes an additional field of the given strength to be generated, without regard for the pre-existing geomagnetic/external fields. \\ \hline
|
||||
|
||||
\code{set\_compensated\_field} [X] [Y] [Z] & Returns: 0 or 1 for success.
|
||||
Accepts decimal point formatted floats, with or without scientific notation. The float() cast must understand it.
|
||||
The field units are Tesla.
|
||||
This causes a field of exactly the given magnitude to be generated by compensating external factors such as the
|
||||
geomagnetic field. \\ \hline
|
||||
|
||||
\code{set\_coil\_currents} [X] [Y] [Z] & Returns: 0 or 1 for success.
|
||||
Accepts decimal point formatted floats, with or without scientific notation. The float() cast must understand it.
|
||||
The field units are Ampere.
|
||||
This establishes the requested current in the individual coils. \\ \hline
|
||||
|
||||
\code{magnetometer\_field} [X] [Y] [Z] & Returns: 1.
|
||||
Accepts decimal point formatted floats, with or without scientific notation. The float() cast must understand it.
|
||||
The field units are Tesla.
|
||||
Sets the state of a virtual magnetometer object which mirrors a physical sensor by means of
|
||||
this command. \\ \hline
|
||||
|
||||
\code{get\_api\_version} & Returns: a string uniquely identifying each API version.
|
||||
This function can be called before \code{declare\_api\_version}. \\ \hline
|
||||
|
||||
\code{declare\_api\_version} [version] & Returns: 0 or 1.
|
||||
Declare the API version the client application was programmed for.
|
||||
It must be compatible with the current API version. This prevents unexpected behavior by forcing programmers to specify which API they are expecting.
|
||||
This function must be called before sending HW commands. \\ \hline
|
||||
\label{tab:tcp_comamnds}
|
||||
\end{longtable}
|
||||
%\normalsize
|
||||
@@ -0,0 +1,191 @@
|
||||
% Template for B.Sc, M.Sc. thesis, etc.
|
||||
% Originally created by Michael Lauer and Dagmar Bock
|
||||
% Updated by Roland A. Gabrielli
|
||||
% Translated and updated by Adam S. Pagan
|
||||
%
|
||||
% Font size 11pt, paper size DIN A4, Book, additional 0.5cm for binding on the left
|
||||
% pdf created via dvi and ps
|
||||
|
||||
\documentclass[a4paper,DIV14,11pt,headsepline,oneside,openany,listof=totoc,BCOR5mm]{scrbook}
|
||||
|
||||
% Page formatting
|
||||
\usepackage{scrlayer-scrpage}
|
||||
\pagestyle{scrheadings}
|
||||
\ofoot[\pagemark]{\pagemark}
|
||||
\cfoot[]{}
|
||||
\ohead{\textsc{\headmark}}
|
||||
\chead{}
|
||||
\ihead{}
|
||||
|
||||
% Special packages
|
||||
\usepackage[dvipsnames]{xcolor}
|
||||
\usepackage{graphicx}
|
||||
\usepackage[dvips]{rotating}
|
||||
\usepackage{floatflt,wrapfig}
|
||||
\usepackage{verbatim}
|
||||
\usepackage{isotope}
|
||||
\usepackage[absolute]{textpos}
|
||||
\usepackage{siunitx}
|
||||
\sisetup{
|
||||
per-mode=fraction,
|
||||
fraction-function=\tfrac
|
||||
}
|
||||
\usepackage{pythonhighlight}
|
||||
\usepackage{listings}
|
||||
\usepackage{todonotes}
|
||||
\usepackage{placeins}
|
||||
\usepackage{footnote}
|
||||
|
||||
% AMS-LaTeX packages:
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{amsthm}
|
||||
|
||||
% Improved pdf presentation
|
||||
\usepackage{ae}
|
||||
\usepackage{pdfpages}
|
||||
|
||||
% Additional table commands
|
||||
\usepackage{array}
|
||||
\usepackage{dcolumn}
|
||||
\usepackage{longtable}
|
||||
\usepackage{tabularx}
|
||||
\usepackage{ltxtable}
|
||||
\usepackage{longtable}
|
||||
\usepackage{supertabular}
|
||||
\usepackage{subcaption}
|
||||
\usepackage{booktabs}
|
||||
\usepackage{multicol}
|
||||
\usepackage{multirow}
|
||||
\usepackage{threeparttable}
|
||||
|
||||
% Additional symbols
|
||||
\usepackage{wasysym}
|
||||
\usepackage{textcomp}
|
||||
\usepackage{pifont}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{eurosym}
|
||||
\chardef\_=`_ % enable use of underscores in text with \_
|
||||
|
||||
% Text commands
|
||||
\newcommand{\code}[1]{\texttt{#1}} % command for inline code (changes font)
|
||||
\newcommand{\myparagraph}[1]{\paragraph{#1}\mbox{}\\} % create paragraph where text starts in new line
|
||||
|
||||
% Positioning of tables and figures after first mention
|
||||
\usepackage{flafter}
|
||||
|
||||
% DOI and URL
|
||||
\usepackage{url}
|
||||
%\usepackage{doi}
|
||||
|
||||
% Referencing
|
||||
\usepackage[backend=biber,style=iso-numeric]{biblatex}
|
||||
\addbibresource{References.bib}
|
||||
\DeclareFieldFormat{labelnumberwidth}{\mkbibbrackets{#1}}
|
||||
\usepackage[hidelinks]{hyperref}
|
||||
\usepackage[nopostdot]{glossaries}
|
||||
\makeglossaries
|
||||
\setacronymstyle{long-short}
|
||||
\loadglsentries{Abbreviation}
|
||||
\usepackage{enumitem}
|
||||
|
||||
% Sets automatic graphics path
|
||||
\graphicspath{{media}}
|
||||
|
||||
% Maximum size of figures
|
||||
\renewcommand{\floatpagefraction}{.8}
|
||||
\renewcommand{\topfraction}{.85}
|
||||
|
||||
% Define thicker table line
|
||||
\makeatletter
|
||||
\def\thickhline{%
|
||||
\noalign{\ifnum0=`}\fi\hrule \@height \thickarrayrulewidth \futurelet
|
||||
\reserved@a\@xthickhline}
|
||||
\def\@xthickhline{\ifx\reserved@a\thickhline
|
||||
\vskip\doublerulesep
|
||||
\vskip-\thickarrayrulewidth
|
||||
\fi
|
||||
\ifnum0=`{\fi}}
|
||||
\makeatother
|
||||
|
||||
% place floats on top of the pages where there are only floats
|
||||
\makeatletter
|
||||
\setlength{\@fptop}{10pt}
|
||||
\makeatother
|
||||
|
||||
\makeatletter
|
||||
\g@addto@macro{\UrlBreaks}{\do\2\do\1}
|
||||
\makeatother
|
||||
|
||||
\newlength{\thickarrayrulewidth}
|
||||
\setlength{\thickarrayrulewidth}{3\arrayrulewidth}
|
||||
|
||||
% prepare subfigure of same height (https://tex.stackexchange.com/questions/218378/forcing-subfigures-to-have-same-height-and-take-overall-x-of-linewidth-in-latex)
|
||||
\newlength{\twosubht}
|
||||
\newsavebox{\twosubbox}
|
||||
|
||||
%
|
||||
\setlength{\parindent}{0mm}
|
||||
%\setlength{\unitlength}{1mm}
|
||||
|
||||
\sloppy
|
||||
|
||||
% ********************************************************************
|
||||
% Begin of document
|
||||
|
||||
\begin{document}
|
||||
|
||||
% Title page
|
||||
\include{Preamble/TitlePage}
|
||||
|
||||
\frontmatter
|
||||
|
||||
% Subject, preface, abstract, acknowledgements, etc.
|
||||
% \includepdf{SubjectOfThesis\_Signed.pdf}
|
||||
|
||||
\pagestyle{scrplain}
|
||||
|
||||
% Table of contents
|
||||
\tableofcontents
|
||||
%\addcontentsline{toc}{chapter}{Contents} % Manually adds self-reference of contents
|
||||
|
||||
% List of figures and list of tables
|
||||
\listoffigures
|
||||
\listoftables
|
||||
%\begin{small}
|
||||
% List of symbols
|
||||
\include{Preamble/ListOfSymbols}
|
||||
%\end{small}
|
||||
\setlist[description]{leftmargin=!, labelwidth=5em} % Change for glossaries
|
||||
\printglossary[nonumberlist]
|
||||
\setlist[description]{style=standard} % reset settings back to default
|
||||
|
||||
% Main body of work (starting with the introduction)
|
||||
\mainmatter
|
||||
\pagestyle{scrheadings}
|
||||
% acronyms that should not be expanded at first mention:
|
||||
\glsunset{pla}
|
||||
\glsunset{usb}
|
||||
\glsunset{pc}
|
||||
\glsunset{tbd}
|
||||
\glsunset{com}
|
||||
\glsunset{api}
|
||||
|
||||
\include{Chapters/03_Software}
|
||||
\include{Chapters/04_Users Guide}
|
||||
|
||||
% References
|
||||
%\bibliographystyle{acm}
|
||||
%\bibliography{References}
|
||||
\addcontentsline{toc}{chapter}{Bibliography} % Manually adds bibliography to contents
|
||||
\printbibliography
|
||||
|
||||
% Appendix
|
||||
\newpage
|
||||
\appendix
|
||||
\include{Appendix/AppendixA}
|
||||
\end{document}
|
||||
|
||||
% End of document
|
||||
% ********************************************************************
|
||||
@@ -0,0 +1,15 @@
|
||||
\chapter{List of Symbols}
|
||||
|
||||
\begin{tabular}[t]{p{1.5cm}lc}
|
||||
\textbf{Symbol} & \textbf{Description} & \textbf{Unit} \\[0.6em]
|
||||
$B$ & Magnetic flux density & \si{\tesla} \\[0.6em]
|
||||
$B_0$ & Ambient magnetic flux density & \si{\tesla} \\[0.6em]
|
||||
$B_{max}$ & Maximum achievable magnetic flux density & \si{\tesla} \\[0.6em]
|
||||
$B_{min}$ & Minimum achievable magnetic flux density & \si{\tesla} \\[0.6em]
|
||||
$I$ & Current & \si{\ampere} \\[0.6em]
|
||||
$I_{max}$ & Maximum current & \si{\ampere} \\[0.6em]
|
||||
$K$ & Coil constant & \si{\tesla\per\ampere} \\[0.6em]
|
||||
$R$ & Electrical resistance & \si{\ohm} \\[0.6em]
|
||||
$U$ & Voltage & \si{\volt} \\[0.6em]
|
||||
$U_{max}$ & Maximum voltage & \si{\volt} \\[0.6em]
|
||||
\end{tabular}
|
||||
@@ -0,0 +1,30 @@
|
||||
\mbox{}
|
||||
\thispagestyle{empty}
|
||||
\begin{textblock*}{124mm}(51mm,47mm)
|
||||
\begin{center}
|
||||
\begin{Large}
|
||||
\textbf{Helmholtz Test Bench Control Software Documentation}\\[5mm]
|
||||
\end{Large}
|
||||
\end{center}
|
||||
\end{textblock*}
|
||||
|
||||
|
||||
|
||||
\begin{textblock*}{124mm}(51mm,180mm)
|
||||
\begin{center}
|
||||
Author:\\
|
||||
Martin Zietz\\
|
||||
Leon Teichröb\\
|
||||
|
||||
Supervisors:\\
|
||||
M.Sc. Markus T. Koller\\
|
||||
M.Sc. Lukas-Maximilian Loidold\\
|
||||
\end{center}
|
||||
\end{textblock*}
|
||||
|
||||
\begin{textblock*}{124mm}(51mm,250mm)
|
||||
\begin{center}
|
||||
Institut f{\"u}r Raumfahrtsysteme, Universit{\"a}t Stuttgart\\
|
||||
März 2021
|
||||
\end{center}
|
||||
\end{textblock*}
|
||||
@@ -0,0 +1,11 @@
|
||||
Using the modified AIAA style:
|
||||
|
||||
1. Copy modified file aiaa.bst into path 'C:\Program Files\MiKTeX 2.9\bibtex\bst\aiaa' (or equivalent) and overwrite original file (be sure to createe a backup first).
|
||||
|
||||
2. Implement the packages 'url' and 'doi' into the master file of your thesis:
|
||||
\usepackage{url}
|
||||
\usepackage{doi}.
|
||||
|
||||
3. Modify your bibliography (.bib) file in the following manner:
|
||||
Instead of 'url = {http://www.url.de}', use 'url = {\url{http://www.url.de}}' to create an active hyperlink in the resulting PDF file. Proceed in the same manner for DOI links, e.g.
|
||||
'doi={\doi{10.2514/6.1997-3074}}' instead of 'doi={10.2514/6.1997-3074}'.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
% Instead of 'url = {http://www.url.de}', use 'url = {\url{http://www.url.de}}' to create an active hyperlink in the resulting PDF file.
|
||||
% Same manner for DOI links, e.g. 'doi={\doi{10.2514/6.1997-3074}}' instead of 'doi={10.2514/6.1997-3074}'.
|
||||
|
||||
@thesis{Blessing_TestBench,
|
||||
author = {Steffen Blessing},
|
||||
title = {Design of a CubeSat Magnetic Field Cage for the Verification of CubeSats Attitude Control Systems},
|
||||
date = {2020},
|
||||
institution = {Institute for Space Systems (IRS), University of Stuttgart},
|
||||
location = {Stuttgart},
|
||||
}
|
||||
|
||||
@online{PS2000B_lib,
|
||||
author = {Sören Sprößig},
|
||||
title = {Python PS2000B Library},
|
||||
url = {https://github.com/ssproessig/Python-PS2000B},
|
||||
urldate = {2020-11-18},
|
||||
}
|
||||
|
||||
@online{Arduino_lib,
|
||||
title = {Arduino Python 3 Command API},
|
||||
url = {https://github.com/mkals/Arduino-Python3-Command-API},
|
||||
urldate = {2020-12-09},
|
||||
}
|
||||
|
||||
@online{auto_py_to_exe,
|
||||
title = {Auto Py to Exe},
|
||||
author = {Brent Vollebregt},
|
||||
url = {https://github.com/brentvollebregt/auto-py-to-exe},
|
||||
urldate = {2020-03-02},
|
||||
}
|
||||
|
||||
@online{Tkinter_tutorial,
|
||||
author = {Harrison Kinsley},
|
||||
title = {Object Oriented Programming Crash Course with Tkinter},
|
||||
url = {https://pythonprogramming.net/object-oriented-programming-crash-course-tkinter/},
|
||||
urldate = {2021-01-19},
|
||||
}
|
||||
|
||||
@INPROCEEDINGS{eive_paper,
|
||||
author={B. {Schoch} and S. {Chartier} and U. {Mohr} and M. {Koller} and S. {Klinkner} and I. {Kallfass}},
|
||||
booktitle={2020 IEEE Space Hardware and Radio Conference (SHaRC)},
|
||||
title={Towards a CubeSat Mission for a Wideband Data Transmission in E-Band},
|
||||
year={2020},
|
||||
volume={},
|
||||
number={},
|
||||
pages={16-19},
|
||||
doi={10.1109/SHaRC47220.2020.9034007}
|
||||
}
|
||||
|
||||
@report{em_präsi_ital,
|
||||
author = {Lorenzo Lunini},
|
||||
title = {Future Satellite TLC systems: The challenge of using very high frequency bands},
|
||||
type = {5th International Multi-Topic ICT Conference},
|
||||
institution = {Politecnico die Milano},
|
||||
date = {2018-04-25},
|
||||
url = {http://luini.deib.polimi.it/docs/Pubblicazioni/AtmosphericChannelModeling_IMTIC2018.pdf},
|
||||
pages = {7},
|
||||
}
|
||||
|
||||
@online{esa_em_bands,
|
||||
title = {Satellite frequency bands},
|
||||
url = {https://www.esa.int/Applications/Telecommunications_Integrated_Applications/Satellite_frequency_bands},
|
||||
organization = {European Space Agency},
|
||||
urldate = {2021-02-27},
|
||||
}
|
||||
|
||||
@report{wegertseder_steels,
|
||||
title = {Werkstoffe - Edelstähle},
|
||||
institution = {Wegertseder GmbH},
|
||||
url = {https://www.wegertseder.com/download/techdat/pdf/420-Zusammensetzung-Edelstahl.pdf},
|
||||
urldate = {2020-11-13},
|
||||
}
|
||||
|
||||
@report{austenitic_steel,
|
||||
title = {Austenitischer Stahl},
|
||||
institution = {BorTec SMT GmbH},
|
||||
url = {https://www.edelstahl-haerten.de/werkstoffe/austenitischer-stahl/},
|
||||
urldate = {2020-11-13},
|
||||
}
|
||||
|
||||
@report{cds_6u,
|
||||
title = {6U CubeSat Design Specification},
|
||||
date = {2018-06-07},
|
||||
version = {Revision 1.0},
|
||||
institution = {California Polytechnic State University},
|
||||
url = {https://static1.squarespace.com/static/5418c831e4b0fa4ecac1bacd/t/5b75dfcd70a6adbee5908fd9/1534451664215/6U_CDS_2018-06-07_rev_1.0.pdf},
|
||||
urldate = {2020-03-03},
|
||||
}
|
||||
|
||||
@report{cds,
|
||||
title = {CubeSat Design Specification},
|
||||
date = {2014-02-20},
|
||||
version = {Revision 13},
|
||||
institution = {California Polytechnic State University},
|
||||
url = {https://blogs.esa.int/philab/files/2019/11/RD-02_CubeSat_Design_Specification_Rev._13_The.pdf},
|
||||
urldate = {2020-03-03},
|
||||
}
|
||||
|
||||
@report{ps2000b_datasheet,
|
||||
title = {EA-PS 2000B Power Supply Unit},
|
||||
date = {2020},
|
||||
institution = {Elektro-Automatik GmbH},
|
||||
url = {https://elektroautomatik.com/shop/media/pdf/7e/58/df/datasheet_ps2000b_tft.pdf},
|
||||
urldate = {2020-11-27},
|
||||
}
|
||||
|
||||
@book{lin_regression,
|
||||
doi = {10.1007/978-3-658-19732-2},
|
||||
url = {https://doi.org/10.1007%2F978-3-658-19732-2},
|
||||
year = 2018,
|
||||
publisher = {Springer Fachmedien Wiesbaden},
|
||||
author = {Irasianty Frost},
|
||||
title = {Einfache lineare Regression},
|
||||
pages = {9},
|
||||
}
|
||||
|
||||
@report{lis3mdl_datasheet,
|
||||
title = {LIS3MDL Digital Output Magnetic Sensor},
|
||||
date = {2017},
|
||||
institution = {ST Microelectronics},
|
||||
url = {https://www.st.com/resource/en/datasheet/lis3mdl.pdf},
|
||||
urldate = {2020-03-06},
|
||||
}
|
||||
|
||||
@book{Fasoulas_rs,
|
||||
doi = {10.1007/978-3-662-49638-1},
|
||||
url = {https://doi.org/10.1007%2F978-3-662-49638-1},
|
||||
year = 2017,
|
||||
publisher = {Springer Berlin Heidelberg},
|
||||
author = {Ernst Messerschmid and Stefanos Fasoulas},
|
||||
title = {Raumfahrtsysteme}
|
||||
}
|
||||
|
||||
@report{sensys_software,
|
||||
title = {Manual Digitizer FGM3D TD},
|
||||
date = {2020},
|
||||
institution = {SENSYS GmbH},
|
||||
version = {Version 1.1},
|
||||
}
|
||||
|
||||
@report{relay,
|
||||
title = {Serie 55 - Industrie-Relais 7 - 10 A},
|
||||
date = {2018},
|
||||
institution = {Finder S.p.A.},
|
||||
url = {https://finder-relais.net/de/finder-relais-serie-55.pdf},
|
||||
urldate = {2020-12-13},
|
||||
}
|
||||
|
||||
@report{arduino_uno,
|
||||
title = {Arduino Uno Rev3 SMD},
|
||||
institution = {Arduino AG},
|
||||
url = {https://store.arduino.cc/arduino-uno-rev3-smd},
|
||||
urldate = {2021-03-02},
|
||||
}
|
||||
|
||||
@report{phyphox,
|
||||
title = {Phyphox Application},
|
||||
institution = {2nd Institute of Physics, RWTH Aachen},
|
||||
url = {https://phyphox.org/de/download-de/},
|
||||
urldate = {2021-02-20},
|
||||
}
|
||||
|
||||
@report{fgm3d_mgm,
|
||||
title = {FGM3D three-axis Fluxgate Magnetometer},
|
||||
institution = {SENSYS GmbH},
|
||||
url = {https://sensysmagnetometer.com/products/sensors-recorder/fluxgates/fgm3d-magnetometer/},
|
||||
urldate = {2021-03-03},
|
||||
}
|
||||
|
||||
@article{source,
|
||||
title={Combination of Interdisciplinary Training in Space Technology with Project-Related Work through the CubeSat SOURCE},
|
||||
author={Stier, Annika and Schweigert, Robin and Galla, Daniel and Lengowski, Michael and Klinkner, Sabine},
|
||||
year={2020},
|
||||
publisher={University of Leicester}
|
||||
}
|
||||
|
||||
@inproceedings{ref:calibration_procedure_magnetometer_helmholtz_cage,
|
||||
author = {Zikmund, A. and Janosek, Michal},
|
||||
year = {2014},
|
||||
month = {05},
|
||||
pages = {473-476},
|
||||
title = {Calibration procedure for triaxial magnetometers without a compensating system or moving parts},
|
||||
isbn = {9781467363860},
|
||||
journal = {Conference Record - IEEE Instrumentation and Measurement Technology Conference},
|
||||
doi = {10.1109/I2MTC.2014.6860790}
|
||||
}
|
||||
|
||||
@thesis{ref:leons_test_bench,
|
||||
author = {Leon Teichröb},
|
||||
title = {Mapping and Calibration of a Helmholtz Magnetic Field Cage and Test of the EIVE Attitude Control System},
|
||||
date = {2021},
|
||||
institution = {Institute for Space Systems (IRS), University of Stuttgart},
|
||||
location = {Stuttgart},
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
%%
|
||||
%% This is file `longtable.sty',
|
||||
%% generated with the docstrip utility.
|
||||
%%
|
||||
%% The original source files were:
|
||||
%%
|
||||
%% longtable.dtx (with options: `package')
|
||||
%%
|
||||
%% This is a generated file.
|
||||
%%
|
||||
%% The source is maintained by the LaTeX Project team and bug
|
||||
%% reports for it can be opened at http://latex-project.org/bugs.html
|
||||
%% (but please observe conditions on bug reports sent to that address!)
|
||||
%%
|
||||
%% Copyright 1993-2016
|
||||
%% The LaTeX3 Project and any individual authors listed elsewhere
|
||||
%% in this file.
|
||||
%%
|
||||
%% This file was generated from file(s) of the Standard LaTeX `Tools Bundle'.
|
||||
%% --------------------------------------------------------------------------
|
||||
%%
|
||||
%% It may be distributed and/or modified under the
|
||||
%% conditions of the LaTeX Project Public License, either version 1.3c
|
||||
%% of this license or (at your option) any later version.
|
||||
%% The latest version of this license is in
|
||||
%% http://www.latex-project.org/lppl.txt
|
||||
%% and version 1.3c or later is part of all distributions of LaTeX
|
||||
%% version 2005/12/01 or later.
|
||||
%%
|
||||
%% This file may only be distributed together with a copy of the LaTeX
|
||||
%% `Tools Bundle'. You may however distribute the LaTeX `Tools Bundle'
|
||||
%% without such generated files.
|
||||
%%
|
||||
%% The list of all files belonging to the LaTeX `Tools Bundle' is
|
||||
%% given in the file `manifest.txt'.
|
||||
%%
|
||||
%% File: longtable.dtx Copyright (C) 1990-2001 David Carlisle
|
||||
\NeedsTeXFormat{LaTeX2e}[1995/06/01]
|
||||
\ProvidesPackage{longtable}
|
||||
[2014/10/28 v4.11 Multi-page Table package (DPC)]
|
||||
\def\LT@err{\PackageError{longtable}}
|
||||
\def\LT@warn{\PackageWarning{longtable}}
|
||||
\def\LT@final@warn{%
|
||||
\AtEndDocument{%
|
||||
\LT@warn{Table \@width s have changed. Rerun LaTeX.\@gobbletwo}}%
|
||||
\global\let\LT@final@warn\relax}
|
||||
\DeclareOption{errorshow}{%
|
||||
\def\LT@warn{\PackageInfo{longtable}}}
|
||||
\DeclareOption{pausing}{%
|
||||
\def\LT@warn#1{%
|
||||
\LT@err{#1}{This is not really an error}}}
|
||||
\DeclareOption{set}{}
|
||||
\DeclareOption{final}{}
|
||||
\ProcessOptions
|
||||
\newskip\LTleft \LTleft=\fill
|
||||
\newskip\LTright \LTright=\fill
|
||||
\newskip\LTpre \LTpre=\bigskipamount
|
||||
\newskip\LTpost \LTpost=\bigskipamount
|
||||
\newcount\LTchunksize \LTchunksize=20
|
||||
\let\c@LTchunksize\LTchunksize
|
||||
\newdimen\LTcapwidth \LTcapwidth=4in
|
||||
\newbox\LT@head
|
||||
\newbox\LT@firsthead
|
||||
\newbox\LT@foot
|
||||
\newbox\LT@lastfoot
|
||||
\newcount\LT@cols
|
||||
\newcount\LT@rows
|
||||
\newcounter{LT@tables}
|
||||
\newcounter{LT@chunks}[LT@tables]
|
||||
\ifx\c@table\undefined
|
||||
\newcounter{table}
|
||||
\def\fnum@table{\tablename~\thetable}
|
||||
\fi
|
||||
\ifx\tablename\undefined
|
||||
\def\tablename{Table}
|
||||
\fi
|
||||
\newtoks\LT@p@ftn
|
||||
\mathchardef\LT@end@pen=30000
|
||||
\def\longtable{%
|
||||
\par
|
||||
\ifx\multicols\@undefined
|
||||
\else
|
||||
\ifnum\col@number>\@ne
|
||||
\@twocolumntrue
|
||||
\fi
|
||||
\fi
|
||||
\if@twocolumn
|
||||
\LT@err{longtable not in 1-column mode}\@ehc
|
||||
\fi
|
||||
\begingroup
|
||||
\@ifnextchar[\LT@array{\LT@array[x]}}
|
||||
\def\LT@array[#1]#2{%
|
||||
\refstepcounter{table}\stepcounter{LT@tables}%
|
||||
\if l#1%
|
||||
\LTleft\z@ \LTright\fill
|
||||
\else\if r#1%
|
||||
\LTleft\fill \LTright\z@
|
||||
\else\if c#1%
|
||||
\LTleft\fill \LTright\fill
|
||||
\fi\fi\fi
|
||||
\let\LT@mcol\multicolumn
|
||||
\let\LT@@tabarray\@tabarray
|
||||
\let\LT@@hl\hline
|
||||
\def\@tabarray{%
|
||||
\let\hline\LT@@hl
|
||||
\LT@@tabarray}%
|
||||
\let\\\LT@tabularcr\let\tabularnewline\\%
|
||||
\def\newpage{\noalign{\break}}%
|
||||
\def\pagebreak{\noalign{\ifnum`}=0\fi\@testopt{\LT@no@pgbk-}4}%
|
||||
\def\nopagebreak{\noalign{\ifnum`}=0\fi\@testopt\LT@no@pgbk4}%
|
||||
\let\hline\LT@hline \let\kill\LT@kill\let\caption\LT@caption
|
||||
\@tempdima\ht\strutbox
|
||||
\let\@endpbox\LT@endpbox
|
||||
\ifx\extrarowheight\@undefined
|
||||
\let\@acol\@tabacol
|
||||
\let\@classz\@tabclassz \let\@classiv\@tabclassiv
|
||||
\def\@startpbox{\vtop\LT@startpbox}%
|
||||
\let\@@startpbox\@startpbox
|
||||
\let\@@endpbox\@endpbox
|
||||
\let\LT@LL@FM@cr\@tabularcr
|
||||
\else
|
||||
\advance\@tempdima\extrarowheight
|
||||
\col@sep\tabcolsep
|
||||
\let\@startpbox\LT@startpbox\let\LT@LL@FM@cr\@arraycr
|
||||
\fi
|
||||
\setbox\@arstrutbox\hbox{\vrule
|
||||
\@height \arraystretch \@tempdima
|
||||
\@depth \arraystretch \dp \strutbox
|
||||
\@width \z@}%
|
||||
\let\@sharp##\let\protect\relax
|
||||
\begingroup
|
||||
\@mkpream{#2}%
|
||||
\xdef\LT@bchunk{%
|
||||
\global\advance\c@LT@chunks\@ne
|
||||
\global\LT@rows\z@\setbox\z@\vbox\bgroup
|
||||
\LT@setprevdepth
|
||||
\tabskip\LTleft \noexpand\halign to\hsize\bgroup
|
||||
\tabskip\z@ \@arstrut \@preamble \tabskip\LTright \cr}%
|
||||
\endgroup
|
||||
\expandafter\LT@nofcols\LT@bchunk&\LT@nofcols
|
||||
\LT@make@row
|
||||
\m@th\let\par\@empty
|
||||
\everycr{}\lineskip\z@\baselineskip\z@
|
||||
\LT@bchunk}
|
||||
\def\LT@no@pgbk#1[#2]{\penalty #1\@getpen{#2}\ifnum`{=0\fi}}
|
||||
\def\LT@start{%
|
||||
\let\LT@start\endgraf
|
||||
\endgraf\penalty\z@\vskip\LTpre
|
||||
\dimen@\pagetotal
|
||||
\advance\dimen@ \ht\ifvoid\LT@firsthead\LT@head\else\LT@firsthead\fi
|
||||
\advance\dimen@ \dp\ifvoid\LT@firsthead\LT@head\else\LT@firsthead\fi
|
||||
\advance\dimen@ \ht\LT@foot
|
||||
\dimen@ii\vfuzz
|
||||
\vfuzz\maxdimen
|
||||
\setbox\tw@\copy\z@
|
||||
\setbox\tw@\vsplit\tw@ to \ht\@arstrutbox
|
||||
\setbox\tw@\vbox{\unvbox\tw@}%
|
||||
\vfuzz\dimen@ii
|
||||
\advance\dimen@ \ht
|
||||
\ifdim\ht\@arstrutbox>\ht\tw@\@arstrutbox\else\tw@\fi
|
||||
\advance\dimen@\dp
|
||||
\ifdim\dp\@arstrutbox>\dp\tw@\@arstrutbox\else\tw@\fi
|
||||
\advance\dimen@ -\pagegoal
|
||||
\ifdim \dimen@>\z@\vfil\break\fi
|
||||
\global\@colroom\@colht
|
||||
\ifvoid\LT@foot\else
|
||||
\global\advance\vsize-\ht\LT@foot
|
||||
\global\advance\@colroom-\ht\LT@foot
|
||||
\dimen@\pagegoal\advance\dimen@-\ht\LT@foot\pagegoal\dimen@
|
||||
\maxdepth\z@
|
||||
\fi
|
||||
\ifvoid\LT@firsthead\copy\LT@head\else\box\LT@firsthead\fi\nobreak
|
||||
\output{\LT@output}}
|
||||
\def\endlongtable{%
|
||||
\crcr
|
||||
\noalign{%
|
||||
\let\LT@entry\LT@entry@chop
|
||||
\xdef\LT@save@row{\LT@save@row}}%
|
||||
\LT@echunk
|
||||
\LT@start
|
||||
\unvbox\z@
|
||||
\LT@get@widths
|
||||
\if@filesw
|
||||
{\let\LT@entry\LT@entry@write\immediate\write\@auxout{%
|
||||
\gdef\expandafter\noexpand
|
||||
\csname LT@\romannumeral\c@LT@tables\endcsname
|
||||
{\LT@save@row}}}%
|
||||
\fi
|
||||
\ifx\LT@save@row\LT@@save@row
|
||||
\else
|
||||
\LT@warn{Column \@width s have changed\MessageBreak
|
||||
in table \thetable}%
|
||||
\LT@final@warn
|
||||
\fi
|
||||
\endgraf\penalty -\LT@end@pen
|
||||
\ifvoid\LT@foot\else
|
||||
\global\advance\vsize\ht\LT@foot
|
||||
\global\advance\@colroom\ht\LT@foot
|
||||
\dimen@\pagegoal\advance\dimen@\ht\LT@foot\pagegoal\dimen@
|
||||
\fi
|
||||
\endgroup
|
||||
\global\@mparbottom\z@
|
||||
\endgraf\penalty\z@\addvspace\LTpost
|
||||
\ifvoid\footins\else\insert\footins{}\fi}
|
||||
\def\LT@nofcols#1&{%
|
||||
\futurelet\@let@token\LT@n@fcols}
|
||||
\def\LT@n@fcols{%
|
||||
\advance\LT@cols\@ne
|
||||
\ifx\@let@token\LT@nofcols
|
||||
\expandafter\@gobble
|
||||
\else
|
||||
\expandafter\LT@nofcols
|
||||
\fi}
|
||||
\def\LT@tabularcr{%
|
||||
\relax\iffalse{\fi\ifnum0=`}\fi
|
||||
\@ifstar
|
||||
{\def\crcr{\LT@crcr\noalign{\nobreak}}\let\cr\crcr
|
||||
\LT@t@bularcr}%
|
||||
{\LT@t@bularcr}}
|
||||
\let\LT@crcr\crcr
|
||||
\let\LT@setprevdepth\relax
|
||||
\def\LT@t@bularcr{%
|
||||
\global\advance\LT@rows\@ne
|
||||
\ifnum\LT@rows=\LTchunksize
|
||||
\gdef\LT@setprevdepth{%
|
||||
\prevdepth\z@\global
|
||||
\global\let\LT@setprevdepth\relax}%
|
||||
\expandafter\LT@xtabularcr
|
||||
\else
|
||||
\ifnum0=`{}\fi
|
||||
\expandafter\LT@LL@FM@cr
|
||||
\fi}
|
||||
\def\LT@xtabularcr{%
|
||||
\@ifnextchar[\LT@argtabularcr\LT@ntabularcr}
|
||||
\def\LT@ntabularcr{%
|
||||
\ifnum0=`{}\fi
|
||||
\LT@echunk
|
||||
\LT@start
|
||||
\unvbox\z@
|
||||
\LT@get@widths
|
||||
\LT@bchunk}
|
||||
\def\LT@argtabularcr[#1]{%
|
||||
\ifnum0=`{}\fi
|
||||
\ifdim #1>\z@
|
||||
\unskip\@xargarraycr{#1}%
|
||||
\else
|
||||
\@yargarraycr{#1}%
|
||||
\fi
|
||||
\LT@echunk
|
||||
\LT@start
|
||||
\unvbox\z@
|
||||
\LT@get@widths
|
||||
\LT@bchunk}
|
||||
\def\LT@echunk{%
|
||||
\crcr\LT@save@row\cr\egroup
|
||||
\global\setbox\@ne\lastbox
|
||||
\unskip
|
||||
\egroup}
|
||||
\def\LT@entry#1#2{%
|
||||
\ifhmode\@firstofone{&}\fi\omit
|
||||
\ifnum#1=\c@LT@chunks
|
||||
\else
|
||||
\kern#2\relax
|
||||
\fi}
|
||||
\def\LT@entry@chop#1#2{%
|
||||
\noexpand\LT@entry
|
||||
{\ifnum#1>\c@LT@chunks
|
||||
1}{0pt%
|
||||
\else
|
||||
#1}{#2%
|
||||
\fi}}
|
||||
\def\LT@entry@write{%
|
||||
\noexpand\LT@entry^^J%
|
||||
\@spaces}
|
||||
\def\LT@kill{%
|
||||
\LT@echunk
|
||||
\LT@get@widths
|
||||
\expandafter\LT@rebox\LT@bchunk}
|
||||
\def\LT@rebox#1\bgroup{%
|
||||
#1\bgroup
|
||||
\unvbox\z@
|
||||
\unskip
|
||||
\setbox\z@\lastbox}
|
||||
\def\LT@blank@row{%
|
||||
\xdef\LT@save@row{\expandafter\LT@build@blank
|
||||
\romannumeral\number\LT@cols 001 }}
|
||||
\def\LT@build@blank#1{%
|
||||
\if#1m%
|
||||
\noexpand\LT@entry{1}{0pt}%
|
||||
\expandafter\LT@build@blank
|
||||
\fi}
|
||||
\def\LT@make@row{%
|
||||
\global\expandafter\let\expandafter\LT@save@row
|
||||
\csname LT@\romannumeral\c@LT@tables\endcsname
|
||||
\ifx\LT@save@row\relax
|
||||
\LT@blank@row
|
||||
\else
|
||||
{\let\LT@entry\or
|
||||
\if!%
|
||||
\ifcase\expandafter\expandafter\expandafter\LT@cols
|
||||
\expandafter\@gobble\LT@save@row
|
||||
\or
|
||||
\else
|
||||
\relax
|
||||
\fi
|
||||
!%
|
||||
\else
|
||||
\aftergroup\LT@blank@row
|
||||
\fi}%
|
||||
\fi}
|
||||
\let\setlongtables\relax
|
||||
\def\LT@get@widths{%
|
||||
\setbox\tw@\hbox{%
|
||||
\unhbox\@ne
|
||||
\let\LT@old@row\LT@save@row
|
||||
\global\let\LT@save@row\@empty
|
||||
\count@\LT@cols
|
||||
\loop
|
||||
\unskip
|
||||
\setbox\tw@\lastbox
|
||||
\ifhbox\tw@
|
||||
\LT@def@row
|
||||
\advance\count@\m@ne
|
||||
\repeat}%
|
||||
\ifx\LT@@save@row\@undefined
|
||||
\let\LT@@save@row\LT@save@row
|
||||
\fi}
|
||||
\def\LT@def@row{%
|
||||
\let\LT@entry\or
|
||||
\edef\@tempa{%
|
||||
\ifcase\expandafter\count@\LT@old@row
|
||||
\else
|
||||
{1}{0pt}%
|
||||
\fi}%
|
||||
\let\LT@entry\relax
|
||||
\xdef\LT@save@row{%
|
||||
\LT@entry
|
||||
\expandafter\LT@max@sel\@tempa
|
||||
\LT@save@row}}
|
||||
\def\LT@max@sel#1#2{%
|
||||
{\ifdim#2=\wd\tw@
|
||||
#1%
|
||||
\else
|
||||
\number\c@LT@chunks
|
||||
\fi}%
|
||||
{\the\wd\tw@}}
|
||||
\def\LT@hline{%
|
||||
\noalign{\ifnum0=`}\fi
|
||||
\penalty\@M
|
||||
\futurelet\@let@token\LT@@hline}
|
||||
\def\LT@@hline{%
|
||||
\ifx\@let@token\hline
|
||||
\global\let\@gtempa\@gobble
|
||||
\gdef\LT@sep{\penalty-\@medpenalty\vskip\doublerulesep}%
|
||||
\else
|
||||
\global\let\@gtempa\@empty
|
||||
\gdef\LT@sep{\penalty-\@lowpenalty\vskip-\arrayrulewidth}%
|
||||
\fi
|
||||
\ifnum0=`{\fi}%
|
||||
\multispan\LT@cols
|
||||
\unskip\leaders\hrule\@height\arrayrulewidth\hfill\cr
|
||||
\noalign{\LT@sep}%
|
||||
\multispan\LT@cols
|
||||
\unskip\leaders\hrule\@height\arrayrulewidth\hfill\cr
|
||||
\noalign{\penalty\@M}%
|
||||
\@gtempa}
|
||||
\def\LT@caption{%
|
||||
\noalign\bgroup
|
||||
\@ifnextchar[{\egroup\LT@c@ption\@firstofone}\LT@capti@n}
|
||||
\def\LT@c@ption#1[#2]#3{%
|
||||
\LT@makecaption#1\fnum@table{#3}%
|
||||
\def\@tempa{#2}%
|
||||
\ifx\@tempa\@empty\else
|
||||
{\let\\\space
|
||||
\addcontentsline{lot}{table}{\protect\numberline{\thetable}{#2}}}%
|
||||
\fi}
|
||||
\def\LT@capti@n{%
|
||||
\@ifstar
|
||||
{\egroup\LT@c@ption\@gobble[]}%
|
||||
{\egroup\@xdblarg{\LT@c@ption\@firstofone}}}
|
||||
\def\LT@makecaption#1#2#3{%
|
||||
\LT@mcol\LT@cols c{\hbox to\z@{\hss\parbox[t]\LTcapwidth{%
|
||||
\sbox\@tempboxa{#1{#2: }#3}%
|
||||
\ifdim\wd\@tempboxa>\hsize
|
||||
#1{#2: }#3%
|
||||
\else
|
||||
\hbox to\hsize{\hfil\box\@tempboxa\hfil}%
|
||||
\fi
|
||||
\endgraf\vskip\baselineskip}%
|
||||
\hss}}}
|
||||
\def\LT@output{%
|
||||
\ifnum\outputpenalty <-\@Mi
|
||||
\ifnum\outputpenalty > -\LT@end@pen
|
||||
\LT@err{floats and marginpars not allowed in a longtable}\@ehc
|
||||
\else
|
||||
\setbox\z@\vbox{\unvbox\@cclv}%
|
||||
\ifdim \ht\LT@lastfoot>\ht\LT@foot
|
||||
\dimen@\pagegoal
|
||||
\advance\dimen@\ht\LT@foot
|
||||
\advance\dimen@-\ht\LT@lastfoot
|
||||
\ifdim\dimen@<\ht\z@
|
||||
\setbox\@cclv\vbox{\unvbox\z@\copy\LT@foot\vss}%
|
||||
\@makecol
|
||||
\@outputpage
|
||||
\global\vsize\@colroom
|
||||
\setbox\z@\vbox{\box\LT@head}%
|
||||
\fi
|
||||
\fi
|
||||
\unvbox\z@\ifvoid\LT@lastfoot\copy\LT@foot\else\box\LT@lastfoot\fi
|
||||
\fi
|
||||
\else
|
||||
\setbox\@cclv\vbox{\unvbox\@cclv\copy\LT@foot\vss}%
|
||||
\@makecol
|
||||
\@outputpage
|
||||
\global\vsize\@colroom
|
||||
\copy\LT@head\nobreak
|
||||
\fi}
|
||||
\def\LT@end@hd@ft#1{%
|
||||
\LT@echunk
|
||||
\ifx\LT@start\endgraf
|
||||
\LT@err
|
||||
{Longtable head or foot not at start of table}%
|
||||
{Increase LTchunksize}%
|
||||
\fi
|
||||
\setbox#1\box\z@
|
||||
\LT@get@widths
|
||||
\LT@bchunk}
|
||||
\def\endfirsthead{\LT@end@hd@ft\LT@firsthead}
|
||||
\def\endhead{\LT@end@hd@ft\LT@head}
|
||||
\def\endfoot{\LT@end@hd@ft\LT@foot}
|
||||
\def\endlastfoot{\LT@end@hd@ft\LT@lastfoot}
|
||||
\def\LT@startpbox#1{%
|
||||
\bgroup
|
||||
\let\@footnotetext\LT@p@ftntext
|
||||
\setlength\hsize{#1}%
|
||||
\@arrayparboxrestore
|
||||
\vrule \@height \ht\@arstrutbox \@width \z@}
|
||||
\def\LT@endpbox{%
|
||||
\@finalstrut\@arstrutbox
|
||||
\egroup
|
||||
\the\LT@p@ftn
|
||||
\global\LT@p@ftn{}%
|
||||
\hfil}
|
||||
\def\LT@p@ftntext#1{%
|
||||
\edef\@tempa{\the\LT@p@ftn\noexpand\footnotetext[\the\c@footnote]}%
|
||||
\global\LT@p@ftn\expandafter{\@tempa{#1}}}%
|
||||
\endinput
|
||||
%%
|
||||
%% End of file `longtable.sty'.
|
||||
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 409 KiB |
@@ -1,422 +0,0 @@
|
||||
# This file contains all classes and functions directly related to the operation of the helmholtz test stand.
|
||||
# The two main classes are Axis and ArduinoCtrl, see their definitions for details.
|
||||
|
||||
# import packages:
|
||||
import numpy as np
|
||||
import serial
|
||||
import traceback
|
||||
from tkinter import messagebox
|
||||
|
||||
# import other project files
|
||||
from User_Interface import ui_print
|
||||
from pyps2000b import PS2000B
|
||||
from Arduino import Arduino
|
||||
import config_handling as config
|
||||
import globals as g
|
||||
|
||||
|
||||
class Axis:
|
||||
# Main class representing an axis (x,y,z) of the test stand
|
||||
# contains static and dynamic status information about this axis and methods to control it
|
||||
|
||||
def __init__(self, index, device, PSU_channel, arduino_pin):
|
||||
# static information
|
||||
self.index = index # index of this axis, 0->X, 1->Y, 2->Z
|
||||
self.device = device # power supply object for this axis (PS2000B class object)
|
||||
self.channel = PSU_channel # power supply unit channel (0 or 1)
|
||||
self.ardPin = arduino_pin # output pin on the arduino for switching polarity on this axis
|
||||
|
||||
self.name = g.AXIS_NAMES[index] # get name of this axis from list in globals.py (e.g. "X-Axis"
|
||||
self.port = g.PORTS[index] # get serial port of this axis PSU
|
||||
|
||||
# read static information from the configuration object (which has read it from the config file or settings):
|
||||
self.resistance = float(config.read_from_config(self.name, "resistance", config.CONFIG_OBJECT))
|
||||
self.max_amps = float(config.read_from_config(self.name, "max_amps", config.CONFIG_OBJECT))
|
||||
self.max_volts = float(config.read_from_config(self.name, "max_volts", config.CONFIG_OBJECT))
|
||||
|
||||
self.coil_constant = float(config.read_from_config(self.name, "coil_const", config.CONFIG_OBJECT))
|
||||
self.ambient_field = float(config.read_from_config(self.name, "ambient_field", config.CONFIG_OBJECT))
|
||||
|
||||
max_field = self.max_amps * self.coil_constant # calculate max field reachable in this axis
|
||||
self.max_field = np.array([-max_field, max_field]) # make array with min/max reachable field (w/o compensation)
|
||||
# calculate max and min field that can be reached after compensating for the ambient field
|
||||
self.max_comp_field = np.array([self.ambient_field - max_field, self.ambient_field + max_field]) # [min, max]
|
||||
|
||||
# initialize dynamic information, this is updated by self.update_status_info() later
|
||||
self.connected = "Not Connected"
|
||||
self.output_active = "Unknown" # power output on the PSU enabled?
|
||||
self.remote_ctrl_active = "Unknown" # remote control on the PSU enabled?
|
||||
self.voltage_setpoint = 0 # target voltage on PSU [V]
|
||||
self.voltage = 0 # actual voltage on PSU [V]
|
||||
self.current_setpoint = 0 # target current on PSU [A]
|
||||
self.current = 0 # actual current on PSU [A]
|
||||
|
||||
self.polarity_switched = "Unknown" # polarity switched on the Arduino?
|
||||
|
||||
self.target_field_comp = 0 # field to be created by coil pair (this is sent to the coils) [T]
|
||||
self.target_field = 0 # field that should occur in measurement area (ambient still needs to be compensated) [T]
|
||||
self.target_current = 0 # signed current that should pass through coil pair [A]
|
||||
|
||||
if self.device is not None:
|
||||
self.update_status_info()
|
||||
|
||||
def update_status_info(self): # Read out the values of the dynamic parameters stored in this object and update them
|
||||
try: # try to read out the data, this will fail on connection error to PSU
|
||||
self.device.update_device_information(self.channel) # update the information in the device object
|
||||
device_status = self.device.get_device_status_information(self.channel) # get object with new status info
|
||||
|
||||
if device_status.output_active: # is the power output active?
|
||||
self.output_active = "Active"
|
||||
else:
|
||||
self.output_active = "Inactive"
|
||||
|
||||
# is remote control active, allowing the device to be controlled by this program?
|
||||
if device_status.remote_control_active:
|
||||
self.remote_ctrl_active = "Active"
|
||||
else:
|
||||
self.remote_ctrl_active = "Inactive"
|
||||
|
||||
# get currents and voltages:
|
||||
self.voltage = self.device.get_voltage(self.channel)
|
||||
self.voltage_setpoint = self.device.get_voltage_setpoint(self.channel)
|
||||
self.current = self.device.get_current(self.channel)
|
||||
self.current_setpoint = self.device.get_current_setpoint(self.channel)
|
||||
|
||||
except (serial.serialutil.SerialException, IndexError): # Connection error, usually the PSU is unplugged
|
||||
if self.connected == "Connected": # only show error messages if the device was connected before this error
|
||||
# Show error as print-out in console and as pop-up:
|
||||
ui_print("Connection Error with %s PSU on %s" % (self.name, self.port))
|
||||
messagebox.showerror("PSU Error", "Connection Error with %s PSU on %s" % (self.name, self.port))
|
||||
# set status attributes to connection error status:
|
||||
self.connected = "Connection Error"
|
||||
self.output_active = "Unknown"
|
||||
self.remote_ctrl_active = "Unknown"
|
||||
else: # no communications error
|
||||
self.connected = "Connected" # PSU is connected
|
||||
|
||||
def print_status(self): # print out the current status of the PSU channel (not used at the moment)
|
||||
ui_print("%s, %0.2f V, %0.2f A"
|
||||
% (self.device.get_device_status_information(self.channel),
|
||||
self.device.get_voltage(self.channel), self.device.get_current(self.channel)))
|
||||
|
||||
def power_down(self): # temporary powerdown, set outputs to 0 but keep connections enabled
|
||||
try:
|
||||
# set class object attributes to 0 to reflect shutdown in status displays, log files etc.
|
||||
self.target_current = 0
|
||||
self.target_field = 0
|
||||
self.target_field_comp = 0
|
||||
|
||||
if self.device is not None: # there is a PSU connected for this axis
|
||||
self.device.set_voltage(0, self.channel) # set voltage on PSU channel to 0
|
||||
self.device.set_current(0, self.channel) # set current on PSU channel to 0
|
||||
self.device.disable_output(self.channel) # disable power output on PSU channel
|
||||
g.ARDUINO.digitalWrite(self.ardPin, "LOW") # set arduino pin for polarity switch relay to unpowered state
|
||||
except Exception as e: # some error was encountered
|
||||
# show error message:
|
||||
ui_print("Error while powering down %s: %s" % (self.name, e))
|
||||
messagebox.showerror("PSU Error!", "Error while powering down %s: \n%s" % (self.name, e))
|
||||
|
||||
def set_signed_current(self, value):
|
||||
# sets current with correct polarity on this axis, this is the primary way to control the test stand
|
||||
|
||||
# ui_print("Attempting to set current", value, "A")
|
||||
self.target_current = value # show target value in object attribute for status display, logging etc.
|
||||
|
||||
if abs(value) > self.max_amps: # prevent excessive currents
|
||||
self.power_down() # set output to 0 and deactivate
|
||||
raise ValueError("Invalid current value on %s. Tried %0.2fA, max. %0.2fA allowed"
|
||||
% (self.name, value, self.max_amps))
|
||||
|
||||
elif value >= 0: # switch the e-box relay to change polarity as needed
|
||||
g.ARDUINO.digitalWrite(self.ardPin, "LOW") # command the output pin on the arduino in the electronics box
|
||||
elif value < 0:
|
||||
g.ARDUINO.digitalWrite(self.ardPin, "HIGH") # command the output pin on the arduino in the electronics box
|
||||
|
||||
# determine voltage limit to be set on PSU, must be high enough to not limit the current:
|
||||
# min. 8V, max. max_volts, in-between as needed with current value (+margin to not limit current)
|
||||
maxVoltage = min(max(1.3 * value * self.resistance, 8), self.max_volts) # limit voltage
|
||||
if self.connected == "Connected": # only try to command the PSU if its actually connected
|
||||
self.device.set_current(abs(value), self.channel) # set desired current
|
||||
self.device.set_voltage(maxVoltage, self.channel) # set voltage limit
|
||||
self.device.enable_output(self.channel) # activate the power output
|
||||
else: # the PSU is not connected
|
||||
ui_print(self.name, "not connected, can't set current.")
|
||||
|
||||
def set_field_simple(self, value): # forms magnetic field as specified by value, w/o cancelling ambient field
|
||||
self.target_field = value # update object attribute for display
|
||||
self.target_field_comp = value # same as above, bc no compensation
|
||||
current = value / self.coil_constant # calculate needed current
|
||||
self.set_signed_current(current) # command the test stand
|
||||
|
||||
def set_field(self, value): # forms magnetic field as specified by value, corrected for ambient field
|
||||
self.target_field = value # update object attribute for display
|
||||
field = value - self.ambient_field # calculate needed field after compensation
|
||||
self.target_field_comp = field # update object attribute for display
|
||||
current = field / self.coil_constant # calculate needed current
|
||||
self.set_signed_current(current) # command the test stand
|
||||
|
||||
|
||||
class ArduinoCtrl(Arduino):
|
||||
# main class to control the electronics box (which means commanding the arduino inside)
|
||||
# inherits from the Arduino library
|
||||
|
||||
def __init__(self):
|
||||
self.connected = "Unknown" # connection status attribute, nominal "Connected"
|
||||
self.pins = [0, 0, 0] # initialize list with pins to switch relay of each axis
|
||||
for i in range(3): # get correct pins from the config
|
||||
self.pins[i] = int(config.read_from_config(g.AXIS_NAMES[i], "relay_pin", config.CONFIG_OBJECT))
|
||||
|
||||
ui_print("\nConnecting to Arduino...")
|
||||
try: # try to set up the arduino
|
||||
Arduino.__init__(self) # search for connected arduino and connect by initializing arduino library class
|
||||
for pin in self.pins:
|
||||
self.pinMode(pin, "Output")
|
||||
self.digitalWrite(pin, "LOW")
|
||||
except Exception as e: # some error occurred, usually the arduino is not connected
|
||||
ui_print("Connection to Arduino failed.", e)
|
||||
self.connected = "Not Connected"
|
||||
else: # connection was successfully established
|
||||
self.connected = "Connected"
|
||||
ui_print("Arduino ready.")
|
||||
|
||||
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:
|
||||
self.digitalWrite(pin, "LOW")
|
||||
|
||||
|
||||
def value_in_limits(axis, key, value): # Check if value is within safe limits (set in globals.py)
|
||||
# axis is string with axis name, e.g. "X-Axis"
|
||||
# key specifies which value to check, e.g. current
|
||||
max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(axis)] # get max value from dictionary in globals.py
|
||||
min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(axis)] # get min value from dictionary in globals.py
|
||||
|
||||
if float(value) > float(max_value): # value is too high
|
||||
return 'HIGH'
|
||||
elif float(value) < float(min_value): # value is too low
|
||||
return 'LOW'
|
||||
else: # value is within limits
|
||||
return 'OK'
|
||||
|
||||
|
||||
def setup_all(): # main test stand initialization function
|
||||
# creates device objects for all PSUs and Arduino and sets their values
|
||||
# initializes an object of class Axis for all three axes (x,y,z)
|
||||
|
||||
# Setup Arduino:
|
||||
try: # broad error handling for unforeseen errors, handling in ArduinoCtrl should catch most errors
|
||||
if g.ARDUINO is not None: # the arduino has been initialized before, so we need to first close its connection
|
||||
try:
|
||||
g.ARDUINO.close() # close serial link
|
||||
except serial.serialutil.SerialException:
|
||||
pass
|
||||
# serial.flush() in Arduino.close() fails when reconnecting
|
||||
# this ignores it and allows serial.close() to execute (I think)
|
||||
except AttributeError:
|
||||
pass
|
||||
# when no Arduino is connected but g.ARDUINO has been initialized then there is nothing to close
|
||||
# this throws an exception, which can be ignored
|
||||
|
||||
g.ARDUINO = ArduinoCtrl() # initialize the arduino object from the control class, connects and sets up
|
||||
|
||||
except Exception as e: # some unforeseen error occurred (not connected issue handled in ArduinoCtrl class)
|
||||
# show error messages to alert user
|
||||
ui_print("Arduino setup failed:", e)
|
||||
ui_print(traceback.print_exc())
|
||||
messagebox.showerror("Error!", "Arduino setup failed:\n%s \nCheck traceback in console." % e)
|
||||
|
||||
# Setup PSUs and axis objects:
|
||||
g.AXES = [] # initialize global list containing the three axis objects
|
||||
|
||||
# get serial ports for the PSUs from config
|
||||
g.XY_PORT = config.read_from_config("PORTS", "xy_port", config.CONFIG_OBJECT)
|
||||
g.Z_PORT = config.read_from_config("PORTS", "z_port", config.CONFIG_OBJECT)
|
||||
g.PORTS = [g.XY_PORT, g.XY_PORT, g.Z_PORT] # write list with PSU port for each axis (X/Y share PSU)
|
||||
|
||||
# setup PSU and axis objects for X and Y axes:
|
||||
ui_print("\nConnecting to XY Device on %s..." % g.XY_PORT)
|
||||
try: # try to connect to the PSU
|
||||
if g.XY_DEVICE is not None: # if PSU has previously been connected we need to close the serial link first
|
||||
ui_print("Closing serial connection on XY device")
|
||||
g.XY_DEVICE.serial.close()
|
||||
g.XY_DEVICE = None
|
||||
g.XY_DEVICE = PS2000B.PS2000B(g.XY_PORT) # setup PSU
|
||||
ui_print("Connection established.")
|
||||
g.X_AXIS = Axis(0, g.XY_DEVICE, 0, g.ARDUINO.pins[0]) # create axis objects (index, PSU, channel, relay pin)
|
||||
g.Y_AXIS = Axis(1, g.XY_DEVICE, 1, g.ARDUINO.pins[1])
|
||||
except serial.serialutil.SerialException: # communications error, usually PSU is not connected or wrong port set
|
||||
g.X_AXIS = Axis(0, None, 0, g.ARDUINO.pins[0]) # create axis objects without the PSU
|
||||
g.Y_AXIS = Axis(1, None, 1, g.ARDUINO.pins[1])
|
||||
ui_print("XY Device not connected or incorrect port set.")
|
||||
|
||||
# same for the Z axis
|
||||
ui_print("Connecting to Z Device on %s..." % g.Z_PORT)
|
||||
try:
|
||||
if g.Z_DEVICE is not None:
|
||||
ui_print("Closing serial connection on Z device")
|
||||
g.Z_DEVICE.serial.close()
|
||||
g.Z_DEVICE = None
|
||||
g.Z_DEVICE = PS2000B.PS2000B(g.Z_PORT)
|
||||
ui_print("Connection established.")
|
||||
g.Z_AXIS = Axis(2, g.Z_DEVICE, 0, g.ARDUINO.pins[2])
|
||||
except serial.serialutil.SerialException:
|
||||
g.Z_AXIS = Axis(2, None, 0, g.ARDUINO.pins[2])
|
||||
ui_print("Z Device not connected or incorrect port set.")
|
||||
|
||||
# put newly created axis objects into a list for access later
|
||||
g.AXES.append(g.X_AXIS)
|
||||
g.AXES.append(g.Y_AXIS)
|
||||
g.AXES.append(g.Z_AXIS)
|
||||
|
||||
ui_print("") # print new line
|
||||
|
||||
|
||||
def set_to_zero(device): # sets voltages and currents to 0 on all channels of a specific PSU
|
||||
device.voltage1 = 0
|
||||
device.current1 = 0
|
||||
device.voltage2 = 0
|
||||
device.current2 = 0
|
||||
|
||||
|
||||
def power_down_all(): # on all PSUs set all outputs to 0 but keep connections enabled
|
||||
for axis in g.AXES:
|
||||
axis.power_down() # set outputs to 0 and pin to low on this axis
|
||||
|
||||
|
||||
def shut_down_all(): # safe shutdown at program end or on error
|
||||
# set outputs to 0 and disable connections on all devices
|
||||
|
||||
ui_print("\nAttempting to safely shut down all devices. Check equipment to confirm.")
|
||||
# start writing string to later show how shutdown on all devices went in a single info pop-up:
|
||||
message = "Tried to shut down all devices. Check equipment to confirm."
|
||||
|
||||
# Shut down PSUs:
|
||||
if g.XY_DEVICE is not None: # the PSU has been setup before
|
||||
try: # try to safe the PSU
|
||||
set_to_zero(g.XY_DEVICE) # set currents and voltages to 0 for both channels
|
||||
g.XY_DEVICE.disable_all() # disable power output on both channels
|
||||
except BaseException as e: # some error occurred, usually device has been disconnected
|
||||
ui_print("Error while deactivating XY PSU:", e) # print the problem in the console
|
||||
message += "\nError while deactivating XY PSU: %s" % e # append status to the message to show later
|
||||
else: # device was successfully deactivated
|
||||
ui_print("XY PSU deactivated.")
|
||||
message += "\nXY PSU deactivated." # append message to show later
|
||||
else: # the device was not connected before
|
||||
# tell user there was no need/no possibility to deactivate:
|
||||
ui_print("XY PSU not connected, can't deactivate.")
|
||||
message += "\nXY PSU not connected, can't deactivate."
|
||||
|
||||
# same as above
|
||||
if g.Z_DEVICE is not None:
|
||||
try:
|
||||
set_to_zero(g.Z_DEVICE)
|
||||
g.Z_DEVICE.disable_all()
|
||||
except BaseException as e:
|
||||
ui_print("Error while deactivating Z PSU:", e)
|
||||
message += "\nError while deactivating Z PSU: %s" % e
|
||||
else:
|
||||
ui_print("Z PSU deactivated.")
|
||||
message += "\nZ PSU deactivated."
|
||||
else:
|
||||
ui_print("Z PSU not connected, can't deactivate.")
|
||||
message += "\nZ PSU not connected, can't deactivate."
|
||||
|
||||
# Shut down Arduino:
|
||||
try:
|
||||
g.ARDUINO.safe() # call safe method in ArduinoCtrl class (all relay pins to LOW)
|
||||
except BaseException as e: # some error occurred
|
||||
ui_print("Arduino safing unsuccessful:", e)
|
||||
message += "\nArduino safing unsuccessful: %s" % e # append to the message to show later
|
||||
# this throws no exception, even when arduino is not connected
|
||||
# ToDo (optional): figure out error handling for this
|
||||
try:
|
||||
g.ARDUINO.close() # close the serial link
|
||||
except BaseException as e: # something went wrong there
|
||||
if g.ARDUINO.connected == "Connected": # Arduino was connected, some error occurred
|
||||
ui_print("Closing Arduino connection failed:", e)
|
||||
message += "\nClosing Arduino connection failed: %s" % e
|
||||
else: # Arduino was not connected, so error is expected
|
||||
ui_print("Arduino not connected, can't close connection.")
|
||||
message += "\nArduino not connected, can't close connection."
|
||||
else: # no problems, connection was successfully closed
|
||||
ui_print("Serial connection to Arduino closed.")
|
||||
message += "\nSerial connection to Arduino closed."
|
||||
|
||||
messagebox.showinfo("Program ended", message) # Show a unified pop-up with how the shutdown on each device went
|
||||
|
||||
|
||||
def set_field_simple(vector): # forms magnetic field as specified by vector, w/o cancelling ambient field
|
||||
for i in [0, 1, 2]:
|
||||
try:
|
||||
g.AXES[i].set_field_simple(vector[i]) # try to set the field on each axis
|
||||
except ValueError as e: # a limit was violated, usually the needed current was too high
|
||||
ui_print(e) # let the user know
|
||||
|
||||
|
||||
def set_field(vector): # forms magnetic field as specified by vector, corrected for ambient field
|
||||
# same as set_field_simple(), but with compensation
|
||||
for i in [0, 1, 2]:
|
||||
try:
|
||||
g.AXES[i].set_field(vector[i])
|
||||
except ValueError as e:
|
||||
ui_print(e)
|
||||
|
||||
|
||||
def set_current_vec(vector): # sets currents on each axis according to given vector
|
||||
i = 0
|
||||
for axis in g.AXES:
|
||||
try:
|
||||
axis.target_field = 0 # set target field attribute to 0 to show that current, not field is controlled atm
|
||||
axis.target_field_comp = 0 # as above
|
||||
|
||||
axis.set_signed_current(vector[i]) # command test stand to set the current
|
||||
except ValueError as e: # current was too high
|
||||
ui_print(e) # print out the error message
|
||||
i += 1
|
||||
|
||||
|
||||
def devices_ok(xy_off=False, z_off=False, arduino_off=False):
|
||||
# check if all devices are connected, return True if yes
|
||||
# checks for individual devices can be disabled by parameters above (default not disabled)
|
||||
try: # handle errors while checking connections
|
||||
if not xy_off: # if check for this device is not disabled
|
||||
if g.XY_DEVICE is not None: # has the handle for this device been set?
|
||||
g.X_AXIS.update_status_info() # update info --> this actually communicates with the device
|
||||
if g.X_AXIS.connected != "Connected": # if not connected
|
||||
return False # return and exit function
|
||||
else: # if handle has not been set the device is inactive --> not ok
|
||||
return False
|
||||
if not z_off: # same as above
|
||||
if g.Z_DEVICE is not None:
|
||||
g.Z_AXIS.update_status_info()
|
||||
if g.Z_AXIS.connected != "Connected":
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
if not arduino_off: # check not disabled
|
||||
g.ARDUINO.update_status_info() # update status info --> attempts communication
|
||||
if g.ARDUINO.connected != "Connected":
|
||||
return False
|
||||
except Exception as e: # if an error is encountered while checking the devices
|
||||
messagebox.showerror("Error!", "Error while checking devices: \n%s" % e) # show error pop-up
|
||||
return False # clearly something is not ok
|
||||
else: # if nothing has triggered so far all devices are ok --> return True
|
||||
return True
|
||||
@@ -1,101 +0,0 @@
|
||||
# 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
|
||||
from datetime import datetime
|
||||
import os
|
||||
from tkinter import filedialog
|
||||
from tkinter import messagebox
|
||||
|
||||
# import other project files
|
||||
import globals as g
|
||||
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 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
|
||||
@@ -1,178 +0,0 @@
|
||||
# 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
|
||||
import numpy as np
|
||||
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("\nStarting 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...
|
||||
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
|
||||
|
||||
# modify data to show instantaneous jumps in field to reflect test stand operation
|
||||
new_array = np.array([[0, 0, 0, 0]], dtype=float) # initialize modified array, zeros to show start from no fields
|
||||
|
||||
last_vals = [0, 0, 0] # [x,y,z] field values from last data point (zero here), used to create step in data
|
||||
for row in array[:, 0:4]: # go through each row in the original array
|
||||
# create extra datapoint at current timestamp, with field values from last to create a "step" in the plot:
|
||||
new_array = np.append(new_array, [[row[0], *last_vals]], axis=0)
|
||||
new_array = np.append(new_array, [row], axis=0) # add actual datapoint for current timestamp
|
||||
last_vals = row[1:4] # save values from current timestamp for next
|
||||
new_array = np.append(new_array, [[new_array[-1, 0], 0, 0, 0]], axis=0) # append last datapoint with 0 fields
|
||||
|
||||
# extract data and plot:
|
||||
t = new_array[:, 0] # extract time column
|
||||
for i in [0, 1, 2]: # go through all three axes
|
||||
data = new_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
|
||||
@@ -1,46 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
# Main file of the program. Run this file to start the application.
|
||||
|
||||
# import packages:
|
||||
|
||||
from os.path import exists
|
||||
import traceback
|
||||
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
|
||||
from src.helmholtz_cage_device import HelmholtzCageDevice
|
||||
import src.globals as g
|
||||
import src.config_handling as config
|
||||
import src.csv_logging as log
|
||||
from src.magnetometer import MagnetometerProxy
|
||||
from src.user_interface import HelmholtzGUI
|
||||
from src.socket_control import SocketInterfaceThread
|
||||
from src.utility import ui_print
|
||||
|
||||
|
||||
def program_end(): # called on exception or when user closes application
|
||||
# safely shuts everything down and saves any unsaved data
|
||||
def program_start():
|
||||
""" Application entry point """
|
||||
print("Starting application")
|
||||
|
||||
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
|
||||
|
||||
config.CONFIG_FILE = 'config.ini' # set the config file path
|
||||
# ToDo: remember what the last config file was
|
||||
config.CONFIG_FILE = config.get_default_config_path() # set the config file path
|
||||
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
|
||||
@@ -48,31 +28,66 @@ try: # start normal operations
|
||||
|
||||
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
|
||||
# initiate communication with devices and initialize all major program objects
|
||||
g.CAGE_DEVICE = HelmholtzCageDevice()
|
||||
# Mostly a data structure to hold field data broadcast by connected tcp client with HW access.
|
||||
g.MAGNETOMETER = MagnetometerProxy()
|
||||
|
||||
print("\nOpening User Interface...")
|
||||
# initialize user interface
|
||||
g.app = HelmholtzGUI()
|
||||
# tell all functions that the user interface is now runnings
|
||||
g.exit_flag = False
|
||||
|
||||
g.app = HelmholtzGUI() # initialize user interface
|
||||
g.exitFlag = False # tell all functions that the user interface is now running
|
||||
g.app.state('zoomed') # open UI in maximized window
|
||||
g.app.StatusDisplay.continuous_label_update(g.app, 500) # initiate regular Status Display updates (ms)
|
||||
ui_print("Program Initialized")
|
||||
config.check_config(config.CONFIG_OBJECT) # check config for values exceeding limits
|
||||
# Connect hardware to HelmholtzCageDevice adapter object
|
||||
g.CAGE_DEVICE.connect_hardware_async()
|
||||
|
||||
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
|
||||
# check config for values exceeding limits
|
||||
config.check_config(config.CONFIG_OBJECT)
|
||||
|
||||
g.app.protocol("WM_DELETE_WINDOW", program_end) # call program_end function if user closes the application
|
||||
# Create TCP/Socket listener
|
||||
socket_controller = SocketInterfaceThread()
|
||||
socket_controller.start()
|
||||
|
||||
g.app.mainloop() # start main program loop
|
||||
# call program_end function if user closes the application
|
||||
g.app.protocol("WM_DELETE_WINDOW", program_end)
|
||||
|
||||
ui_print("Application initialized")
|
||||
# start main program loop
|
||||
g.app.mainloop()
|
||||
|
||||
|
||||
except Exception as e: # An error has occurred somewhere in the program
|
||||
print("\nAn error occurred, Shutting down.")
|
||||
# shop pup-up error message:
|
||||
message = "%s.\nSee python console traceback for more details. " \
|
||||
"\nShutting down devices, check equipment to confirm." % e
|
||||
messagebox.showerror("Error!", message)
|
||||
print(traceback.print_exc()) # print error traceback in the python console
|
||||
program_end() # safely close everything and shut down devices
|
||||
def program_end():
|
||||
""" Safely shuts everything down and saves any unsaved data.
|
||||
Called on exception or when user closes application """
|
||||
|
||||
# tell everything else the application has been closed
|
||||
g.exit_flag = True
|
||||
|
||||
# shut down devices and end all threads.
|
||||
g.CAGE_DEVICE.destroy()
|
||||
|
||||
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(filepath) # write the data to the chosen file
|
||||
|
||||
if g.app is not None:
|
||||
g.app.destroy() # close application
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
program_start()
|
||||
except Exception as e: # An error has occurred somewhere in the program
|
||||
print("\nAn error occurred, shutting down.")
|
||||
# shop pop-up error message:
|
||||
message = "%s.\nSee Python console traceback for more details. " \
|
||||
"\nShutting down devices, check equipment to confirm." % e
|
||||
messagebox.showerror("Error!", message)
|
||||
# print error traceback in the python console
|
||||
print(traceback.print_exc())
|
||||
# safely close everything and shut down devices
|
||||
program_end()
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Helmholtz Cage Control Software
|
||||
|
||||
This software is used to control the magnetic field test bench at the Institute for Space Systems (IRS) at
|
||||
the University of Stuttgart. The test bench consists of three Helmholtz coil pairs, one in each spatial axis.
|
||||
The amount of current flowing through these coils is controlled by two PS2000B power supply units.
|
||||
The polarity is switched by a set of relays that are controller by an Arduino Uno.
|
||||
|
||||
This repository contains the source code and is used to develop and modify the program. Releases are distributed
|
||||
in a [separate repository](https://egit.irs.uni-stuttgart.de/zietzm/Helmholtz_Test_Bench_Releases/releases).
|
||||
|
||||
## Installation
|
||||
1. Download latest release from
|
||||
[distribution repository](https://egit.irs.uni-stuttgart.de/zietzm/Helmholtz_Test_Bench_Releases/releases)
|
||||
2. Unpack ZIP-Folder
|
||||
3. Run `Helmholtz Cage Control.exe`
|
||||
|
||||
Alternatively use the `Helmholtz Cage Control.exe` from the `Release` folder in this repository.
|
||||
|
||||
## Users Guide
|
||||
For detailed instructions please refer to the main documentation PDF in the `Documentation` folder.
|
||||
|
||||
## Changelog
|
||||
### V 1.0 (02.03.2021)
|
||||
Initial Version
|
||||
|
||||
### V 1.1 (10.03.2021)
|
||||
- updated UI with user feedback
|
||||
|
||||
### V 1.1.1 (27.03.2021)
|
||||
- added documentation
|
||||
- tweaks and ToDo's after testing with all devices connected (communication with PSUs is too slow)
|
||||
- UI tweaks to better fit smaller display of cleanroom PC
|
||||
@@ -1,5 +1,15 @@
|
||||
numpy==1.19.3 # bug in numpy 1.19.4, 1.19.3 used as workaround
|
||||
pyserial~=3.5
|
||||
future~=0.18.2
|
||||
pandas~=1.1.5
|
||||
matplotlib~=3.3.2
|
||||
appdirs==1.4.4
|
||||
cycler==0.10.0
|
||||
future==0.18.2
|
||||
kiwisolver==1.3.2
|
||||
matplotlib==3.3.4
|
||||
# numpy==1.19.3 ## do not include versioning to avoid versioning conflict
|
||||
pandas==1.1.5
|
||||
Pillow==8.4.0
|
||||
pyparsing==2.4.7
|
||||
pyserial==3.5
|
||||
python-dateutil==2.8.2
|
||||
pytz==2021.3
|
||||
scipy==1.7.1
|
||||
six==1.16.0
|
||||
screeninfo~=0.8.1
|
||||
@@ -0,0 +1,40 @@
|
||||
from src.arduino import Arduino
|
||||
from src.utility import ui_print
|
||||
import src.config_handling as config_handling
|
||||
import src.globals as g
|
||||
|
||||
|
||||
class ArduinoDevice(Arduino):
|
||||
""" Main class to control the electronics box (which means commanding the arduino inside).
|
||||
Inherits from the Arduino library. All axis indices are go from 0-2 for x,y and z respectively"""
|
||||
|
||||
def __init__(self, com_port):
|
||||
self.pins = [0, 0, 0] # initialize list with pins to switch relay of each axis
|
||||
for i in range(3): # get correct pins from the config
|
||||
self.pins[i] = int(config_handling.read_from_config(g.AXIS_NAMES[i], "relay_pin", config_handling.CONFIG_OBJECT))
|
||||
|
||||
# try to set up the arduino. Exceptions are handled by caller
|
||||
# search for connected arduino and connect by initializing arduino library class
|
||||
Arduino.__init__(self, port=com_port, timeout=2)
|
||||
for pin in self.pins:
|
||||
self.pinMode(pin, "Output")
|
||||
self.digitalWrite(pin, "LOW")
|
||||
|
||||
def set_axis_polarity(self, axis_index, reverse):
|
||||
"""Sets the polarity of the axis (indexed with 0-2). True is reverse polarity"""
|
||||
if reverse:
|
||||
self.digitalWrite(self.pins[axis_index], "HIGH")
|
||||
else:
|
||||
self.digitalWrite(self.pins[axis_index], "LOW")
|
||||
|
||||
def get_axis_polarity(self, idx):
|
||||
"""Returns a bool indicating whether the axis polarity is reversed (True)."""
|
||||
return self.digitalRead(self.pins[idx]) # pin is HIGH --> relay is switched
|
||||
|
||||
def idle(self):
|
||||
"""Sets relay switching pins to low to de-power most of the electronics box"""
|
||||
for pin in self.pins:
|
||||
self.digitalWrite(pin, "LOW")
|
||||
|
||||
def shutdown(self):
|
||||
self.idle()
|
||||
@@ -2,14 +2,16 @@
|
||||
# The configparser module is used for processing. Config files are of type .ini
|
||||
|
||||
# import packages:
|
||||
import pathlib
|
||||
import appdirs
|
||||
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
|
||||
import src.globals as g
|
||||
from src.utility import ui_print
|
||||
import src.user_interface as ui
|
||||
import src.helmholtz_cage_device as helmholtz_cage_device
|
||||
|
||||
global CONFIG_FILE # string storing the path of the used config file
|
||||
global CONFIG_OBJECT # object of type ConfigParser(), storing all configuration information
|
||||
@@ -34,7 +36,7 @@ def read_from_config(section, key, config_object): # read a specific value from
|
||||
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)
|
||||
ui_print("Error while reading config file:", e)
|
||||
raise KeyError("Could not find key", key, "in config file.")
|
||||
|
||||
|
||||
@@ -51,7 +53,7 @@ def edit_config(section, key, value, override=False): # edit a specific value i
|
||||
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
|
||||
value_ok = helmholtz_cage_device.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
|
||||
@@ -71,23 +73,21 @@ def edit_config(section, key, value, override=False): # edit a specific value i
|
||||
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.")
|
||||
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.")
|
||||
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)
|
||||
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
|
||||
@@ -101,17 +101,22 @@ def check_config(config_object): # check all numeric values in the config and s
|
||||
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:
|
||||
# Print errors to console
|
||||
ui_print("Config file contains errors:")
|
||||
for axis in g.AXIS_NAMES:
|
||||
# print out results for each axis
|
||||
ui_print(axis, ":", *concerns[axis])
|
||||
|
||||
# shop pop-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
|
||||
g.app.show_frame(ui.HardwareConfiguration) # open configuration window so user can check values
|
||||
else:
|
||||
pass
|
||||
# If everything is fine, don't generate log spam
|
||||
# ui_print("Config file checked.")
|
||||
|
||||
|
||||
def reset_config_to_default(): # reset values in config object to defaults (set in globals.py)
|
||||
@@ -126,6 +131,12 @@ def reset_config_to_default(): # reset values in config object to defaults (set
|
||||
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
|
||||
config.add_section("Supplies") # add section for PSU serial ports
|
||||
for key in g.default_psu_config.keys(): # go through dictionary of default serial ports
|
||||
config.set("Supplies", key, str(g.default_psu_config[key])) # set the value for each axis
|
||||
|
||||
|
||||
def get_default_config_path():
|
||||
config_folder = pathlib.Path(appdirs.user_config_dir(appname="helmholtz_test_bench", appauthor="IRS"))
|
||||
config_folder.mkdir(parents=True, exist_ok=True)
|
||||
return str(config_folder.joinpath("config.ini"))
|
||||
@@ -0,0 +1,189 @@
|
||||
# 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.
|
||||
|
||||
from datetime import datetime
|
||||
import os
|
||||
import csv
|
||||
|
||||
from tkinter import filedialog
|
||||
from tkinter import messagebox
|
||||
|
||||
import src.globals as g
|
||||
from src.utility import ui_print
|
||||
|
||||
|
||||
log_data = [] # List containing the logged data, in-program representation of csv numerical 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
|
||||
|
||||
# Dictionary listing the options that control what is present is the log output
|
||||
# Key: The option that this control sets
|
||||
# Value: displayed in the UI
|
||||
logging_selection_options = {
|
||||
'timestamp': 'Timestamp',
|
||||
'device_stat': 'PSU + Arduino Status',
|
||||
'x_axis': 'Log X-Axis Data',
|
||||
'y_axis': 'Log Y-Axis Data',
|
||||
'z_axis': 'Log Z-Axis Data',
|
||||
'v': 'Voltage Setpoint',
|
||||
'v_actual': 'Actual Voltage',
|
||||
'i': 'Current Setpoint',
|
||||
'i_actual': 'Actual Current',
|
||||
'tgt_field': 'Target Field',
|
||||
'tgt_field_raw': 'Target Field Raw',
|
||||
'tgt_field_i': 'Target Current',
|
||||
'inv': 'Inverted',
|
||||
'mag_stat': 'Magnetometer Status',
|
||||
'mag_field': 'Magnetometer Reading',
|
||||
}
|
||||
|
||||
|
||||
# This returns a dictionary defining the human readable column headers (Value) corresponding to the short forms (key)
|
||||
# It has many similarities to the options above, but there is not a perfect correspondence between columns and logging
|
||||
# options. This is both in terms of the user friendly name, which can differ, and the options.
|
||||
def get_long_column_header():
|
||||
header = {
|
||||
'date': 'Date',
|
||||
'time': 'Timestamp',
|
||||
't': 'Time',
|
||||
'arduino_stat': 'Arduino Status',
|
||||
'xy_psu_stat': 'XY PSU Status',
|
||||
'z_psu_stat': 'Z PSU Status',
|
||||
'mag_stat': 'Magnetometer Status',
|
||||
}
|
||||
|
||||
for i in ['x', 'y', 'z']:
|
||||
i_caps = i.upper()
|
||||
header[f'{i}_v'] = f'{i_caps}-Axis Voltage Setpoint'
|
||||
header[f'{i}_v_actual'] = f'{i_caps}-Axis Actual Voltage'
|
||||
header[f'{i}_i'] = f'{i_caps}-Axis Current Setpoint'
|
||||
header[f'{i}_i_actual'] = f'{i_caps}-Axis Actual Current'
|
||||
header[f'{i}_tgt_field'] = f'{i_caps}-Axis Target Field'
|
||||
header[f'{i}_tgt_field_raw'] = f'{i_caps}-Axis Target Field Raw'
|
||||
header[f'{i}_tgt_field_i'] = f'{i_caps}-Axis Target Current'
|
||||
header[f'{i}_inv'] = f'{i_caps}-Axis Inverted'
|
||||
header[f'{i}_mag_field'] = f'Magnetometer Reading {i_caps}'
|
||||
|
||||
return header
|
||||
|
||||
|
||||
def log_datapoint(settings):
|
||||
"""Logs a single row of data into the log_data list"""
|
||||
# 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
|
||||
|
||||
# --- PRODUCE NEW DATA ROW ---
|
||||
# d for 'data'
|
||||
d = {}
|
||||
|
||||
# Timestamp
|
||||
if settings['timestamp']:
|
||||
d['date'] = datetime.now().date() # get current date
|
||||
d['time'] = datetime.now().strftime("%H:%M:%S.%f") # get string with current time in correct format
|
||||
d['t'] = (datetime.now() - zero_time).total_seconds() # calculate timestamp relative to the start of the logging
|
||||
|
||||
# PSU + Arduino status
|
||||
if settings['device_stat']:
|
||||
d['arduino_stat'] = g.CAGE_DEVICE.arduino is not None
|
||||
d['xy_psu_stat'] = g.CAGE_DEVICE.psu1 is not None
|
||||
d['z_psu_stat'] = g.CAGE_DEVICE.psu2 is not None
|
||||
|
||||
# Axis data
|
||||
axes = []
|
||||
if settings['x_axis']:
|
||||
axes.append(0)
|
||||
if settings['y_axis']:
|
||||
axes.append(1)
|
||||
if settings['z_axis']:
|
||||
axes.append(2)
|
||||
for i in axes:
|
||||
pre = ['x_', 'y_', 'z_'][i]
|
||||
axis = g.CAGE_DEVICE.axes[i]
|
||||
|
||||
# Cached PSU state data
|
||||
if axis.psu:
|
||||
psu_state = axis.psu.cached_channel_state(axis.channel)
|
||||
else:
|
||||
psu_state = {'voltage_setpoint': None,
|
||||
'voltage': None,
|
||||
'current_setpoint': None,
|
||||
'current': None}
|
||||
|
||||
if settings['v']:
|
||||
d[pre + 'v'] = psu_state['voltage_setpoint']
|
||||
if settings['v_actual']:
|
||||
d[pre + 'v_actual'] = psu_state['voltage']
|
||||
if settings['i']:
|
||||
d[pre + 'i'] = psu_state['current_setpoint']
|
||||
if settings['i_actual']:
|
||||
d[pre + 'i_actual'] = psu_state['current']
|
||||
# All the rest
|
||||
if settings['tgt_field']:
|
||||
d[pre + 'tgt_field'] = axis.target_field
|
||||
if settings['tgt_field_raw']:
|
||||
d[pre + 'tgt_field_raw'] = axis.target_field_raw
|
||||
if settings['tgt_field_i']:
|
||||
d[pre + 'tgt_field_i'] = axis.target_current
|
||||
if settings['inv']:
|
||||
d[pre + 'inv'] = axis.polarity
|
||||
|
||||
# Mangetometer field
|
||||
if settings['mag_field']:
|
||||
for i in [0, 1, 2]:
|
||||
pre = ['x_', 'y_', 'z_'][i]
|
||||
d[pre + 'mag_field'] = g.MAGNETOMETER.field[i]
|
||||
|
||||
# Magnetometer status
|
||||
if settings['mag_stat']:
|
||||
d['mag_stat'] = g.MAGNETOMETER.connected
|
||||
|
||||
# Save the datapoint
|
||||
log_data.append(d) # append the new data to the current 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_print("No file selected, can't save logged data.")
|
||||
return None
|
||||
else: # a valid file name was entered
|
||||
return filepath
|
||||
|
||||
|
||||
def write_to_file(filepath):
|
||||
# get global variables for use in this function:
|
||||
global unsaved_data
|
||||
global log_data
|
||||
if filepath is not None: # user has selected a file and no errors occurred
|
||||
ui_print("Writing logged data to file", filepath)
|
||||
try:
|
||||
# write data collected in log_data list to csv file in excel format:
|
||||
with open(filepath, 'w', newline='') as f:
|
||||
writer = csv.writer(f)
|
||||
headers = log_data[0].keys()
|
||||
long_headers = [get_long_column_header()[header] for header in headers]
|
||||
writer.writerow(long_headers)
|
||||
writer.writerow(headers)
|
||||
for row in log_data:
|
||||
writer.writerow(row.values())
|
||||
except PermissionError:
|
||||
message = "No permission to write to: \n%s. \nFile may be open in another program." % filepath
|
||||
messagebox.showerror("Permission Error", message)
|
||||
except Exception 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_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 = [] # reset to an empty list, i.e. clearing all logged data
|
||||
@@ -0,0 +1,197 @@
|
||||
# This 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 time
|
||||
from io import StringIO
|
||||
|
||||
import pandas
|
||||
import numpy as np
|
||||
from threading import *
|
||||
from tkinter import messagebox
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from src.exceptions import DeviceBusy, DeviceAccessError
|
||||
from src.utility import ui_print
|
||||
import src.user_interface as ui
|
||||
import src.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
|
||||
|
||||
# Acquire cage device. This resource will only be released after the thread is ended.
|
||||
try:
|
||||
self.cage_dev = g.CAGE_DEVICE.request_proxy()
|
||||
except DeviceBusy:
|
||||
raise DeviceAccessError("Failed to acquire coil control. Required for ambient field calibration.")
|
||||
|
||||
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_print("\nStarting Sequence Execution...")
|
||||
try:
|
||||
self.execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence
|
||||
finally:
|
||||
self.cage_dev.idle() # set currents and voltages to 0, set arduino pins to low
|
||||
# Release the proxy so other components can use it
|
||||
self.cage_dev.close()
|
||||
|
||||
# when the sequence has ended, reset buttons on the UI:
|
||||
if not g.exit_flag: # 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()
|
||||
|
||||
@property
|
||||
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 bench accordingly
|
||||
|
||||
# array format: [time (s), xField (T), yField (T), zField (T)]
|
||||
self.cage_dev.idle() # 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 = (parent.xy_override.get() or g.CAGE_DEVICE.psu1 is not None) and\
|
||||
(parent.z_override.get() or g.CAGE_DEVICE.psu2 is not None) and\
|
||||
(parent.arduino_override.get() or g.CAGE_DEVICE.arduino is not None)
|
||||
# True or False depending on devices status, checks for some devices may be overridden by user
|
||||
if not all_connected:
|
||||
ui_print("Required devices are not present, sequence aborted.")
|
||||
messagebox.showwarning("Device Error!", "Required devices are not present, sequence aborted.")
|
||||
return
|
||||
|
||||
i = 0 # index of the current array row
|
||||
while i < len(array):
|
||||
if self.stopped or g.exit_flag:
|
||||
# Interrupt sequence
|
||||
ui_print("Sequence interrupted, powering down channels.")
|
||||
# Channels powered down in run function
|
||||
return
|
||||
|
||||
# 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
|
||||
target_t = array[i, 0] # Target execution time of data point
|
||||
if t >= target_t: # time for this row has come
|
||||
field_vec = array[i, 1:4] # extract desired field vector
|
||||
ui_print("[{:5.3f}s] B=[{:.1f}, {:.1f}, {:.1f}]\u03BCT for t={:.2f}s".format(t,
|
||||
field_vec[0] * 1e6,
|
||||
field_vec[1] * 1e6,
|
||||
field_vec[2] * 1e6,
|
||||
target_t))
|
||||
self.cage_dev.set_field_compensated(field_vec) # send field vector to test bench
|
||||
|
||||
# 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 bench is commanded
|
||||
logger.log_datapoint() # log data
|
||||
|
||||
i = i + 1 # next row
|
||||
|
||||
elif t <= target_t - delay - 0.02: # is there enough time to sleep before the next row?
|
||||
time.sleep(delay) # sleep to give other threads time to run
|
||||
|
||||
ui_print("Sequence executed, powering down channels.")
|
||||
|
||||
|
||||
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 or period commas. Do not use these characters as a thousands seperator!
|
||||
with open(filepath, 'r') as csv_file:
|
||||
# Normalize seperators
|
||||
csv_string = csv_file.read().replace(',', '.')
|
||||
# read csv file without column headers
|
||||
file = pandas.read_csv(StringIO(csv_string), sep=';', decimal='.', header=0)
|
||||
array = file.to_numpy() # convert csv to array
|
||||
return array
|
||||
|
||||
|
||||
def check_array_ok(array):
|
||||
"""Checks if values are within limits, and if not shows a warning message."""
|
||||
# check if any magnetic fields in an array exceed the test bench limits
|
||||
warnings = []
|
||||
for i in [0, 1, 2]: # go through axes/columns
|
||||
# get limits the test bench can do
|
||||
min_val, max_val = g.CAGE_DEVICE.axes[i].max_comp_field
|
||||
for row_idx in range(array.shape[0]):
|
||||
data_point = array[row_idx, i + 1] # extract data for this axis from array
|
||||
if data_point > max_val or data_point < min_val:
|
||||
# Out of bounds
|
||||
warnings.append({'row': row_idx+1, 'axis': g.AXIS_NAMES[i]})
|
||||
|
||||
# show warning pop-up if values are exceeding limits
|
||||
nr_warnings = len(warnings)
|
||||
if nr_warnings > 0:
|
||||
warning_msg = "Found field values exceeding limits of test bench.\n"
|
||||
# Only print the first three warnings
|
||||
for i in range(min(nr_warnings, 3)):
|
||||
warning_msg += "[Line {}] {} exceeds limits.\n".format(warnings[i]['row'], warnings[i]['axis'])
|
||||
if nr_warnings > 3:
|
||||
warning_msg += "And {} more...".format(nr_warnings - 3)
|
||||
# Show all warnings collectively
|
||||
messagebox.showwarning("Value Limits Warning!", warning_msg)
|
||||
|
||||
|
||||
def plot_field_sequence(array, width, height): # create plot of fixed size (pixels) from array
|
||||
# ToDo (optional): polar plots, plots of angle...
|
||||
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
|
||||
|
||||
# modify data to show instantaneous jumps in field to reflect test bench operation
|
||||
new_array = np.array([[0, 0, 0, 0]], dtype=float) # initialize modified array, zeros to show start from no fields
|
||||
|
||||
last_vals = [0, 0, 0] # [x,y,z] field values from last data point (zero here), used to create step in data
|
||||
for row in array[:, 0:4]: # go through each row in the original array
|
||||
# create extra datapoint at current timestamp, with field values from last to create a "step" in the plot:
|
||||
new_array = np.append(new_array, [[row[0], *last_vals]], axis=0)
|
||||
new_array = np.append(new_array, [row], axis=0) # add actual datapoint for current timestamp
|
||||
last_vals = row[1:4] # save values from current timestamp for next
|
||||
new_array = np.append(new_array, [[new_array[-1, 0], 0, 0, 0]], axis=0) # append last datapoint with 0 fields
|
||||
|
||||
# extract data and plot:
|
||||
t = new_array[:, 0] # extract time column
|
||||
for i in [0, 1, 2]: # go through all three axes
|
||||
data = new_array[:, i + 1] * 1e6 # extract field column of this axis and convert to microtesla
|
||||
min_val, max_val = g.CAGE_DEVICE.axes[i].max_comp_field * 1e6 # get limits of achievable field
|
||||
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
|
||||
@@ -0,0 +1,18 @@
|
||||
class ProxyNotOwnedException(Exception):
|
||||
"""Should not occur in correct operation. For whatever reason, this means the proxy was invalidated."""
|
||||
pass
|
||||
|
||||
|
||||
class DeviceAccessError(Exception):
|
||||
"""General error indicating that HW access failed."""
|
||||
pass
|
||||
|
||||
|
||||
class DeviceBusy(DeviceAccessError):
|
||||
"""Error thrown when the HW proxy (i.e. access) cannot be acquired"""
|
||||
pass
|
||||
|
||||
|
||||
class MagFieldOutOfBounds(Exception):
|
||||
"""Set magnetic field must be a positive number between 0 and 200µT!"""
|
||||
pass
|
||||
@@ -3,20 +3,17 @@
|
||||
|
||||
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)
|
||||
# Main Tkinter application object will be stored here (class HelmholtzGUI)
|
||||
app = None
|
||||
|
||||
# Axis objects will be stored here (class Axis)
|
||||
X_AXIS = None
|
||||
Y_AXIS = None
|
||||
Z_AXIS = None
|
||||
# The main access point for all hardware commands
|
||||
CAGE_DEVICE = None
|
||||
|
||||
AXES = None # list containing [X_AXIS, Y_AXIS, Z_AXIS]
|
||||
# Magnetometer proxy object providing access to mag. data from an external client per tcp interface
|
||||
MAGNETOMETER = None
|
||||
|
||||
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
|
||||
# list with the names of each axis, used mainly for printing functions
|
||||
AXIS_NAMES = ["X-Axis", "Y-Axis", "Z-Axis"]
|
||||
|
||||
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)
|
||||
@@ -25,7 +22,7 @@ global PORTS # list containing [XY_PORT, XY_PORT, Z_PORT], used in loops where
|
||||
|
||||
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
|
||||
exit_flag = 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
|
||||
@@ -35,15 +32,24 @@ exitFlag = True # False when main window is open, True otherwise
|
||||
# Dictionary for numerical values:
|
||||
# format: key: [default values], [maximum values], [minimum values]
|
||||
default_arrays = {
|
||||
"coil_const": np.array([[38.957, 40.408, 37.754], [50, 50, 50], [0, 0, 0]]) * 1e-6, # Coil constants [x,y,z] [T/A]
|
||||
"coil_const": np.array([[38.83, 38.65, 37.30], [50, 50, 50], [0, 0, 0]]) * 1e-6, # Coil constants [x,y,z] [T/A]
|
||||
"ambient_field": np.array([[0, 0, 0], [200, 200, 200], [-200, -200, -200]]) * 1e-6, # ambient magnetic field [T]
|
||||
"resistance": np.array([[3.131, 3.107, 3.129], [5, 5, 5], [1, 1, 1]], dtype=float), # resistance of circuits [Ohm]
|
||||
"max_volts": np.array([[15, 15, 15], [16, 16, 16], [0, 0, 0]], dtype=float), # max. voltage, limited to 16V by used diodes! [V]
|
||||
"max_amps": np.array([[5, 5, 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
|
||||
# Dictionary for PSU configuration:
|
||||
default_psu_config = {
|
||||
"supply_model": "ql355tp",
|
||||
'arduino_port': "COM",
|
||||
"xy_port": "COM", # Default serial port where PSU for X- and Y-Axes is connected
|
||||
"z_port": "COM", # Default serial port where PSU for Z-Axis is connected
|
||||
}
|
||||
|
||||
# Configuration for socket interface
|
||||
SOCKET_PORT = 6677
|
||||
SOCKET_MAX_CONNECTIONS = 5
|
||||
|
||||
# Hardware safety limits
|
||||
MAG_MAG_FIELD = 5*37*1e-6 # [µT=A*µT/A] min coil constant x PSU current limit
|
||||
@@ -0,0 +1,607 @@
|
||||
import time
|
||||
import traceback
|
||||
from threading import RLock, Thread, Event
|
||||
from tkinter import messagebox
|
||||
import numpy as np
|
||||
|
||||
from src.arduino_device import ArduinoDevice
|
||||
from src.psu_device import PSUDevicePS2000B, PSUDeviceQL355TP
|
||||
from src.utility import ui_print
|
||||
from src.exceptions import DeviceBusy, ProxyNotOwnedException
|
||||
import src.config_handling as config_handling
|
||||
import src.globals as g
|
||||
|
||||
|
||||
class HelmholtzCageDevice:
|
||||
"""This is the central object for controlling all the test bench related HW. This way, access can be
|
||||
synchronized and exclusive to a single controller at once. This device always exists, irrespective of
|
||||
which devices are actually connected. Only the request_proxy, shutdown and destroy methods should be used !!!
|
||||
Provides subscriber interface for periodic status information.
|
||||
Provides proxy model to control access."""
|
||||
|
||||
# This class is thread safe
|
||||
|
||||
POLLING_INTERVAL = 1 # Seconds between polling the device state
|
||||
|
||||
def __init__(self):
|
||||
# Indicates all the threads should be joined
|
||||
self._stop_flag = Event()
|
||||
|
||||
# --- POLLING SUBSCRIBERS ---
|
||||
# This is a list of object callbacks interested in receiving device status updates.
|
||||
# This will primarily include the front-end which wants to update its display data.
|
||||
# The callback functions should accept a dict as an argument of the form {'arduino':, 'axes':[{}, {}, {}]}
|
||||
self._subscribers = []
|
||||
|
||||
# --- COMMAND QUEUEING ---
|
||||
self.command_lock = RLock()
|
||||
# Indicates to the command executing thread that a new command has arrived for execution
|
||||
self.new_command_flag = Event()
|
||||
# Contains the next command to be executed
|
||||
self.command = None
|
||||
|
||||
# --- PROXY MODEL SETUP ---
|
||||
# Locks all proxy requesting and releasing access
|
||||
self.proxy_lock = RLock()
|
||||
# Contains the id of the current in control proxy
|
||||
self.proxy_id = None
|
||||
# Contains the current context manager proxy. All other proxy instances are managed externally
|
||||
self.context_manager_proxy = None
|
||||
|
||||
# --- STATUS VARIABLES ---
|
||||
# These are used to store information about our last command to provide status updates asynchronously
|
||||
self.target_field = [0, 0, 0]
|
||||
self.target_field_raw = [0, 0, 0]
|
||||
self.target_current = [0, 0, 0]
|
||||
|
||||
# --- DEVICE SETUP ---
|
||||
# Must be acquired to access any hardware
|
||||
self.hardware_lock = RLock()
|
||||
|
||||
# Com ports
|
||||
self.com_port_psu1 = None
|
||||
self.com_port_psu2 = None
|
||||
self.com_port_arduino = None
|
||||
|
||||
# PSU object used
|
||||
self.psu_type = None
|
||||
|
||||
# Hardware object variables
|
||||
self.arduino = None
|
||||
self.psu1 = None
|
||||
self.psu2 = None
|
||||
|
||||
# --- AXIS CONFIGURATION ---
|
||||
# This is also hardware related, but in a separate section to keep it clean.
|
||||
# Get all settings from the config file
|
||||
self.axes = []
|
||||
# Loop over axes
|
||||
for i in range(3):
|
||||
# The axes talks to the HW objects (Arduino, PSU) referenced in this object
|
||||
self.axes.append(Axis(i, self))
|
||||
|
||||
# --- Preliminary PSU type info ---
|
||||
# Required post-initialization but before hardware has been connected
|
||||
psu_type_string = config_handling.read_from_config("Supplies", "supply_model",
|
||||
config_handling.CONFIG_OBJECT)
|
||||
if psu_type_string == "ps2000b":
|
||||
self.psu_type = PSUDevicePS2000B
|
||||
elif psu_type_string == "ql355tp":
|
||||
self.psu_type = PSUDeviceQL355TP
|
||||
else:
|
||||
raise Exception("Invalid psu model:\n{}".format(self.psu_type))
|
||||
|
||||
# --- HW COMMUNICATION THREAD ---
|
||||
self._cmd_exec_thread = Thread(target=self._cmd_exec_thread_method)
|
||||
self._cmd_exec_thread.start()
|
||||
|
||||
self._hw_poll_thread = Thread(target=self._hw_poll_thread_method)
|
||||
self._hw_poll_thread.start()
|
||||
|
||||
# TODO: Move to proxy
|
||||
def reconnect_hardware(self):
|
||||
with self.hardware_lock:
|
||||
self.shutdown()
|
||||
self.connect_hardware()
|
||||
|
||||
# TODO: Move to proxy
|
||||
def connect_hardware(self):
|
||||
"""Connects devices. Does not check if they are already connected: Remember to call shutdown first"""
|
||||
with self.hardware_lock:
|
||||
# All devices are usable if the object exists. None indicates
|
||||
# the device is not connected/not working properly
|
||||
|
||||
# Fetch config variables
|
||||
self.com_port_psu1 = config_handling.read_from_config("Supplies", "xy_port",
|
||||
config_handling.CONFIG_OBJECT)
|
||||
self.com_port_psu2 = config_handling.read_from_config("Supplies", "z_port",
|
||||
config_handling.CONFIG_OBJECT)
|
||||
self.com_port_arduino = config_handling.read_from_config("Supplies", "arduino_port",
|
||||
config_handling.CONFIG_OBJECT)
|
||||
psu_type_string = config_handling.read_from_config("Supplies", "supply_model",
|
||||
config_handling.CONFIG_OBJECT)
|
||||
|
||||
# Arduino setup
|
||||
try:
|
||||
self.arduino = ArduinoDevice(self.com_port_arduino)
|
||||
ui_print("Connected to Arduino on port {}".format(self.com_port_arduino))
|
||||
except Exception as e:
|
||||
self.arduino = None
|
||||
# show error messages to alert user
|
||||
ui_print("Error connecting to Arduino:\n{}".format(e))
|
||||
|
||||
# PSU setup
|
||||
if psu_type_string == "ps2000b":
|
||||
self.psu_type = PSUDevicePS2000B
|
||||
elif psu_type_string == "ql355tp":
|
||||
self.psu_type = PSUDeviceQL355TP
|
||||
else:
|
||||
raise Exception("Invalid psu model:\n{}".format(self.psu_type))
|
||||
|
||||
# psu1: controls xy axis
|
||||
try:
|
||||
self.psu1 = self.psu_type(self.com_port_psu1)
|
||||
ui_print("Connected to PSU 1 on port {}".format(self.com_port_psu1))
|
||||
except Exception as e:
|
||||
self.psu1 = None
|
||||
ui_print("Error connecting to PSU 1:\n{}".format(e))
|
||||
|
||||
# psu2: controls z axis
|
||||
try:
|
||||
self.psu2 = self.psu_type(self.com_port_psu2)
|
||||
ui_print("Connected to PSU 2 on port {}".format(self.com_port_psu2))
|
||||
except Exception as e:
|
||||
self.psu2 = None
|
||||
ui_print("Error connecting to PSU 2:\n{}".format(e))
|
||||
|
||||
# The axes may not be deleted, so a special method for reinitialization is provided.
|
||||
for axis in self.axes:
|
||||
axis.reload_config()
|
||||
|
||||
# Zero and activate channels. This is a sort of "armed" state so that we can send commands later
|
||||
self.idle()
|
||||
|
||||
def reconnect_hardware_async(self):
|
||||
"""Disconnects and reconnects devices in a non-blocking call.
|
||||
Acquires hardware lock and blocks other cage operations."""
|
||||
connect_hardware_thread = Thread(target=self.reconnect_hardware)
|
||||
connect_hardware_thread.start()
|
||||
|
||||
def connect_hardware_async(self):
|
||||
"""Connects devices in a non-blocking call.
|
||||
Acquires hardware lock and blocks other cage operations."""
|
||||
connect_hardware_thread = Thread(target=self.connect_hardware)
|
||||
connect_hardware_thread.start()
|
||||
|
||||
def idle(self):
|
||||
""" Zero and activate channels """
|
||||
if self.psu1 is not None:
|
||||
self.psu1.idle()
|
||||
if self.psu2 is not None:
|
||||
self.psu2.idle()
|
||||
if self.arduino is not None:
|
||||
self.arduino.idle()
|
||||
# Since these actions are not handled by the axes objects, also make sure to update their target field status
|
||||
for axis in self.axes:
|
||||
axis.target_current = 0
|
||||
|
||||
def request_proxy(self):
|
||||
"""Returns a new HelmholtzCageProxy or None, depending on if access is available"""
|
||||
with self.proxy_lock:
|
||||
if not self.proxy_id:
|
||||
# The interface is available, return a new proxy object
|
||||
new_proxy = HelmholtzCageProxy(self)
|
||||
self.proxy_id = id(new_proxy)
|
||||
return new_proxy
|
||||
else:
|
||||
# The interface is occupied, the caller must tolerate that the request failed.
|
||||
raise DeviceBusy
|
||||
|
||||
def __enter__(self):
|
||||
"""Enables: with g.CAGE_DEVICE as dev:"""
|
||||
self.context_manager_proxy = self.request_proxy()
|
||||
return self.context_manager_proxy
|
||||
|
||||
def release_proxy(self, proxy_obj):
|
||||
"""Releases the proxy to free access for other controllers. Should only be called when proxy is destroyed"""
|
||||
if self.proxy_valid(proxy_obj):
|
||||
# This only frees the interface if it really was the active proxy
|
||||
self.proxy_id = None
|
||||
# Otherwise do nothing, this case requires no behaviour
|
||||
|
||||
def __exit__(self, *args):
|
||||
"""Enables: with g.CAGE_DEVICE as dev:"""
|
||||
self.release_proxy(self.context_manager_proxy)
|
||||
|
||||
def proxy_valid(self, proxy_obj):
|
||||
"""Returns True if the proxy currently owns the device."""
|
||||
with self.proxy_lock:
|
||||
return id(proxy_obj) == self.proxy_id
|
||||
|
||||
def subscribe_status_updates(self, callback):
|
||||
# List containing all interested subscribers.
|
||||
# We won't check if a callback is added twice. Not our responsibility
|
||||
self._subscribers.append(callback)
|
||||
|
||||
def queue_command(self, proxy_obj, command):
|
||||
""" Queues a dict for immediate execution containing the command for the cage as a whole.
|
||||
Since the newest command should always be run, it is not a real queue (just a variable)"""
|
||||
|
||||
with self.proxy_lock:
|
||||
if id(proxy_obj) != self.proxy_id:
|
||||
raise ProxyNotOwnedException()
|
||||
|
||||
with self.command_lock:
|
||||
# Overwrite any command that was queued but not yet executed. We now only care about the newer command
|
||||
self.command = command
|
||||
self.new_command_flag.set()
|
||||
|
||||
def _cmd_exec_thread_method(self):
|
||||
"""This method forms the main thread for hardware command execution."""
|
||||
while not self._stop_flag.is_set():
|
||||
self.new_command_flag.wait()
|
||||
self.new_command_flag.clear()
|
||||
# Avoid blocking the buffer while we are executing.
|
||||
with self.command_lock:
|
||||
# Dicts and lists are mutable so must be (deep)copied
|
||||
command_buffer = HelmholtzCageDevice._copy_command(self.command)
|
||||
self.command = None # Processed commands are removed from "buffer"
|
||||
if command_buffer:
|
||||
# Unpack command into "action" and argument
|
||||
command_string = command_buffer['command']
|
||||
command_arg = command_buffer['arg']
|
||||
# Check which command and delegate to responsible function
|
||||
if command_string == "set_signed_currents":
|
||||
self._set_signed_currents(command_arg)
|
||||
elif command_string == "set_field_raw":
|
||||
self._set_field_raw(command_arg)
|
||||
elif command_string == "set_field_compensated":
|
||||
self._set_field_compensated(command_arg)
|
||||
elif command_string == 'idle':
|
||||
self.idle()
|
||||
else:
|
||||
raise Exception("Command unknown!")
|
||||
|
||||
def _hw_poll_thread_method(self):
|
||||
"""This method forms the main thread for hardware command execution."""
|
||||
while True:
|
||||
# We will have to check if we passed this statement due to a stop flag or due to the polling interval
|
||||
stop_flag_set = self._stop_flag.wait(timeout=self.POLLING_INTERVAL)
|
||||
if stop_flag_set:
|
||||
return
|
||||
|
||||
ard_conn = self.arduino is not None
|
||||
status_data = {'axes': [],
|
||||
'arduino_connected': ard_conn}
|
||||
with self.hardware_lock:
|
||||
# This polls all three axes at once
|
||||
for axis in self.axes:
|
||||
status_data['axes'].append(axis.get_status_dict())
|
||||
|
||||
# Distribute status data to all interested subscribers
|
||||
for subscriber in self._subscribers:
|
||||
subscriber(status_data)
|
||||
|
||||
@staticmethod
|
||||
def _copy_command(command):
|
||||
# PyCharm has an issue with the deepcopy tool, so just handle the copying manually.
|
||||
if command is None:
|
||||
return command
|
||||
try:
|
||||
return {'command': command['command'],
|
||||
'arg': command['arg'].copy()}
|
||||
except AttributeError:
|
||||
# AttributeError: '---' object has no attribute 'copy'
|
||||
# Should be immutable. Otherwise we have a problem
|
||||
return {'command': command['command'],
|
||||
'arg': command['arg']}
|
||||
|
||||
def _set_field_raw(self, arg):
|
||||
for axis, field in zip(self.axes, arg):
|
||||
with self.hardware_lock:
|
||||
axis.set_field_raw(field)
|
||||
|
||||
def _set_field_compensated(self, arg):
|
||||
for axis, field in zip(self.axes, arg):
|
||||
with self.hardware_lock:
|
||||
axis.set_field_compensated(field)
|
||||
|
||||
def _set_signed_currents(self, arg):
|
||||
"""Sets the currents in the array arg in the respective coils x->y->z.
|
||||
This function imposes safety limits by clamping the current when beyond the maximum."""
|
||||
|
||||
# One pass for every axis
|
||||
for axis, current in zip(self.axes, arg):
|
||||
# Talk to hardware
|
||||
with self.hardware_lock:
|
||||
try:
|
||||
axis.set_signed_current(current)
|
||||
except Exception as e:
|
||||
ui_print("Error {}: Unexpected error occured:\n{}".format(axis.name, e))
|
||||
traceback.print_exc()
|
||||
|
||||
def get_psu_for_axis(self, axis_index):
|
||||
"""Determine which channel of which psu is required"""
|
||||
# TODO: This kind of stuff belongs in the config and should not be hardcoded
|
||||
if axis_index == 0 or axis_index == 1:
|
||||
psu = self.psu1
|
||||
channel = self.psu_type.valid_channels()[axis_index]
|
||||
port = self.com_port_psu1
|
||||
else:
|
||||
psu = self.psu2
|
||||
channel = self.psu_type.valid_channels()[0]
|
||||
port = self.com_port_psu2
|
||||
return psu, channel, port
|
||||
|
||||
def destroy(self):
|
||||
"""The object cannot be recovered after calling destroy"""
|
||||
|
||||
# Send signals to kill threads:
|
||||
# TODO: Handle timeout behaviour
|
||||
self._stop_flag.set()
|
||||
# _cmd_exec_thread:
|
||||
with self.command_lock:
|
||||
self.command = None
|
||||
self.new_command_flag.set() # Causes the thread to unblock
|
||||
self._cmd_exec_thread.join(timeout=2)
|
||||
# _hw_poll_thread:
|
||||
# This thread is stopped just by setting the _stop_flag
|
||||
self._hw_poll_thread.join(timeout=2)
|
||||
|
||||
# Shutdown the hardware
|
||||
msg = self.shutdown()
|
||||
|
||||
messagebox.showinfo("Program ended",
|
||||
msg) # Show a unified pop-up with how the shutdown on each device went
|
||||
|
||||
def shutdown(self):
|
||||
""" Shuts down the hardware. This special command overrides the currently active proxy."""
|
||||
|
||||
# This waiting period is not easily removed without resulting in unexpected behaviour
|
||||
with self.hardware_lock:
|
||||
ui_print("\nAttempting to safely shut down all devices. Check equipment to confirm.")
|
||||
# start writing string to later show how shutdown on all devices went in a single info pop-up:
|
||||
message = "Tried to shut down all devices. Check equipment to confirm."
|
||||
|
||||
# Shutdown XY PSU
|
||||
if self.psu1 is not None:
|
||||
try:
|
||||
self.psu1.shutdown()
|
||||
self.psu1.destroy()
|
||||
self.psu1 = None
|
||||
except Exception as e:
|
||||
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."
|
||||
|
||||
# Shutdown Z PSU
|
||||
if self.psu2 is not None:
|
||||
try:
|
||||
self.psu2.shutdown()
|
||||
self.psu2.destroy()
|
||||
self.psu2 = None
|
||||
except Exception as e:
|
||||
ui_print("Error while deactivating Z PSU:", e) # print the problem in the console
|
||||
message += "\nError while deactivating Z PSU: %s" % e # append status to the message to show later
|
||||
else: # device was successfully deactivated
|
||||
ui_print("Z PSU deactivated.")
|
||||
message += "\nZ 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("Z PSU not connected, can't deactivate.")
|
||||
message += "\nZ PSU not connected, can't deactivate."
|
||||
|
||||
# Shut down arduino:
|
||||
if self.arduino is not None:
|
||||
try:
|
||||
self.arduino.shutdown()
|
||||
self.arduino.close()
|
||||
self.arduino = None
|
||||
except Exception as e:
|
||||
ui_print("Error while deactivating Arduino:", e) # print the problem in the console
|
||||
message += "\nError while deactivating Arduino: %s" % e # append status to the message to show later
|
||||
else: # device was successfully deactivated
|
||||
ui_print("Arduino deactivated.")
|
||||
message += "\nArduino 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("Arduino not connected, can't deactivate.")
|
||||
message += "\nArduino not connected, can't deactivate."
|
||||
|
||||
return message
|
||||
|
||||
|
||||
class Axis:
|
||||
def __init__(self, axis_idx, cage_dev):
|
||||
"""
|
||||
This class is an adapter to axis-specific and non-specific (e.g. psu status) information. Most attributes
|
||||
are supplied as properties, which query the parent component of that attribute/info. It also wraps device calls.
|
||||
"""
|
||||
|
||||
self.idx = axis_idx
|
||||
self.cage_dev = cage_dev
|
||||
self.name = g.AXIS_NAMES[axis_idx] # Also used in some indexing operations, like in the config file
|
||||
|
||||
# Create other axis properties using config parameters
|
||||
self.coil_const = None
|
||||
self.ambient_field = None
|
||||
self.resistance = None
|
||||
self.max_volts = None
|
||||
self.max_amps = None
|
||||
self.reload_config()
|
||||
|
||||
# State variables
|
||||
self.target_current = 0
|
||||
self.polarity = False
|
||||
|
||||
def reload_config(self):
|
||||
self.coil_const = float(config_handling.read_from_config(self.name, "coil_const", config_handling.CONFIG_OBJECT))
|
||||
self.ambient_field = float(config_handling.read_from_config(self.name, "ambient_field", config_handling.CONFIG_OBJECT))
|
||||
self.resistance = float(config_handling.read_from_config(self.name, "resistance", config_handling.CONFIG_OBJECT))
|
||||
self.max_volts = float(config_handling.read_from_config(self.name, "max_volts", config_handling.CONFIG_OBJECT))
|
||||
self.max_amps = float(config_handling.read_from_config(self.name, "max_amps", config_handling.CONFIG_OBJECT))
|
||||
|
||||
def set_field_raw(self, field):
|
||||
self.set_signed_current(field / self.coil_const)
|
||||
|
||||
def set_field_compensated(self, field):
|
||||
self.set_field_raw(field - self.ambient_field)
|
||||
|
||||
def set_signed_current(self, current):
|
||||
"""Sets current on axis"""
|
||||
|
||||
# Check current limits
|
||||
if abs(current) <= self.max_amps:
|
||||
safe_current = current
|
||||
else:
|
||||
safe_current = self.max_amps
|
||||
ui_print("Warning {}: Attempted to exceed current limit".format(self.name))
|
||||
|
||||
# Update state variables to be queried. This should be set even if it is only a "virtual" action with no
|
||||
# connected devices.
|
||||
self.target_current = safe_current
|
||||
|
||||
if not self.arduino:
|
||||
ui_print("Warning {}: Cannot set field/current without Arduino".format(self.name))
|
||||
return
|
||||
|
||||
if not self.psu:
|
||||
ui_print("Warning {}: Cannot set field/current without PSU".format(self.name))
|
||||
return
|
||||
|
||||
# TODO: Check for exceptions
|
||||
# Set polarity on Arduino
|
||||
if safe_current < 0:
|
||||
# Reverse polarity
|
||||
self.polarity = True # Track the state
|
||||
self.arduino.set_axis_polarity(self.idx, True)
|
||||
else:
|
||||
# Positive polarity (default case)
|
||||
self.polarity = False # Track the state
|
||||
self.arduino.set_axis_polarity(self.idx, False)
|
||||
|
||||
# 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)
|
||||
voltage_limit = min(max(1.3 * abs(safe_current) * self.resistance, 8), self.max_volts) # limit voltage
|
||||
|
||||
# Set voltages and currents. Outputs should already be active from initializer.
|
||||
self.psu.set_current(self.channel, abs(safe_current))
|
||||
self.psu.set_voltage(self.channel, voltage_limit)
|
||||
|
||||
@property
|
||||
def arduino(self):
|
||||
return self.cage_dev.arduino
|
||||
|
||||
@property
|
||||
def com_port(self):
|
||||
_, _, port = self.cage_dev.get_psu_for_axis(self.idx)
|
||||
return port
|
||||
|
||||
@property
|
||||
def psu(self):
|
||||
psu, _, _ = self.cage_dev.get_psu_for_axis(self.idx)
|
||||
return psu
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
_, channel, _ = self.cage_dev.get_psu_for_axis(self.idx)
|
||||
return channel
|
||||
|
||||
@property
|
||||
def target_field(self):
|
||||
return self.target_current * self.coil_const + self.ambient_field
|
||||
|
||||
@property
|
||||
def target_field_raw(self):
|
||||
return self.target_current * self.coil_const
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
return self.psu is not None and self.arduino is not None
|
||||
|
||||
@property
|
||||
def max_field(self):
|
||||
max_field_magnitude = self.max_amps * self.coil_const
|
||||
return np.array([-max_field_magnitude, max_field_magnitude])
|
||||
|
||||
@property
|
||||
def max_comp_field(self):
|
||||
max_field_magnitude = self.max_amps * self.coil_const
|
||||
return np.array([self.ambient_field - max_field_magnitude, self.ambient_field + max_field_magnitude])
|
||||
|
||||
@property
|
||||
def arduino_connected(self):
|
||||
return self.arduino is not None
|
||||
|
||||
@property
|
||||
def psu_connected(self):
|
||||
return self.psu is not None
|
||||
|
||||
def get_status_dict(self):
|
||||
"""Dict containing all data from this model to pass to the front-end. Some data is only available through
|
||||
this interface, since it also polls the hardware for current set-points"""
|
||||
# This is a slow operation, watch out!
|
||||
if self.psu:
|
||||
status = self.psu.poll_channel_state(self.channel)
|
||||
else:
|
||||
status = {}
|
||||
if self.arduino:
|
||||
status['polarity'] = self.polarity
|
||||
status['connected'] = self.connected
|
||||
status['port'] = self.com_port
|
||||
status['channel'] = self.channel
|
||||
status['target_field_raw'] = self.target_field_raw
|
||||
status['target_field'] = self.target_field
|
||||
status['target_current'] = self.target_current
|
||||
|
||||
return status
|
||||
|
||||
|
||||
class HelmholtzCageProxy:
|
||||
""" Proxy for the HelmholtzCageDevice.
|
||||
This is the only way the application should communicate with the HelmholtzCageDevice object"""
|
||||
def __init__(self, cage_device):
|
||||
self.cage_device = cage_device
|
||||
|
||||
def set_signed_currents(self, vector):
|
||||
self.cage_device.queue_command(self, {'command': 'set_signed_currents', 'arg': vector})
|
||||
|
||||
def set_field_raw(self, vector):
|
||||
self.cage_device.queue_command(self, {'command': 'set_field_raw', 'arg': vector})
|
||||
|
||||
def set_field_compensated(self, vector):
|
||||
self.cage_device.queue_command(self, {'command': 'set_field_compensated', 'arg': vector})
|
||||
|
||||
def idle(self):
|
||||
"""Puts the helmholtz cage into an idle state with zeroed fields"""
|
||||
self.cage_device.queue_command(self, {'command': 'idle', 'arg': None})
|
||||
|
||||
def close(self):
|
||||
self.cage_device.release_proxy(self)
|
||||
|
||||
def __del__(self):
|
||||
# This is a fallback method and should not be relied on. Call 'close' manually
|
||||
if self.cage_device.proxy_valid(self):
|
||||
self.cage_device.release_proxy(self)
|
||||
ui_print("Warning: Proxy implicitly released. Use close() instead.")
|
||||
|
||||
|
||||
def value_in_limits(axis, key, value):
|
||||
"""Check if value is within safe limits (set in globals.py)"""
|
||||
# axis is string with axis name, e.g. "X-Axis"
|
||||
# key specifies which value to check, e.g. current
|
||||
max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(axis)] # get max value from dictionary in globals.py
|
||||
min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(axis)] # get min value from dictionary in globals.py
|
||||
|
||||
if float(value) > float(max_value): # value is too high
|
||||
return 'HIGH'
|
||||
elif float(value) < float(min_value): # value is too low
|
||||
return 'LOW'
|
||||
else: # value is within limits
|
||||
return 'OK'
|
||||
@@ -0,0 +1,28 @@
|
||||
from threading import RLock
|
||||
import numpy as np
|
||||
|
||||
|
||||
class MagnetometerProxy:
|
||||
"""This class facilitates magnetometer data access, which is provided by a tcp client."""
|
||||
|
||||
def __init__(self):
|
||||
self.connected = False
|
||||
self._field_lock = RLock()
|
||||
self._field = np.array([0, 0, 0])
|
||||
|
||||
def set_connection_closed(self):
|
||||
"""Used by the TCP connection manager to indicate that the client has disconnected and that no new data
|
||||
will arrive."""
|
||||
self.connected = False
|
||||
|
||||
@property
|
||||
def field(self):
|
||||
with self._field_lock:
|
||||
return self._field
|
||||
|
||||
@field.setter
|
||||
def field(self, val):
|
||||
# If we receive data, we can assume we are connected
|
||||
self.connected = True
|
||||
with self._field_lock:
|
||||
self._field = val
|
||||
@@ -16,6 +16,7 @@
|
||||
# [1] = "PS 2000B Programming Guide" from 2015-05-28
|
||||
# [2] = "PS 2000B object list"
|
||||
#
|
||||
import time
|
||||
|
||||
import serial
|
||||
import struct
|
||||
@@ -388,7 +389,9 @@ class PS2000B:
|
||||
|
||||
def set_current(self, value, channel):
|
||||
self.update_device_information(channel)
|
||||
time.sleep(0.05)
|
||||
self.enable_remote_control(channel)
|
||||
time.sleep(0.05)
|
||||
curr = int(round((value * 25600.0) / self.__device_information.nominal_current))
|
||||
self.__send_device_data(Objects.SET_VALUE_CURRENT, curr, channel)
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
# ABC is template used to create python abstract classes
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
import serial
|
||||
|
||||
from src.ps2000b import PS2000B # Module containing all PS2000B HW functions and classes
|
||||
from src.utility import ui_print
|
||||
|
||||
|
||||
class PSUDevice(ABC):
|
||||
"""The PSUDevice abstract class defines a generic interface for power supplies used with the test bench.
|
||||
It can be subclassed to easily support new hardware."""
|
||||
|
||||
def __init__(self, com_port):
|
||||
"""PSUDevice assumes a serial connection"""
|
||||
self.com_port = com_port
|
||||
self.cached_state = dict.fromkeys(self.valid_channels()) # Used for components that require state on demand
|
||||
|
||||
@abstractmethod
|
||||
def enable_channel(self, channel_nr):
|
||||
"""Most PSUs offer completely disabling or enabling a channel, beyond setting V=0, I=0.
|
||||
Can throw exceptions"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def disable_channel(self, channel_nr):
|
||||
"""Most PSUs offer completely disabling or enabling a channel, beyond setting V=0, I=0.
|
||||
Can throw exceptions"""
|
||||
pass
|
||||
|
||||
# Warning: not abstract, does not need to be implemented
|
||||
def set_channel_state(self, channel_nr, enabled):
|
||||
"""True: Enable channel, False: Disable channel
|
||||
Can throw exceptions"""
|
||||
if enabled:
|
||||
self.enable_channel(channel_nr)
|
||||
else:
|
||||
self.disable_channel(channel_nr)
|
||||
|
||||
@abstractmethod
|
||||
def set_current(self, channel_nr, current):
|
||||
"""Set the current limit. Actual current dependent on OVP or OCP mode.
|
||||
Can throw exceptions"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_voltage(self, channel_nr, voltage):
|
||||
"""Set the voltage limit. Actual voltage dependent on OVP or OCP mode.
|
||||
Can throw exceptions"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def poll_channel_state(self, channel_nr):
|
||||
"""Return a dictionary with the entries below. WARNING: this call is blocking and potentially slow!
|
||||
Can throw exceptions"""
|
||||
# Should also set self.cached_state
|
||||
# return {'active': False, 'remote_active': False,
|
||||
# 'actual_voltage':0, 'limit_voltage':0, 'actual_current':0, 'limit_current':0}
|
||||
pass
|
||||
|
||||
def cached_channel_state(self, channel_nr):
|
||||
"""Return a dictionary with the entries below. Uses the values obtained during last poll.
|
||||
May contain None-entries"""
|
||||
|
||||
if self.cached_state[channel_nr]:
|
||||
return self.cached_state[channel_nr]
|
||||
else:
|
||||
return {'active': None, 'remote_active': None,
|
||||
'actual_voltage': None,
|
||||
'limit_voltage': None,
|
||||
'actual_current': None,
|
||||
'limit_current': None}
|
||||
|
||||
def idle(self):
|
||||
"""Zero all outputs but activate channels so commands can be sent."""
|
||||
for ch in self.valid_channels():
|
||||
self.set_current(ch, 0)
|
||||
self.set_voltage(ch, 0)
|
||||
self.enable_channel(ch)
|
||||
|
||||
@abstractmethod
|
||||
def shutdown(self):
|
||||
"""Shuts the PSU down safely and makes sure ALL outputs are off."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def destroy(self):
|
||||
"""Disconnects the device"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def valid_channels():
|
||||
"""Returns a list containing valid channel numbers"""
|
||||
pass
|
||||
|
||||
|
||||
class PSUDevicePS2000B(PSUDevice):
|
||||
"""Provides HW support for the Elektro-Automatik PS2000B power supply.
|
||||
This class is an adapter for the already existing PS2000B library, to have a consistent interface"""
|
||||
|
||||
MIN_DELAY = 0.05
|
||||
|
||||
def __init__(self, com_port):
|
||||
super().__init__(com_port)
|
||||
"""Can fail; Check for serial.SerialException"""
|
||||
self.dev = PS2000B.PS2000B(com_port)
|
||||
# dev_info is a class which contains hw specific constants, such as nominal voltage and current
|
||||
self.dev_info = self.dev.get_device_information() # Cache this result
|
||||
time.sleep(self.MIN_DELAY)
|
||||
|
||||
@staticmethod
|
||||
def valid_channels():
|
||||
# Dependent on PSU, the PS2000B has 2 channels
|
||||
return [0, 1]
|
||||
|
||||
def enable_channel(self, channel_nr):
|
||||
self.dev.enable_output(channel_nr)
|
||||
time.sleep(self.MIN_DELAY)
|
||||
|
||||
def disable_channel(self, channel_nr):
|
||||
self.dev.enable_output(channel_nr)
|
||||
time.sleep(self.MIN_DELAY)
|
||||
|
||||
def set_current(self, channel_nr, current):
|
||||
self.dev.set_current(current, channel_nr)
|
||||
time.sleep(self.MIN_DELAY)
|
||||
|
||||
def set_voltage(self, channel_nr, voltage):
|
||||
self.dev.set_voltage(voltage, channel_nr)
|
||||
time.sleep(self.MIN_DELAY)
|
||||
|
||||
def poll_channel_state(self, channel_nr):
|
||||
self.dev.update_device_information(channel_nr) # update the information in the device object
|
||||
time.sleep(self.MIN_DELAY)
|
||||
dev_status = self.dev.get_device_status_information(channel_nr) # get object with new status info
|
||||
|
||||
# This is more efficient than the dev.get_voltage() call, since it does not require another update_device_info
|
||||
voltage = dev_status.actual_voltage_percent * self.dev_info.nominal_voltage # Extracted from PS2000B library
|
||||
voltage_setp = self.dev.get_voltage_setpoint(channel_nr)
|
||||
time.sleep(self.MIN_DELAY)
|
||||
current = dev_status.actual_current_percent * self.dev_info.nominal_current
|
||||
current_setp = self.dev.get_current_setpoint(channel_nr)
|
||||
time.sleep(self.MIN_DELAY)
|
||||
|
||||
# Format should match the provided template in abstract PSUDevice class.
|
||||
self.cached_state[channel_nr] = {'active': dev_status.output_active,
|
||||
'remote_active': dev_status.remote_control_active,
|
||||
'voltage': voltage, 'voltage_setpoint': voltage_setp,
|
||||
'current': current, 'current_setpoint': current_setp}
|
||||
return self.cached_state[channel_nr]
|
||||
|
||||
def shutdown(self):
|
||||
for ch in self.valid_channels():
|
||||
self.disable_channel(ch)
|
||||
self.set_current(ch, 0)
|
||||
self.set_voltage(ch, 0)
|
||||
|
||||
def destroy(self):
|
||||
self.dev.serial.close()
|
||||
|
||||
|
||||
class PSUDeviceQL355TP(PSUDevice):
|
||||
"""HW interface for QL355TP from AIM-TTi Instruments"""
|
||||
|
||||
def __init__(self, com_port):
|
||||
"""Can fail; Check for serial.SerialException"""
|
||||
super().__init__(com_port)
|
||||
self._serial_object = serial.Serial(
|
||||
port=self.com_port,
|
||||
baudrate=19200,
|
||||
timeout=0.5,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS
|
||||
)
|
||||
self.set_output_range(0, 0) # Put the PSU into the 15V/5A range
|
||||
self.set_output_range(1, 0)
|
||||
self.reset_breaker() # Reset the breaker in case we are coming from an unclean state
|
||||
self.cached_state = dict.fromkeys(self.valid_channels()) # Used for components that require state on demand
|
||||
|
||||
@staticmethod
|
||||
def valid_channels():
|
||||
# Dependent on PSU, the QL355TP has 2 normal channels. The auxiliary channel is not usable for our purpose
|
||||
return [1, 2]
|
||||
|
||||
def enable_channel(self, channel_nr):
|
||||
# The serial interface is documented in the QL355TP handbook
|
||||
self._serial_object.write("OP{} 1\n".format(channel_nr).encode())
|
||||
|
||||
def disable_channel(self, channel_nr):
|
||||
# The serial interface is documented in the QL355TP handbook
|
||||
self._serial_object.write("OP{} 0\n".format(channel_nr).encode())
|
||||
|
||||
def set_current(self, channel_nr, current):
|
||||
# The serial interface is documented in the QL355TP handbook
|
||||
self._serial_object.write("I{} {}\n".format(channel_nr, current).encode())
|
||||
|
||||
def set_voltage(self, channel_nr, voltage):
|
||||
# The serial interface is documented in the QL355TP handbook
|
||||
self._serial_object.write("V{} {}\n".format(channel_nr, voltage).encode())
|
||||
|
||||
def poll_channel_state(self, channel_nr):
|
||||
# Request channel state
|
||||
self._serial_object.write("OP{}?\n".format(channel_nr).encode())
|
||||
resp = self._serial_object.read_until()
|
||||
output_active = resp.decode().rstrip()
|
||||
|
||||
# Request current current
|
||||
self._serial_object.write("I{}O?\n".format(channel_nr).encode())
|
||||
resp = self._serial_object.read_until()
|
||||
# Trim whitespace and units
|
||||
current = float(resp.decode().rstrip()[:-1])
|
||||
|
||||
# Request current setpoint
|
||||
self._serial_object.write("I{}?\n".format(channel_nr).encode())
|
||||
resp = self._serial_object.read_until()
|
||||
# Trim whitespace, prefix and units
|
||||
current_setp = float(resp.decode().rstrip()[3:-1])
|
||||
|
||||
# Request current voltage
|
||||
self._serial_object.write("V{}O?\n".format(channel_nr).encode())
|
||||
resp = self._serial_object.read_until()
|
||||
# Trim whitespace and units
|
||||
voltage = float(resp.decode().rstrip()[:-1])
|
||||
|
||||
# Request voltage setpoint
|
||||
self._serial_object.write("V{}?\n".format(channel_nr).encode())
|
||||
resp = self._serial_object.read_until()
|
||||
# Trim whitespace and units
|
||||
voltage_setp = float(resp.decode().rstrip()[3:-1])
|
||||
|
||||
# Format should match the provided template in abstract PSUDevice class.
|
||||
# The remote_active property is assumed to always be True since it cant be read
|
||||
# (it should be since we are talking to it)
|
||||
self.cached_state[channel_nr] = {'active': output_active, 'remote_active': True,
|
||||
'voltage': voltage, 'voltage_setpoint': voltage_setp,
|
||||
'current': current, 'current_setpoint': current_setp}
|
||||
return self.cached_state[channel_nr]
|
||||
|
||||
def set_output_range(self, channel_nr, value_range):
|
||||
"""The QL355TP supports various output ranges. We require the 15V/5A mode to achieve the greatest range."""
|
||||
# range 0: 15V/5A, 1=35V/3A, 2=35V/500mA
|
||||
self._serial_object.write("RANGE{} {}\n".format(channel_nr, value_range).encode())
|
||||
|
||||
def reset_breaker(self):
|
||||
"""The QL355TP includes an internal breaker, which must be reset if the
|
||||
maximum voltage or current is exceeded"""
|
||||
self._serial_object.write("TRIPRST\n".encode())
|
||||
|
||||
def shutdown(self):
|
||||
for ch in self.valid_channels():
|
||||
self.disable_channel(ch)
|
||||
self.set_current(ch, 0)
|
||||
self.set_voltage(ch, 0)
|
||||
|
||||
def destroy(self):
|
||||
self._serial_object.close()
|
||||
@@ -0,0 +1,196 @@
|
||||
from threading import Thread
|
||||
import socket
|
||||
import numpy as np
|
||||
|
||||
import src.globals as g
|
||||
from src.utility import ui_print
|
||||
import src.helmholtz_cage_device as helmholtz_cage_device
|
||||
|
||||
# --- Definition of TCP interface ---
|
||||
#
|
||||
# Clients should by default initialize a TCP connection to port 6677
|
||||
# The commands shown must be terminated with a single \n (newline) char
|
||||
# Commands may be split across multiple packets.
|
||||
# Before useful commands can be sent, declare_api_version must be called.
|
||||
#
|
||||
# A description of the TCP api (safety limits are always enforced):
|
||||
#
|
||||
# set_raw_field [X comp.] [Y comp.] [Z comp.]
|
||||
# Returns: 0 or 1 for success
|
||||
# Accepts decimal point formatted floats, with or without scientific notation. The float() cast must understand it.
|
||||
# The field units are Tesla
|
||||
# This causes an additional field of the given strength to be generated, without regard for the pre-existing
|
||||
# geomagnetic/external fields.
|
||||
#
|
||||
# set_compensated_field [X comp.] [Y comp.] [Z comp.]
|
||||
# Returns: 0 or 1 for success
|
||||
# Accepts decimal point formatted floats, with or without scientific notation. The float() cast must understand it.
|
||||
# The field units are Tesla
|
||||
# This causes a field of exactly the given magnitude to be generated by compensating external factors such as the
|
||||
# geomagnetic field.
|
||||
#
|
||||
# set_coil_currents [X comp.] [Y comp.] [Z comp.]
|
||||
# Returns: 0 or 1 for success
|
||||
# Accepts decimal point formatted floats, with or without scientific notation. The float() cast must understand it.
|
||||
# The field units are Ampere
|
||||
# This establishes the requested current in the individual coils.
|
||||
#
|
||||
# get_api_version
|
||||
# Returns: a string uniquely identifying each API version.
|
||||
# This function can be called before declare_api_version.
|
||||
# Please dont put
|
||||
#
|
||||
# magnetometer_field [X comp.] [Y comp.] [Z comp.]
|
||||
# Returns: 1
|
||||
# Accepts decimal point formatted floats, with or without scientific notation. The float() cast must understand it.
|
||||
# The field units are Tesla
|
||||
# Sets the state of an a virtual magnetometer object which mirrors a physical sensor providing data by means of
|
||||
# this command.
|
||||
#
|
||||
# declare_api_version [version]
|
||||
# Returns: 0 or 1 (terminated with newline)
|
||||
# Declare the api version the client application was programmed for. It must be compatible with the current
|
||||
# API version. This prevents unexpected behaviour by forcing programmers to specify which API they are expecting.
|
||||
# This function must be called before sending HW commands.
|
||||
|
||||
|
||||
SOCKET_INTERFACE_API_VERSION = "2"
|
||||
|
||||
|
||||
class ClientConnectionThread(Thread):
|
||||
def __init__(self, client_socket, address):
|
||||
Thread.__init__(self)
|
||||
self.client_socket = client_socket
|
||||
self.client_address = address
|
||||
|
||||
# Indicates whether this thread was providing magnetometer data. If yes, set the magnetometer proxy object as
|
||||
# disconnected when the socket is closed.
|
||||
self.magnetometer_connection = False
|
||||
|
||||
# Holds proxy model to cage device if required. Is initialized lazily to prevent always blocking interface
|
||||
self._cage_dev = None
|
||||
self.api_compat = False # Indicates whether the client has a compatible API version
|
||||
|
||||
def run(self):
|
||||
msg = ''
|
||||
while True:
|
||||
try:
|
||||
raw_msg = self.client_socket.recv(2048).decode()
|
||||
# Check for end of stream
|
||||
if raw_msg == "":
|
||||
self.client_socket.close()
|
||||
if self._cage_dev:
|
||||
self._cage_dev.close()
|
||||
g.MAGNETOMETER.connected = False
|
||||
return
|
||||
# Process message
|
||||
for char in raw_msg:
|
||||
if char == '\n':
|
||||
msg = msg.rstrip() # Some systems will try to send \r characters... looking at you windows O_O
|
||||
try:
|
||||
response = self.handle_msg(msg)
|
||||
except Exception as e:
|
||||
ui_print("An error occurred while processing a client message")
|
||||
ui_print("Msg: {}".format(msg))
|
||||
ui_print(e)
|
||||
response = "err"
|
||||
self.client_socket.sendall((response + '\n').encode('utf-8'))
|
||||
msg = ''
|
||||
else:
|
||||
msg += char
|
||||
except ConnectionResetError as e:
|
||||
ui_print("A connection was closed by the client.")
|
||||
self.client_socket.close()
|
||||
if self._cage_dev:
|
||||
self._cage_dev.close()
|
||||
g.MAGNETOMETER.connected = False
|
||||
return
|
||||
|
||||
def handle_msg(self, message):
|
||||
""" Executes command logic and returns string response (for client). """
|
||||
tokens = message.split(" ")
|
||||
if tokens[0] == "get_api_version":
|
||||
return SOCKET_INTERFACE_API_VERSION
|
||||
elif tokens[0] == "declare_api_version":
|
||||
if tokens[1] == SOCKET_INTERFACE_API_VERSION:
|
||||
self.api_compat = True
|
||||
return "1"
|
||||
else:
|
||||
ui_print("Declared socket API version ({}) is incompatible with current version ({})!".format(tokens[1], SOCKET_INTERFACE_API_VERSION))
|
||||
return "0"
|
||||
else:
|
||||
# api_compat indicates we have checked the api version and are ready to accept commands
|
||||
if self.api_compat:
|
||||
if tokens[0] == "set_raw_field":
|
||||
x = float(tokens[1])
|
||||
y = float(tokens[2])
|
||||
z = float(tokens[3])
|
||||
field_vec = np.array([x, y, z], dtype=np.float32)
|
||||
# uncompensated
|
||||
self.cage_dev.set_field_raw(field_vec)
|
||||
return "1"
|
||||
elif tokens[0] == "set_compensated_field":
|
||||
x = float(tokens[1])
|
||||
y = float(tokens[2])
|
||||
z = float(tokens[3])
|
||||
field_vec = np.array([x, y, z], dtype=np.float32)
|
||||
# compensated
|
||||
self.cage_dev.set_field_compensated(field_vec)
|
||||
return "1"
|
||||
elif tokens[0] == "set_coil_currents":
|
||||
x = float(tokens[1])
|
||||
y = float(tokens[2])
|
||||
z = float(tokens[3])
|
||||
current_vec = np.array([x, y, z], dtype=np.float32)
|
||||
self.cage_dev.set_signed_currents(current_vec)
|
||||
return "1"
|
||||
elif tokens[0] == "magnetometer_field":
|
||||
"""The client is sending us information about the magnetometer state. This is used for some
|
||||
calibration procedures for example."""
|
||||
x = float(tokens[1])
|
||||
y = float(tokens[2])
|
||||
z = float(tokens[3])
|
||||
field = np.array([x, y, z], dtype=np.float32)
|
||||
g.MAGNETOMETER.field = field
|
||||
self.magnetometer_connection = True
|
||||
return "1"
|
||||
else:
|
||||
# The message given is unknown. The programmer probably did not intend for this, so display an error
|
||||
# even if is not inherently problematic.
|
||||
raise Exception("The command '{}' is unknown".format(tokens[0]))
|
||||
else:
|
||||
raise Exception("The command '{}' may not be called before 'declare_api_version'".format(tokens[0]))
|
||||
|
||||
@property
|
||||
def cage_dev(self):
|
||||
if self._cage_dev is None:
|
||||
try:
|
||||
self._cage_dev = g.CAGE_DEVICE.request_proxy()
|
||||
except helmholtz_cage_device.DeviceBusy:
|
||||
# Return none. This will cause an error and show up as "err" on the client
|
||||
# A more helpful error message is shown on application side
|
||||
ui_print("Socket client attempted to acquire busy device.")
|
||||
return self._cage_dev
|
||||
|
||||
|
||||
class SocketInterfaceThread(Thread):
|
||||
def __init__(self):
|
||||
Thread.__init__(self, daemon=True)
|
||||
self.server_socket = None
|
||||
|
||||
# Can throw exception, which should be passed on to the instantiator of this class
|
||||
self.configure_tcp_port()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
(client_socket, address) = self.server_socket.accept()
|
||||
new_thread = ClientConnectionThread(client_socket, address)
|
||||
new_thread.start()
|
||||
ui_print("Accepted connection from {}".format(address))
|
||||
|
||||
def configure_tcp_port(self):
|
||||
# Creates and configures the listening port
|
||||
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server_socket.bind(('', g.SOCKET_PORT))
|
||||
self.server_socket.listen(5) # Limit to max. 5 simultaneous connections
|
||||
ui_print("Listening for TCP connections on port {}".format(g.SOCKET_PORT))
|
||||
@@ -0,0 +1,51 @@
|
||||
import csv
|
||||
from tkinter import filedialog
|
||||
import numpy as np
|
||||
import src.globals as g
|
||||
|
||||
|
||||
def ui_print(*content):
|
||||
"""prints text to built-in console, use exactly like normal print(). Requires the ui to be initialized"""
|
||||
output = " ".join([str(c) for c in content])
|
||||
|
||||
if not g.exit_flag and g.app is not None:
|
||||
# application is still running --> output window is visible
|
||||
g.app.OutputConsole.put(output)
|
||||
else:
|
||||
# if window is not open, do normal print
|
||||
print(output)
|
||||
|
||||
|
||||
def save_dict_list_to_csv(filename, data, query_path=False):
|
||||
"""Creates a csv file under the specified path containing one row for each dict in the list 'data'.
|
||||
The file receives a header containing the keys of the first dict entry.
|
||||
Each dict should use the same keys."""
|
||||
if query_path:
|
||||
filename = filedialog.asksaveasfilename(initialfile=filename, title="Select csv save location...",
|
||||
filetypes=(("CSV", "*.csv"),))
|
||||
|
||||
with open(filename, mode='w', newline='') as csv_file:
|
||||
fieldnames = data[0].keys()
|
||||
csv_writer = csv.DictWriter(csv_file, fieldnames=fieldnames, delimiter=',', quotechar='"',
|
||||
quoting=csv.QUOTE_NONNUMERIC)
|
||||
|
||||
csv_writer.writeheader()
|
||||
for row in data:
|
||||
csv_writer.writerow(row)
|
||||
|
||||
|
||||
def load_dict_list_from_csv(filename, query_path=False):
|
||||
""" Reads a csv file under the specified path containing one row for each dict in the list 'data'.
|
||||
The file header containing the keys of the first dict entry is deleted upon reading.
|
||||
Each dict should use the same keys."""
|
||||
if query_path:
|
||||
filename = filedialog.askopenfilename(initialfile=filename, title="Select csv file location...",
|
||||
filetypes=(("CSV", "*.csv"),))
|
||||
data = np.genfromtxt(filename, dtype=float, delimiter=',')
|
||||
data = data[1:len(data), :] # remove header
|
||||
# Save data point to raw_data list
|
||||
raw_data = []
|
||||
for i in range(data.shape[0]):
|
||||
raw_data.append({'applied_x': data[i][0], 'applied_y': data[i][1], 'applied_z': data[i][2],
|
||||
'measured_x': data[i][3], 'measured_y': data[i][4], 'measured_z': data[i][5]})
|
||||
return data, raw_data, filename
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
@@ -0,0 +1,48 @@
|
||||
import serial
|
||||
import time
|
||||
from datetime import datetime
|
||||
import socket
|
||||
|
||||
# Lookup table for cage axis to magnetometer transformation
|
||||
# First entry: Magnetometer z axis to cage x (reversed) [1,1]
|
||||
# axis_mapping = [[corresponding axis (0=x,1=y,2=z), direction (+-1) ...]
|
||||
# axis_mapping = [[2, -1], [0, 1], [1, -1]] # Z_MGM = -X_HH, X_MGM = +Y_HH, Y_MGM = -Z_HH,
|
||||
axis_mapping = [[0, -1], # X_MGM = -X_HH
|
||||
[2, -1], # Y_MGM = -Z_HH
|
||||
[1, -1]] # Z_MGM = -Y_HH,
|
||||
|
||||
# Helmholtz control software tcp port
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect(("localhost", 6677))
|
||||
s.sendall("declare_api_version 2\n".encode())
|
||||
|
||||
# FGM3D software virtual serial port
|
||||
ser = serial.Serial("COM11")
|
||||
|
||||
# check whether day time saving is current or not
|
||||
if time.localtime().tm_isdst:
|
||||
dst = 2
|
||||
else:
|
||||
dst = 1
|
||||
|
||||
line = b""
|
||||
ready = False
|
||||
while True:
|
||||
line += ser.read()
|
||||
if line[-2:] == b"\r\n":
|
||||
new_line = line[:-2].decode('ascii')
|
||||
|
||||
delta = datetime.now().timestamp()*1000 - int(new_line.split(';')[0])+(dst*60*60*1000)
|
||||
if delta < 500 and not ready:
|
||||
ready = True
|
||||
print("FGM3D adapter script ready!")
|
||||
elif not ready:
|
||||
print("FGM3D adapter script not ready!")
|
||||
if ready:
|
||||
# Data is not valid otherwise
|
||||
# Only use the x y and z values
|
||||
tokens = [float(v.replace(',', '.')) for v in new_line.split(";")[1:-1]]
|
||||
axis_fields = [tokens[m[0]]*m[1] for m in axis_mapping]
|
||||
axis_fields = ["{:.6e}".format(v) for v in axis_fields]
|
||||
s.sendall(("magnetometer_field {} {} {}\n".format(*axis_fields)).encode())
|
||||
line = b""
|
||||