From 09059752f8e03be3b4a0313047f9a34367050cd0 Mon Sep 17 00:00:00 2001 From: maxkrapp1 Date: Mon, 16 Jan 2023 11:27:22 +0100 Subject: [PATCH] Added function to switch to EPC without power off. Package added to pip. --- .github/workflows/publish-to-pypi.yml | 19 + .gitignore | 12 +- Examples/BCMuxInterface/BCMuxInterface.py | 73 +-- .../BasicIntroduction/BasicIntroduction.ipynb | 34 +- .../BasicIntroduction/BasicIntroduction.py | 27 +- Examples/CVImportPlot/CVImportPlot.ipynb | 30 +- Examples/CVImportPlot/CVImportPlot.py | 25 +- .../CurrentVoltageCurve.ipynb | 21 +- .../CurrentVoltageCurve.py | 39 +- .../CyclicVoltammetry/CyclicVoltammetry.ipynb | 34 +- .../CyclicVoltammetry/CyclicVoltammetry.py | 34 +- Examples/DCSequencer/DCSequencer.ipynb | 36 +- Examples/DCSequencer/DCSequencer.py | 12 +- Examples/EIS/EIS.ipynb | 36 +- Examples/EIS/EIS.py | 24 +- Examples/EISCVLaTeX/EISCVLaTeX.ipynb | 25 +- Examples/EISCVLaTeX/EISCVLaTeX.py | 154 +++--- Examples/EISImportPlot/EISImportPlot.ipynb | 23 +- Examples/EISImportPlot/EISImportPlot.py | 34 +- Examples/EISPad4/EISPad4.ipynb | 32 +- Examples/EISPad4/EISPad4.py | 52 +- Examples/EISvsParameter/EISvsParameter.ipynb | 32 +- Examples/EISvsParameter/EISvsParameter.py | 83 +-- .../ExternalDeviceFRA/ExternalDeviceFRA.ipynb | 35 +- .../ExternalDeviceFRA/ExternalDeviceFRA.py | 28 +- .../FileExchangeEIS/FileExchangeEIS.ipynb | 44 +- Examples/FileExchangeEIS/FileExchangeEIS.py | 60 +-- Examples/HeartBeat/HeartBeat.ipynb | 36 +- Examples/HeartBeat/HeartBeat.py | 23 +- .../HeartBeatLiveData/HeartBeatLiveData.ipynb | 357 +++++++++++++ .../HeartBeatLiveData/HeartBeatLiveData.py | 124 +++++ .../ImpedanceMultiCellCycle.ipynb | 486 ++++++++++++++++++ .../ImpedanceMultiCellCycle.py | 196 +++++++ .../ImpedanceRampHotSwap.ipynb | 407 +++++++++++++++ .../ImpedanceRampHotSwap.py | 145 ++++++ .../LoadWithExternalSource.ipynb | 23 +- .../LoadWithExternalSource.py | 86 ++-- Examples/jupyter_utils.py | 107 ++++ README.md | 31 +- delta_remote/connection.py | 2 +- delta_remote/script_wrapper.py | 2 +- pyproject.toml | 36 +- thales_remote/connection.py | 12 +- thales_remote/epc_scpi_handler.py | 350 +++++++++++++ thales_remote/error.py | 2 +- thales_remote/file_interface.py | 83 ++- thales_remote/script_wrapper.py | 401 ++++++++------- 47 files changed, 3034 insertions(+), 933 deletions(-) create mode 100644 .github/workflows/publish-to-pypi.yml create mode 100644 Examples/HeartBeatLiveData/HeartBeatLiveData.ipynb create mode 100644 Examples/HeartBeatLiveData/HeartBeatLiveData.py create mode 100644 Examples/ImpedanceMultiCellCycle/ImpedanceMultiCellCycle.ipynb create mode 100644 Examples/ImpedanceMultiCellCycle/ImpedanceMultiCellCycle.py create mode 100644 Examples/ImpedanceRampHotSwap/ImpedanceRampHotSwap.ipynb create mode 100644 Examples/ImpedanceRampHotSwap/ImpedanceRampHotSwap.py create mode 100644 Examples/jupyter_utils.py create mode 100644 thales_remote/epc_scpi_handler.py diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml new file mode 100644 index 0000000..7254722 --- /dev/null +++ b/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,19 @@ +name: Publish Python 🐍 distributions 📦 to PyPI + +on: + release: + types: [published] + +jobs: + build-n-publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - run: python3 -m pip install --upgrade build && python3 -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index f41fcc0..1306ab3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,14 @@ __pycache__ /dist/ /*.egg-info/ -# IDEs \ No newline at end of file +# IDEs +/deploy/ +/.vscode/ +/.vs/ +/.settings/ +/.ipynb_checkpoints/ +/docs/ +/source/_build/ +*__pycache__ +/.project +/.pydevproject diff --git a/Examples/BCMuxInterface/BCMuxInterface.py b/Examples/BCMuxInterface/BCMuxInterface.py index dffeeff..602f890 100644 --- a/Examples/BCMuxInterface/BCMuxInterface.py +++ b/Examples/BCMuxInterface/BCMuxInterface.py @@ -1,14 +1,15 @@ import socket -class BCMuxInterface(): - """ BC-Mux control class. - + +class BCMuxInterface: + """BC-Mux control class. + With this class the `BC-MUX Multiplexer `_ can be controlled remotely without the BC-Mux Controller software. - + The USB interface of the BC Mux is not supported by Python. Also the network settings must be done with the program BC-Mux Network Config before using python. - + The BC-MUX is an extension which makes it possible to separate up to 16 channels of a cyclizer individually with switch boxes from the cyclizer and to switch them to the Zennium for e.g. impedance measurements. This allows the cyclizer to be extended up to 16 channels with sequential @@ -18,76 +19,76 @@ class BCMuxInterface(): This class makes it possible to control the Zennium and the Multiplexer from one Python instance via Remote2, which makes the use more flexible than with the BC-Mux Controller. Also, if the cyclizer supports it, the complete system can be controlled from a single Python software. - + :param ip: SerialCommandInterface object to control the device. :type ip: str :param port: SerialDataInterface object for online data. :type port: int """ - + BUFFER_SIZE = 1024 - + def __init__(self, ip, port): self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.ip = ip self.port = port self.socket.connect((self.ip, self.port)) return - + def close(self): - """ Closing the connection. - + """Closing the connection. + Disconnects the TCP/IP connection to the BC-MUX. """ self.socket.close() return - + def connectChannel(self, channel): - """ Connects the channel to the zennium. - + """Connects the channel to the zennium. + With this command, a channel is disconnected from the cyclizer and switched to the Zennium, for example for impedance measurements. - + :param channel: The channel to connect to the zennium. :returns: The response string from the device. :rtype: string """ command = "ch {}" return self._executeCommandAndReadReply(command.format(channel)) - + def disconnectChannel(self): - """ Disconnects all channels from the zennium. - + """Disconnects all channels from the zennium. + All channels are disconnected from the Zennium and switched to the specific cyclizer channel. - + :returns: The response string from the device. :rtype: string """ command = "ch 0" return self._executeCommandAndReadReply(command) - + def setPulseLength(self, length): - """ Setting the relais control. - + """Setting the relais control. + The BC-MUX supports switchboxes containing monostable or bistable relais. With this command, the control of the relais is set. - + If a number other than 0 is set, the relay is switched with a pulse. The pulse is then the number in milliseconds long. - + :param length: The length of the switching pulse in milliseconds. 0 for monostable relays. :returns: The response string from the device. :rtype: string """ command = "puls {}" return self._executeCommandAndReadReply(command.format(length)) - + def _executeCommandAndReadReply(self, command): - """ Private function to send a command to the device and read a string. - + """Private function to send a command to the device and read a string. + This command sends the command to the device and returns the response from the device. - - :returns: Response string from the device. + + :returns: Response string from the device. :rtype: string """ command += "\r\n" @@ -95,21 +96,21 @@ def _executeCommandAndReadReply(self, command): data = self.socket.recv(BCMuxInterface.BUFFER_SIZE) return data.decode("utf-8") -if __name__ == '__main__': + +if __name__ == "__main__": TCP_IP = "169.169.169.169" TCP_PORT = 4223 - + bcMux = BCMuxInterface(TCP_IP, TCP_PORT) - + bcMux.setPulseLength(250) bcMux.disconnectChannel() - + for i in range(16): print(f"Channel: {i+1}") - bcMux.connectChannel(i+1) + bcMux.connectChannel(i + 1) bcMux.disconnectChannel() - + bcMux.close() print("finish") - diff --git a/Examples/BasicIntroduction/BasicIntroduction.ipynb b/Examples/BasicIntroduction/BasicIntroduction.ipynb index 9d37d33..0121072 100644 --- a/Examples/BasicIntroduction/BasicIntroduction.ipynb +++ b/Examples/BasicIntroduction/BasicIntroduction.ipynb @@ -75,28 +75,15 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "TARGET_HOST = \"localhost\"\n", "\n", "if __name__ == \"__main__\":\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(TARGET_HOST, \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()" + " zenniumConnection.connectToTerm(TARGET_HOST, \"ScriptRemote\")" ] }, { @@ -377,11 +364,9 @@ } ], "metadata": { - "interpreter": { - "hash": "ac59ebe37160ed0dfa835113d9b8498d9f09ceb179beaac4002f036b9467c963" - }, "kernelspec": { - "display_name": "Python 3.9.7 64-bit", + "display_name": "Python 3", + "language": "python", "name": "python3" }, "language_info": { @@ -394,9 +379,14 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.11.1 (tags/v3.11.1:a7a450f, Dec 6 2022, 19:58:39) [MSC v.1934 64 bit (AMD64)]" }, - "orig_nbformat": 4 + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "5238573367df39f7286bb46f9ff5f08f63a01a80960060ce41e3c79b190280fa" + } + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/Examples/BasicIntroduction/BasicIntroduction.py b/Examples/BasicIntroduction/BasicIntroduction.py index 17cde68..340f484 100644 --- a/Examples/BasicIntroduction/BasicIntroduction.py +++ b/Examples/BasicIntroduction/BasicIntroduction.py @@ -2,34 +2,36 @@ import math import cmath from thales_remote.connection import ThalesRemoteConnection -from thales_remote.script_wrapper import PotentiostatMode,ThalesRemoteScriptWrapper +from thales_remote.script_wrapper import PotentiostatMode, ThalesRemoteScriptWrapper + def printImpedance(impedance): - print(f"Impedance: {abs(impedance):>10.3e} ohm {cmath.phase(impedance)/cmath.pi*180.0:>10.2f} degree") + print( + f"Impedance: {abs(impedance):>10.3e} ohm {cmath.phase(impedance)/cmath.pi*180.0:>10.2f} degree" + ) return + def spectrum(scriptHandle, lower_frequency, upper_frequency, number_of_points): log_lower_frequency = math.log(lower_frequency) log_upper_frequency = math.log(upper_frequency) - log_interval_spacing = (log_upper_frequency - log_lower_frequency) / (number_of_points - 1) - + log_interval_spacing = (log_upper_frequency - log_lower_frequency) / ( + number_of_points - 1 + ) + for i in range(number_of_points): current_frequency = math.exp(log_lower_frequency + log_interval_spacing * i) print(f"Frequency: {current_frequency:e} Hz") printImpedance(scriptHandle.getImpedance(current_frequency)) - + return + TARGET_HOST = "localhost" if __name__ == "__main__": zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm(TARGET_HOST, "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() + zenniumConnection.connectToTerm(TARGET_HOST, "ScriptRemote") zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() @@ -59,7 +61,7 @@ def spectrum(scriptHandle, lower_frequency, upper_frequency, number_of_points): zahnerZennium.enablePotentiostat() zahnerZennium.setFrequency(2000) zahnerZennium.setNumberOfPeriods(3) - + zahnerZennium.enablePotentiostat() zahnerZennium.getCurrent() @@ -76,4 +78,3 @@ def spectrum(scriptHandle, lower_frequency, upper_frequency, number_of_points): zenniumConnection.disconnectFromTerm() print("finish") - diff --git a/Examples/CVImportPlot/CVImportPlot.ipynb b/Examples/CVImportPlot/CVImportPlot.ipynb index fc760d6..db5aa11 100644 --- a/Examples/CVImportPlot/CVImportPlot.ipynb +++ b/Examples/CVImportPlot/CVImportPlot.ipynb @@ -41,33 +41,19 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "\n", "if __name__ == \"__main__\":\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(\n", - " \"localhost\", \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", + " zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", "\n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()\n", "\n", - " zahnerZennium.calibrateOffsets()\n" + " zahnerZennium.calibrateOffsets()" ] }, { @@ -187,7 +173,7 @@ " Determine the maximum current for the next measurement\n", " from the maximum current of the last CV measurement.\n", " \"\"\"\n", - " latestMeasurement = IscImport(fileInterface.getLatestReceivedFile()[\"binary_data\"])\n", + " latestMeasurement = IscImport(fileInterface.getLatestReceivedFile().binaryData)\n", " maximumCurrent = max(abs(latestMeasurement.getCurrentArray()))\n", "\n", " zahnerZennium.setCVMaximumCurrent(maximumCurrent * 3)\n", @@ -241,11 +227,11 @@ "source": [ " iscFileFromDisc = IscImport(r\"C:\\THALES\\temp\\cv\\cv_1000mVs.isc\")\n", "\n", - " iscFiles = [IscImport(file[\"binary_data\"]) for file in fileInterface.getReceivedFiles()]\n", + " iscFiles = [IscImport(file.binaryData) for file in fileInterface.getReceivedFiles()]\n", "\n", " for iscFile in iscFiles:\n", " print(f\"{iscFile.getScanRate()} V/s\\tmeasurement finished at {iscFile.getMeasurementEndDateTime()}\")\n", - " \n" + " " ] }, { @@ -314,7 +300,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.10.5 (tags/v3.10.5:f377153, Jun 6 2022, 16:14:13) [MSC v.1929 64 bit (AMD64)]" }, "orig_nbformat": 4 }, diff --git a/Examples/CVImportPlot/CVImportPlot.py b/Examples/CVImportPlot/CVImportPlot.py index de7118c..b4caf6e 100644 --- a/Examples/CVImportPlot/CVImportPlot.py +++ b/Examples/CVImportPlot/CVImportPlot.py @@ -12,13 +12,7 @@ if __name__ == "__main__": zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm( - "localhost", "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() + zenniumConnection.connectToTerm("localhost", "ScriptRemote") zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() @@ -67,7 +61,7 @@ Determine the maximum current for the next measurement from the maximum current of the last CV measurement. """ - latestMeasurement = IscImport(fileInterface.getLatestReceivedFile()["binary_data"]) + latestMeasurement = IscImport(fileInterface.getLatestReceivedFile().binaryData) maximumCurrent = max(abs(latestMeasurement.getCurrentArray())) zahnerZennium.setCVMaximumCurrent(maximumCurrent * 3) @@ -75,21 +69,25 @@ zenniumConnection.disconnectFromTerm() fileInterface.close() - iscFileFromDisc = IscImport(r"C:\THALES\temp\cv\cv_1000mVs.isc") - iscFiles = [IscImport(file["binary_data"]) for file in fileInterface.getReceivedFiles()] + iscFiles = [IscImport(file.binaryData) for file in fileInterface.getReceivedFiles()] for iscFile in iscFiles: - print(f"{iscFile.getScanRate()} V/s\tmeasurement finished at {iscFile.getMeasurementEndDateTime()}") - + print( + f"{iscFile.getScanRate()} V/s\tmeasurement finished at {iscFile.getMeasurementEndDateTime()}" + ) figCV, (axis) = plt.subplots(1, 1) figCV.suptitle("Cyclic Voltammetry at different scan rates") for iscFile in iscFiles: - axis.plot(iscFile.getVoltageArray(), iscFile.getCurrentArray(),label=f"{iscFile.getScanRate()} $\\frac{{V}}{{s}}$") + axis.plot( + iscFile.getVoltageArray(), + iscFile.getCurrentArray(), + label=f"{iscFile.getScanRate()} $\\frac{{V}}{{s}}$", + ) axis.grid(which="both") axis.xaxis.set_major_formatter(EngFormatter(unit="$V$")) @@ -101,4 +99,3 @@ figCV.set_size_inches(18, 18) plt.show() figCV.savefig("CV.svg") - diff --git a/Examples/CurrentVoltageCurve/CurrentVoltageCurve.ipynb b/Examples/CurrentVoltageCurve/CurrentVoltageCurve.ipynb index 9b29bd7..4e0d5ff 100644 --- a/Examples/CurrentVoltageCurve/CurrentVoltageCurve.ipynb +++ b/Examples/CurrentVoltageCurve/CurrentVoltageCurve.ipynb @@ -38,26 +38,13 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "if __name__ == \"__main__\":\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", + " zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", " \n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()" @@ -426,7 +413,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.10.5 (tags/v3.10.5:f377153, Jun 6 2022, 16:14:13) [MSC v.1929 64 bit (AMD64)]" }, "orig_nbformat": 4, "vscode": { diff --git a/Examples/CurrentVoltageCurve/CurrentVoltageCurve.py b/Examples/CurrentVoltageCurve/CurrentVoltageCurve.py index d1d28d6..29113bd 100644 --- a/Examples/CurrentVoltageCurve/CurrentVoltageCurve.py +++ b/Examples/CurrentVoltageCurve/CurrentVoltageCurve.py @@ -9,13 +9,8 @@ if __name__ == "__main__": zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm("localhost", "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() - + zenniumConnection.connectToTerm("localhost", "ScriptRemote") + zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() @@ -24,7 +19,7 @@ zahnerZennium.setIENaming("individual") zahnerZennium.calibrateOffsets() - + zahnerZennium.setIEFirstEdgePotential(0) zahnerZennium.setIEFirstEdgePotentialRelation("absolute") zahnerZennium.setIESecondEdgePotential(0.4) @@ -33,14 +28,14 @@ zahnerZennium.setIEThirdEdgePotentialRelation("absolute") zahnerZennium.setIEFourthEdgePotential(0) zahnerZennium.setIEFourthEdgePotentialRelation("absolute") - + zahnerZennium.setIEPotentialResolution(0.005) zahnerZennium.setIEMinimumWaitingTime(0.1) zahnerZennium.setIEMaximumWaitingTime(3) - zahnerZennium.setIERelativeTolerance(0.01) #1 % - zahnerZennium.setIEAbsoluteTolerance(0.001) #1 mA + zahnerZennium.setIERelativeTolerance(0.01) # 1 % + zahnerZennium.setIEAbsoluteTolerance(0.001) # 1 mA zahnerZennium.setIEOhmicDrop(0) - + zahnerZennium.setIEScanRate(0.05) zahnerZennium.setIEMaximumCurrent(3) zahnerZennium.setIEMinimumCurrent(-3) @@ -50,33 +45,36 @@ zahnerZennium.checkIESetup() print(zahnerZennium.readIESetup()) - + zahnerZennium.measureIE() zahnerZennium.setIESweepMode("dynamic scan") zahnerZennium.setIEOutputFileName("ie_dynamic") - + zahnerZennium.checkIESetup() print(zahnerZennium.readIESetup()) - + zahnerZennium.measureIE() zahnerZennium.setIESweepMode("fixed sampling") zahnerZennium.setIEOutputFileName("ie_fixed") - + zahnerZennium.checkIESetup() print(zahnerZennium.readIESetup()) - + zahnerZennium.measureIE() zenniumConnection.disconnectFromTerm() measurementData = IssImport(r"C:\THALES\temp\ie\ie_steady.iss") - fig, (axis) = plt.subplots(1, 1) - axis.semilogy(measurementData.getVoltageArray(), abs(measurementData.getCurrentArray()), color = "red") - + axis.semilogy( + measurementData.getVoltageArray(), + abs(measurementData.getCurrentArray()), + color="red", + ) + axis.grid(which="both") axis.xaxis.set_major_formatter(EngFormatter(unit="V")) axis.yaxis.set_major_formatter(EngFormatter(unit="A")) @@ -86,4 +84,3 @@ plt.show() print("finish") - diff --git a/Examples/CyclicVoltammetry/CyclicVoltammetry.ipynb b/Examples/CyclicVoltammetry/CyclicVoltammetry.ipynb index 86e6c94..0ce7a88 100644 --- a/Examples/CyclicVoltammetry/CyclicVoltammetry.ipynb +++ b/Examples/CyclicVoltammetry/CyclicVoltammetry.ipynb @@ -33,26 +33,13 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "if __name__ == \"__main__\":\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", + " zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", " \n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()\n", @@ -281,11 +268,9 @@ } ], "metadata": { - "interpreter": { - "hash": "ac59ebe37160ed0dfa835113d9b8498d9f09ceb179beaac4002f036b9467c963" - }, "kernelspec": { - "display_name": "Python 3.9.7 64-bit", + "display_name": "Python 3", + "language": "python", "name": "python3" }, "language_info": { @@ -298,9 +283,14 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.1 (tags/v3.11.1:a7a450f, Dec 6 2022, 19:58:39) [MSC v.1934 64 bit (AMD64)]" }, - "orig_nbformat": 4 + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "5238573367df39f7286bb46f9ff5f08f63a01a80960060ce41e3c79b190280fa" + } + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/Examples/CyclicVoltammetry/CyclicVoltammetry.py b/Examples/CyclicVoltammetry/CyclicVoltammetry.py index 135937d..cfb9480 100644 --- a/Examples/CyclicVoltammetry/CyclicVoltammetry.py +++ b/Examples/CyclicVoltammetry/CyclicVoltammetry.py @@ -4,13 +4,8 @@ if __name__ == "__main__": zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm("localhost", "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() - + zenniumConnection.connectToTerm("localhost", "ScriptRemote") + zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() zahnerZennium.calibrateOffsets() @@ -25,46 +20,47 @@ zahnerZennium.setCVUpperReversingPotential(2) zahnerZennium.setCVLowerReversingPotential(0) zahnerZennium.setCVEndPotential(1) - + zahnerZennium.setCVStartHoldTime(2) zahnerZennium.setCVEndHoldTime(2) - + zahnerZennium.setCVCycles(1.5) zahnerZennium.setCVSamplesPerCycle(400) zahnerZennium.setCVScanRate(0.5) - + zahnerZennium.setCVMaximumCurrent(0.03) zahnerZennium.setCVMinimumCurrent(-0.03) - + zahnerZennium.setCVOhmicDrop(0) - + zahnerZennium.disableCVAutoRestartAtCurrentOverflow() zahnerZennium.disableCVAutoRestartAtCurrentUnderflow() zahnerZennium.disableCVAnalogFunctionGenerator() zahnerZennium.checkCVSetup() print(zahnerZennium.readCVSetup()) - + zahnerZennium.measureCV() zahnerZennium.selectPotentiostat(1) zahnerZennium.setCVNaming("individual") zahnerZennium.setCVOutputPath(r"C:\THALES\temp\cv") - + ScanRatesForMeasurement = [0.1, 0.2, 0.5, 1.0] for scanRate in ScanRatesForMeasurement: - zahnerZennium.setCVOutputFileName("cv_scanrate_{:d}mVs".format(int(scanRate * 1000))) + zahnerZennium.setCVOutputFileName( + "cv_scanrate_{:d}mVs".format(int(scanRate * 1000)) + ) zahnerZennium.setCVScanRate(scanRate) - + zahnerZennium.checkCVSetup() print(zahnerZennium.readCVSetup()) - + zahnerZennium.measureCV() zahnerZennium.selectPotentiostat(0) - + zenniumConnection.disconnectFromTerm() print("finish") - diff --git a/Examples/DCSequencer/DCSequencer.ipynb b/Examples/DCSequencer/DCSequencer.ipynb index e47c509..72c7552 100644 --- a/Examples/DCSequencer/DCSequencer.ipynb +++ b/Examples/DCSequencer/DCSequencer.ipynb @@ -34,27 +34,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "if __name__ == \"__main__\":\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", - " \n", + " zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", + " \n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()" ] @@ -215,11 +202,9 @@ } ], "metadata": { - "interpreter": { - "hash": "ac59ebe37160ed0dfa835113d9b8498d9f09ceb179beaac4002f036b9467c963" - }, "kernelspec": { - "display_name": "Python 3.9.7 64-bit", + "display_name": "Python 3", + "language": "python", "name": "python3" }, "language_info": { @@ -232,9 +217,14 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.1 (tags/v3.11.1:a7a450f, Dec 6 2022, 19:58:39) [MSC v.1934 64 bit (AMD64)]" }, - "orig_nbformat": 4 + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "5238573367df39f7286bb46f9ff5f08f63a01a80960060ce41e3c79b190280fa" + } + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/Examples/DCSequencer/DCSequencer.py b/Examples/DCSequencer/DCSequencer.py index 22ca529..381f88d 100644 --- a/Examples/DCSequencer/DCSequencer.py +++ b/Examples/DCSequencer/DCSequencer.py @@ -4,13 +4,8 @@ if __name__ == "__main__": zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm("localhost", "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() - + zenniumConnection.connectToTerm("localhost", "ScriptRemote") + zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() @@ -33,7 +28,6 @@ zahnerZennium.runSequenceFile(r"C:\Users\XXX\Desktop\myZahnerSequence.seq") zahnerZennium.selectPotentiostat(0) - + zenniumConnection.disconnectFromTerm() print("finish") - diff --git a/Examples/EIS/EIS.ipynb b/Examples/EIS/EIS.ipynb index 2941aaa..0a1effe 100644 --- a/Examples/EIS/EIS.ipynb +++ b/Examples/EIS/EIS.ipynb @@ -33,27 +33,14 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "if __name__ == \"__main__\":\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", - " \n", + " zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", + " \n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()" ] @@ -278,11 +265,9 @@ } ], "metadata": { - "interpreter": { - "hash": "ac59ebe37160ed0dfa835113d9b8498d9f09ceb179beaac4002f036b9467c963" - }, "kernelspec": { - "display_name": "Python 3.9.7 64-bit", + "display_name": "Python 3", + "language": "python", "name": "python3" }, "language_info": { @@ -295,9 +280,14 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.1 (tags/v3.11.1:a7a450f, Dec 6 2022, 19:58:39) [MSC v.1934 64 bit (AMD64)]" }, - "orig_nbformat": 4 + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "5238573367df39f7286bb46f9ff5f08f63a01a80960060ce41e3c79b190280fa" + } + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/Examples/EIS/EIS.py b/Examples/EIS/EIS.py index 77a0e50..bce8351 100644 --- a/Examples/EIS/EIS.py +++ b/Examples/EIS/EIS.py @@ -1,16 +1,11 @@ import sys from thales_remote.connection import ThalesRemoteConnection -from thales_remote.script_wrapper import PotentiostatMode,ThalesRemoteScriptWrapper +from thales_remote.script_wrapper import PotentiostatMode, ThalesRemoteScriptWrapper if __name__ == "__main__": zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm("localhost", "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() - + zenniumConnection.connectToTerm("localhost", "ScriptRemote") + zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() @@ -35,9 +30,9 @@ zahnerZennium.setScanStrategy("single") zahnerZennium.enablePotentiostat() - + zahnerZennium.measureEIS() - + zahnerZennium.disablePotentiostat() zahnerZennium.selectPotentiostat(1) @@ -48,14 +43,14 @@ zahnerZennium.setAmplitude(50e-3) zahnerZennium.setScanDirection("startToMin") - + zahnerZennium.measureEIS() zahnerZennium.setEISNaming("individual") zahnerZennium.setEISOutputPath(r"C:\THALES\temp\test3") - + AmplitudesIn_mV_forMeasurement = [5, 10, 20, 50] - + for amplitude in AmplitudesIn_mV_forMeasurement: zahnerZennium.setEISOutputFileName("spectraAmplitude{}mV".format(amplitude)) zahnerZennium.setAmplitude(amplitude / 1000) @@ -64,7 +59,6 @@ zahnerZennium.setAmplitude(0) zahnerZennium.selectPotentiostat(0) - + zenniumConnection.disconnectFromTerm() print("finish") - diff --git a/Examples/EISCVLaTeX/EISCVLaTeX.ipynb b/Examples/EISCVLaTeX/EISCVLaTeX.ipynb index 6ffb32f..16445ba 100644 --- a/Examples/EISCVLaTeX/EISCVLaTeX.ipynb +++ b/Examples/EISCVLaTeX/EISCVLaTeX.ipynb @@ -69,12 +69,7 @@ " remoteIP = \"localhost\"\n", "\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(remoteIP, \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", + " zenniumConnection.connectToTerm(remoteIP, \"ScriptRemote\")\n", " \n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()\n", @@ -180,7 +175,7 @@ } ], "source": [ - " CVmeasurementData = IscImport(measDataInterface.getLatestReceivedFile()[\"binary_data\"])\n", + " CVmeasurementData = IscImport(measDataInterface.getLatestReceivedFile().binaryData)\n", " maximumCurrent = max(CVmeasurementData.getCurrentArray())\n", " minimumCurrent = min(CVmeasurementData.getCurrentArray())\n", " \n", @@ -278,7 +273,7 @@ } ], "source": [ - " EISmeasurementData = IsmImport(measDataInterface.getLatestReceivedFile()[\"binary_data\"])\n", + " EISmeasurementData = IsmImport(measDataInterface.getLatestReceivedFile().binaryData)\n", " \n", " zenniumConnection.disconnectFromTerm()\n", " measDataInterface.close()\n", @@ -445,11 +440,8 @@ } ], "metadata": { - "interpreter": { - "hash": "b89b5cfaba6639976dc87ff2fec6d58faec662063367e2c229c520fe71072417" - }, "kernelspec": { - "display_name": "Python 3.10.0 64-bit", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -463,9 +455,14 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.10.5 (tags/v3.10.5:f377153, Jun 6 2022, 16:14:13) [MSC v.1929 64 bit (AMD64)]" }, - "orig_nbformat": 4 + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "26de051ba29f2982a8de78e945f0abaf191376122a1563185a90213a26c5da77" + } + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/Examples/EISCVLaTeX/EISCVLaTeX.py b/Examples/EISCVLaTeX/EISCVLaTeX.py index 1ed092d..851d4b6 100644 --- a/Examples/EISCVLaTeX/EISCVLaTeX.py +++ b/Examples/EISCVLaTeX/EISCVLaTeX.py @@ -2,7 +2,7 @@ import os from thales_remote.connection import ThalesRemoteConnection -from thales_remote.script_wrapper import ThalesRemoteScriptWrapper,PotentiostatMode +from thales_remote.script_wrapper import ThalesRemoteScriptWrapper, PotentiostatMode from thales_remote.file_interface import ThalesFileInterface from zahner_analysis.file_import.isc_import import IscImport @@ -18,13 +18,8 @@ remoteIP = "localhost" zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm(remoteIP, "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() - + zenniumConnection.connectToTerm(remoteIP, "ScriptRemote") + zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() @@ -32,61 +27,66 @@ measDataInterface.disableSaveReceivedFilesToDisk() measDataInterface.enableKeepReceivedFilesInObject() measDataInterface.enableAutomaticFileExchange(fileExtensions="*.ism*.isc") - + zahnerZennium.calibrateOffsets() - zahnerZennium.setCVStartPotential(0) zahnerZennium.setCVUpperReversingPotential(1) zahnerZennium.setCVLowerReversingPotential(0) zahnerZennium.setCVEndPotential(0) - + zahnerZennium.setCVStartHoldTime(2) zahnerZennium.setCVEndHoldTime(2) - + zahnerZennium.setCVCycles(1.5) zahnerZennium.setCVSamplesPerCycle(400) - + zahnerZennium.setCVMaximumCurrent(1e-3) zahnerZennium.setCVMinimumCurrent(-1e-3) - + zahnerZennium.setCVOhmicDrop(0) - + zahnerZennium.disableCVAutoRestartAtCurrentOverflow() zahnerZennium.disableCVAutoRestartAtCurrentUnderflow() zahnerZennium.disableCVAnalogFunctionGenerator() - + zahnerZennium.setCVNaming("individual") zahnerZennium.setCVOutputPath(r"C:\THALES\temp\cv") zahnerZennium.setCVOutputFileName("cv_measurement") - + scanRate = 0.25 - + zahnerZennium.setCVScanRate(scanRate) - + zahnerZennium.checkCVSetup() zahnerZennium.measureCV() - CVmeasurementData = IscImport(measDataInterface.getLatestReceivedFile()["binary_data"]) + CVmeasurementData = IscImport(measDataInterface.getLatestReceivedFile().binaryData) maximumCurrent = max(CVmeasurementData.getCurrentArray()) minimumCurrent = min(CVmeasurementData.getCurrentArray()) - - capacitance = ((maximumCurrent-minimumCurrent)/2)/scanRate - + + capacitance = ((maximumCurrent - minimumCurrent) / 2) / scanRate + capacitanceFormatter = EngFormatter(places=3, unit="F") print(f"{capacitanceFormatter.format_data(capacitance)} Capacitor") - + figCV, (axis) = plt.subplots(1, 1) - figCV.suptitle(f"Cyclic Voltammetry {capacitanceFormatter.format_data(capacitance)} Capacitor") - - axis.plot(CVmeasurementData.getVoltageArray(), CVmeasurementData.getCurrentArray(), color = "red") - + figCV.suptitle( + f"Cyclic Voltammetry {capacitanceFormatter.format_data(capacitance)} Capacitor" + ) + + axis.plot( + CVmeasurementData.getVoltageArray(), + CVmeasurementData.getCurrentArray(), + color="red", + ) + axis.grid(which="both") axis.xaxis.set_major_formatter(EngFormatter(unit="V")) axis.yaxis.set_major_formatter(EngFormatter(unit="A")) axis.set_xlabel(r"Voltage") axis.set_ylabel(r"Current") - + figCV.set_size_inches(10, 10) plt.show() figCV.savefig("CV.pdf") @@ -94,7 +94,7 @@ zahnerZennium.setEISNaming("individual") zahnerZennium.setEISOutputPath(r"C:\THALES\temp") zahnerZennium.setEISOutputFileName("eis_measurement") - + zahnerZennium.setPotentiostatMode(PotentiostatMode.POTMODE_POTENTIOSTATIC) zahnerZennium.setAmplitude(10e-3) zahnerZennium.setPotential(0) @@ -107,27 +107,29 @@ zahnerZennium.setUpperStepsPerDecade(5) zahnerZennium.setScanDirection("startToMin") zahnerZennium.setScanStrategy("single") - + zahnerZennium.enablePotentiostat() zahnerZennium.measureEIS() zahnerZennium.disablePotentiostat() - + zahnerZennium.setAmplitude(0) - EISmeasurementData = IsmImport(measDataInterface.getLatestReceivedFile()["binary_data"]) - + EISmeasurementData = IsmImport(measDataInterface.getLatestReceivedFile().binaryData) + zenniumConnection.disconnectFromTerm() measDataInterface.close() - + impedanceFrequencies = EISmeasurementData.getFrequencyArray() impedanceAbsolute = EISmeasurementData.getImpedanceArray() impedancePhase = EISmeasurementData.getPhaseArray() figBode, (impedanceAxis) = plt.subplots(1, 1) figBode.suptitle(f"EIS {capacitanceFormatter.format_data(capacitance)} Capacitor") - + phaseAxis = impedanceAxis.twinx() - - impedanceAxis.loglog(impedanceFrequencies, impedanceAbsolute, marker="o", markersize=3, color = "blue") + + impedanceAxis.loglog( + impedanceFrequencies, impedanceAbsolute, marker="o", markersize=3, color="blue" + ) impedanceAxis.xaxis.set_major_formatter(EngFormatter(unit="Hz")) impedanceAxis.yaxis.set_major_formatter(EngFormatter(unit="$\Omega$")) impedanceAxis.set_xlabel(r"f") @@ -135,8 +137,14 @@ impedanceAxis.yaxis.label.set_color("blue") impedanceAxis.grid(which="both") impedanceAxis.set_xlim([min(impedanceFrequencies), max(impedanceFrequencies)]) - - phaseAxis.semilogx(impedanceFrequencies, np.abs(impedancePhase * (360 / (2 * np.pi))), marker="o", markersize=3, color = "red") + + phaseAxis.semilogx( + impedanceFrequencies, + np.abs(impedancePhase * (360 / (2 * np.pi))), + marker="o", + markersize=3, + color="red", + ) phaseAxis.yaxis.set_major_formatter(EngFormatter(unit="$°$", sep="")) phaseAxis.xaxis.set_major_formatter(EngFormatter(unit="Hz")) phaseAxis.set_xlabel(r"f") @@ -148,53 +156,59 @@ figBode.savefig("EIS.pdf") mpl.rcParams["axes.unicode_minus"] = False - prefixFormatter = EngFormatter(places=3, sep = "") + prefixFormatter = EngFormatter(places=3, sep="") defaultMu = prefixFormatter.ENG_PREFIXES[-6] - prefixFormatter.ENG_PREFIXES[-6] = "\\textmu" #LaTeX notation for micro - - with open("EIS.csv","wb") as file: + prefixFormatter.ENG_PREFIXES[-6] = "\\textmu" # LaTeX notation for micro + + with open("EIS.csv", "wb") as file: file.write(bytearray("Frequency;Impedance;Phase" + os.linesep, "utf-8")) - - for freq, imp, phase in zip(impedanceFrequencies, impedanceAbsolute, impedancePhase): - file.write(bytearray(f"{prefixFormatter.format_data(freq)};{prefixFormatter.format_data(imp)};{prefixFormatter.format_data(phase * (360 / (2 * np.pi)))}" + os.linesep, "utf-8")) - + + for freq, imp, phase in zip( + impedanceFrequencies, impedanceAbsolute, impedancePhase + ): + file.write( + bytearray( + f"{prefixFormatter.format_data(freq)};{prefixFormatter.format_data(imp)};{prefixFormatter.format_data(phase * (360 / (2 * np.pi)))}" + + os.linesep, + "utf-8", + ) + ) objectName = input("Input Test Object Identifier:") latex_jinja_env = jinja2.Environment( - variable_start_string = '\PYVAR{', - variable_end_string = '}', - trim_blocks = True, - autoescape = False, - loader = jinja2.FileSystemLoader(os.path.abspath('.')) + variable_start_string="\PYVAR{", + variable_end_string="}", + trim_blocks=True, + autoescape=False, + loader=jinja2.FileSystemLoader(os.path.abspath(".")), ) - + template = latex_jinja_env.get_template(r"report.tex") currentFormatter = EngFormatter(unit="A") - currentFormatter.ENG_PREFIXES[-6] = "\\mu " #LaTeX notation for micro - + currentFormatter.ENG_PREFIXES[-6] = "\\mu " # LaTeX notation for micro + fileString = template.render( - objectname = objectName, - capacitance = capacitanceFormatter.format_data(capacitance), - cv_filename = "CV.pdf", - eis_filename = "EIS.pdf", - eis_csv_filename = "EIS.csv", - cv_maximum_current = currentFormatter.format_data(maximumCurrent), - cv_minimum_current = currentFormatter.format_data(minimumCurrent), - cv_scanrate = scanRate - ) - + objectname=objectName, + capacitance=capacitanceFormatter.format_data(capacitance), + cv_filename="CV.pdf", + eis_filename="EIS.pdf", + eis_csv_filename="EIS.csv", + cv_maximum_current=currentFormatter.format_data(maximumCurrent), + cv_minimum_current=currentFormatter.format_data(minimumCurrent), + cv_scanrate=scanRate, + ) + fileString = bytearray(fileString, "utf-8") f = open("report_filled.tex", "wb") f.write(fileString) f.close() - + # Only needed for Jupyter, that the kernel does not have to be restarted all the time. currentFormatter.ENG_PREFIXES[-6] = defaultMu - - command = f"pdflatex.exe report_filled.tex -jobname=\"{objectName}\"" + + command = f'pdflatex.exe report_filled.tex -jobname="{objectName}"' os.system(command) os.popen(f"{objectName}.pdf") print("finish") - diff --git a/Examples/EISImportPlot/EISImportPlot.ipynb b/Examples/EISImportPlot/EISImportPlot.ipynb index 6a0f8de..4244a2c 100644 --- a/Examples/EISImportPlot/EISImportPlot.ipynb +++ b/Examples/EISImportPlot/EISImportPlot.ipynb @@ -39,28 +39,15 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "id": "0eee3d81", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "if __name__ == \"__main__\":\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", - " \n", + " zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", + " \n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()\n", " \n", @@ -313,7 +300,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.10.5 (tags/v3.10.5:f377153, Jun 6 2022, 16:14:13) [MSC v.1929 64 bit (AMD64)]" } }, "nbformat": 4, diff --git a/Examples/EISImportPlot/EISImportPlot.py b/Examples/EISImportPlot/EISImportPlot.py index 258bdfa..23d39bc 100644 --- a/Examples/EISImportPlot/EISImportPlot.py +++ b/Examples/EISImportPlot/EISImportPlot.py @@ -1,25 +1,20 @@ import sys from thales_remote.connection import ThalesRemoteConnection -from thales_remote.script_wrapper import PotentiostatMode,ThalesRemoteScriptWrapper +from thales_remote.script_wrapper import PotentiostatMode, ThalesRemoteScriptWrapper from zahner_analysis.file_import.ism_import import IsmImport -from zahner_analysis.plotting.impedance_plot import nyquistPlotter,bodePlotter +from zahner_analysis.plotting.impedance_plot import nyquistPlotter, bodePlotter import matplotlib.pyplot as plt import numpy as np if __name__ == "__main__": zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm("localhost", "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() - + zenniumConnection.connectToTerm("localhost", "ScriptRemote") + zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() - + zahnerZennium.calibrateOffsets() zahnerZennium.setEISNaming("counter") @@ -43,24 +38,24 @@ zahnerZennium.enablePotentiostat() zahnerZennium.measureEIS() zahnerZennium.disablePotentiostat() - + zahnerZennium.setAmplitude(0) - + zenniumConnection.disconnectFromTerm() ismFile = IsmImport(r"C:\THALES\temp\test1\spectra_0001.ism") - + impedanceFrequencies = ismFile.getFrequencyArray() - + impedanceAbsolute = ismFile.getImpedanceArray() impedancePhase = ismFile.getPhaseArray() - + impedanceComplex = ismFile.getComplexImpedanceArray() print("Measurement end time: " + str(ismFile.getMeasurementEndDateTime())) (figNyquist, nyquistAxis) = nyquistPlotter(impedanceObject=ismFile) - + figNyquist.suptitle("Nyquist") figNyquist.set_size_inches(18, 18) @@ -69,11 +64,10 @@ figNyquist.savefig("nyquist.svg") (figBode, (impedanceAxis, phaseAxis)) = bodePlotter(impedanceObject=ismFile) - + figBode.suptitle("Bode") figBode.set_size_inches(18, 12) - + plt.show() - - figBode.savefig("bode.svg") + figBode.savefig("bode.svg") diff --git a/Examples/EISPad4/EISPad4.ipynb b/Examples/EISPad4/EISPad4.ipynb index b465843..e14d44f 100644 --- a/Examples/EISPad4/EISPad4.ipynb +++ b/Examples/EISPad4/EISPad4.ipynb @@ -43,27 +43,14 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "id": "english-secretariat", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "if __name__ == \"__main__\":\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", + " zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", " \n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()\n", @@ -269,11 +256,9 @@ } ], "metadata": { - "interpreter": { - "hash": "ac59ebe37160ed0dfa835113d9b8498d9f09ceb179beaac4002f036b9467c963" - }, "kernelspec": { - "display_name": "Python 3.9.7 64-bit", + "display_name": "Python 3", + "language": "python", "name": "python3" }, "language_info": { @@ -286,7 +271,12 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.1 (tags/v3.11.1:a7a450f, Dec 6 2022, 19:58:39) [MSC v.1934 64 bit (AMD64)]" + }, + "vscode": { + "interpreter": { + "hash": "5238573367df39f7286bb46f9ff5f08f63a01a80960060ce41e3c79b190280fa" + } } }, "nbformat": 4, diff --git a/Examples/EISPad4/EISPad4.py b/Examples/EISPad4/EISPad4.py index 94d46cb..56ae941 100644 --- a/Examples/EISPad4/EISPad4.py +++ b/Examples/EISPad4/EISPad4.py @@ -1,6 +1,6 @@ import sys from thales_remote.connection import ThalesRemoteConnection -from thales_remote.script_wrapper import PotentiostatMode,ThalesRemoteScriptWrapper +from thales_remote.script_wrapper import PotentiostatMode, ThalesRemoteScriptWrapper from zahner_analysis.file_import.ism_import import IsmImport import matplotlib.pyplot as plt @@ -9,16 +9,11 @@ if __name__ == "__main__": zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm("localhost", "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() - + zenniumConnection.connectToTerm("localhost", "ScriptRemote") + zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() - + zahnerZennium.calibrateOffsets() zahnerZennium.setEISNaming("counter") @@ -39,8 +34,8 @@ zahnerZennium.setScanDirection("startToMax") zahnerZennium.setScanStrategy("single") - zahnerZennium.setupPAD4(1,1,1) - zahnerZennium.setupPAD4(1,2,1) + zahnerZennium.setupPAD4(1, 1, 1) + zahnerZennium.setupPAD4(1, 2, 1) zahnerZennium.enablePAD4() zahnerZennium.enablePotentiostat() @@ -54,13 +49,13 @@ impedanceAbsoluteStack = ismFileStack.getImpedanceArray() impedancePhaseStack = ismFileStack.getPhaseArray() impedanceComplexStack = ismFileStack.getComplexImpedanceArray() - + ismFileCell1 = IsmImport(r"C:\THALES\temp\test1\spectra_cells_0001_ser01.ism") impedanceFrequenciesCell1 = ismFileCell1.getFrequencyArray() impedanceAbsoluteCell1 = ismFileCell1.getImpedanceArray() impedancePhaseCell1 = ismFileCell1.getPhaseArray() impedanceComplexCell1 = ismFileCell1.getComplexImpedanceArray() - + ismFileCell2 = IsmImport(r"C:\THALES\temp\test1\spectra_cells_0001_ser02.ism") impedanceFrequenciesCell2 = ismFileCell2.getFrequencyArray() impedanceAbsoluteCell2 = ismFileCell2.getImpedanceArray() @@ -69,19 +64,36 @@ figNyquist, (nyquistAxis) = plt.subplots(1, 1) figNyquist.suptitle("Nyquist") - - nyquistAxis.plot(np.real(impedanceComplexStack), -np.imag(impedanceComplexStack), marker="x", markersize=5, label="Stack") - nyquistAxis.plot(np.real(impedanceComplexCell1), -np.imag(impedanceComplexCell1), marker="x", markersize=5, label="Cell 1") - nyquistAxis.plot(np.real(impedanceComplexCell2), -np.imag(impedanceComplexCell2), marker="x", markersize=5, label="Cell 2") - + + nyquistAxis.plot( + np.real(impedanceComplexStack), + -np.imag(impedanceComplexStack), + marker="x", + markersize=5, + label="Stack", + ) + nyquistAxis.plot( + np.real(impedanceComplexCell1), + -np.imag(impedanceComplexCell1), + marker="x", + markersize=5, + label="Cell 1", + ) + nyquistAxis.plot( + np.real(impedanceComplexCell2), + -np.imag(impedanceComplexCell2), + marker="x", + markersize=5, + label="Cell 2", + ) + nyquistAxis.grid(which="both") nyquistAxis.set_aspect("equal") nyquistAxis.xaxis.set_major_formatter(EngFormatter(unit="$\Omega$")) nyquistAxis.yaxis.set_major_formatter(EngFormatter(unit="$\Omega$")) nyquistAxis.set_xlabel(r"$Z_{\rm re}$") nyquistAxis.set_ylabel(r"$-Z_{\rm im}$") - nyquistAxis.legend(fontsize = "large") + nyquistAxis.legend(fontsize="large") figNyquist.set_size_inches(20, 8) plt.show() figNyquist.savefig("nyquist.svg") - diff --git a/Examples/EISvsParameter/EISvsParameter.ipynb b/Examples/EISvsParameter/EISvsParameter.ipynb index 7fb18f2..eb9f0bb 100644 --- a/Examples/EISvsParameter/EISvsParameter.ipynb +++ b/Examples/EISvsParameter/EISvsParameter.ipynb @@ -44,27 +44,14 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "21b7744e", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "if __name__ == \"__main__\":\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", + " zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", " \n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()\n", @@ -367,11 +354,9 @@ } ], "metadata": { - "interpreter": { - "hash": "ac59ebe37160ed0dfa835113d9b8498d9f09ceb179beaac4002f036b9467c963" - }, "kernelspec": { - "display_name": "Python 3.9.7 64-bit", + "display_name": "Python 3", + "language": "python", "name": "python3" }, "language_info": { @@ -384,7 +369,12 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.1 (tags/v3.11.1:a7a450f, Dec 6 2022, 19:58:39) [MSC v.1934 64 bit (AMD64)]" + }, + "vscode": { + "interpreter": { + "hash": "5238573367df39f7286bb46f9ff5f08f63a01a80960060ce41e3c79b190280fa" + } } }, "nbformat": 4, diff --git a/Examples/EISvsParameter/EISvsParameter.py b/Examples/EISvsParameter/EISvsParameter.py index f4f2b40..1d11765 100644 --- a/Examples/EISvsParameter/EISvsParameter.py +++ b/Examples/EISvsParameter/EISvsParameter.py @@ -1,30 +1,25 @@ import sys from thales_remote.connection import ThalesRemoteConnection -from thales_remote.script_wrapper import PotentiostatMode,ThalesRemoteScriptWrapper +from thales_remote.script_wrapper import PotentiostatMode, ThalesRemoteScriptWrapper from zahner_analysis.file_import.ism_import import IsmImport import matplotlib.pyplot as plt import numpy as np from matplotlib.ticker import EngFormatter, StrMethodFormatter -from matplotlib import ticker,colors,cm +from matplotlib import ticker, colors, cm if __name__ == "__main__": zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm("localhost", "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() - + zenniumConnection.connectToTerm("localhost", "ScriptRemote") + zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() - + zahnerZennium.calibrateOffsets() zahnerZennium.setEISNaming("individual") zahnerZennium.setEISOutputPath(r"C:\THALES\temp") - + zahnerZennium.setPotentiostatMode(PotentiostatMode.POTMODE_POTENTIOSTATIC) zahnerZennium.setAmplitude(10e-3) zahnerZennium.setLowerFrequencyLimit(100) @@ -37,54 +32,75 @@ zahnerZennium.setScanDirection("startToMin") zahnerZennium.setScanStrategy("single") - potentialsToMeasure = np.linspace(0,0.3,13) + potentialsToMeasure = np.linspace(0, 0.3, 13) print(potentialsToMeasure) for potential in potentialsToMeasure: - filename = "{:d}_mvdc".format(int(round(potential*1000))) + filename = "{:d}_mvdc".format(int(round(potential * 1000))) print("step: " + filename) zahnerZennium.setEISOutputFileName(filename) zahnerZennium.setPotential(potential) - + zahnerZennium.enablePotentiostat() zahnerZennium.measureEIS() zahnerZennium.disablePotentiostat() - + zahnerZennium.setAmplitude(0) zenniumConnection.disconnectFromTerm() absoluteImpedances = [] phases = [] - + for potential in potentialsToMeasure: - ismFile = IsmImport(r"C:\THALES\temp\{:d}_mvdc.ism".format(int(round(potential*1000)))) + ismFile = IsmImport( + r"C:\THALES\temp\{:d}_mvdc.ism".format(int(round(potential * 1000))) + ) absoluteImpedances.append(ismFile.getImpedanceArray()) phases.append(ismFile.getPhaseArray()) absoluteImpedances = np.array(absoluteImpedances) phases = np.array(phases) phases = np.abs(phases * (360 / (2 * np.pi))) - + impedanceFrequencies = ismFile.getFrequencyArray() - - X,Y = np.meshgrid(impedanceFrequencies,potentialsToMeasure) - impedanceFigure, impedancePlot = plt.subplots(1,1) + X, Y = np.meshgrid(impedanceFrequencies, potentialsToMeasure) + + impedanceFigure, impedancePlot = plt.subplots(1, 1) impedanceFigure.suptitle("Impedance vs. DC Voltage vs. Frequency") - - ticks = np.power(10, np.arange(np.floor(np.log10(absoluteImpedances.min())-1), np.ceil(np.log10(absoluteImpedances.max())+1))) - levels = np.logspace(np.floor(np.log10(absoluteImpedances.min())-1), np.ceil(np.log10(absoluteImpedances.max())), num=200) - impedanceContour = impedancePlot.contourf(X, Y, absoluteImpedances, levels = levels, norm = colors.LogNorm(absoluteImpedances.min(), absoluteImpedances.max(), True), cmap="jet") - + + ticks = np.power( + 10, + np.arange( + np.floor(np.log10(absoluteImpedances.min()) - 1), + np.ceil(np.log10(absoluteImpedances.max()) + 1), + ), + ) + levels = np.logspace( + np.floor(np.log10(absoluteImpedances.min()) - 1), + np.ceil(np.log10(absoluteImpedances.max())), + num=200, + ) + impedanceContour = impedancePlot.contourf( + X, + Y, + absoluteImpedances, + levels=levels, + norm=colors.LogNorm(absoluteImpedances.min(), absoluteImpedances.max(), True), + cmap="jet", + ) + impedancePlot.set_xlabel(r"Frequency") impedancePlot.set_xscale("log") impedancePlot.xaxis.set_major_formatter(EngFormatter(unit="Hz")) - + impedancePlot.set_ylabel(r"DC Voltage") impedancePlot.yaxis.set_major_formatter(EngFormatter(unit="V")) - - impedanceBar = impedanceFigure.colorbar(impedanceContour, ticks=ticks, format=EngFormatter(unit="$\Omega$")) - impedanceBar.set_label('| Impedance |') + + impedanceBar = impedanceFigure.colorbar( + impedanceContour, ticks=ticks, format=EngFormatter(unit="$\Omega$") + ) + impedanceBar.set_label("| Impedance |") impedanceFigure.set_size_inches(14, 12) plt.tight_layout() plt.show() @@ -94,7 +110,7 @@ phaseFigure.suptitle("Phase vs. DC Voltage vs. Frequency") levels = np.linspace(phases.min(), phases.max(), 91) - phaseContour = phasePlot.contourf(X, Y, phases, levels = levels, cmap="jet") + phaseContour = phasePlot.contourf(X, Y, phases, levels=levels, cmap="jet") phasePlot.set_xlabel(r"Frequency") phasePlot.set_xscale("log") @@ -103,11 +119,12 @@ phasePlot.set_ylabel(r"DC Voltage") phasePlot.yaxis.set_major_formatter(EngFormatter(unit="V")) - phaseBar = phaseFigure.colorbar(phaseContour, format=StrMethodFormatter("{x:.0f}$°$")) + phaseBar = phaseFigure.colorbar( + phaseContour, format=StrMethodFormatter("{x:.0f}$°$") + ) phaseBar.set_label("| Phase |") phaseFigure.set_size_inches(14, 12) plt.tight_layout() plt.show() phaseFigure.savefig("phase_contour.svg") - diff --git a/Examples/ExternalDeviceFRA/ExternalDeviceFRA.ipynb b/Examples/ExternalDeviceFRA/ExternalDeviceFRA.ipynb index 63c6bae..8673e17 100644 --- a/Examples/ExternalDeviceFRA/ExternalDeviceFRA.ipynb +++ b/Examples/ExternalDeviceFRA/ExternalDeviceFRA.ipynb @@ -32,28 +32,15 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "\n", "if __name__ == \"__main__\":\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", - "\n", + " zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", + " \n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()" ] @@ -205,11 +192,8 @@ } ], "metadata": { - "interpreter": { - "hash": "b89b5cfaba6639976dc87ff2fec6d58faec662063367e2c229c520fe71072417" - }, "kernelspec": { - "display_name": "Python 3.10.2 64-bit", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -223,9 +207,14 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.11.1 (tags/v3.11.1:a7a450f, Dec 6 2022, 19:58:39) [MSC v.1934 64 bit (AMD64)]" }, - "orig_nbformat": 4 + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "5238573367df39f7286bb46f9ff5f08f63a01a80960060ce41e3c79b190280fa" + } + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/Examples/ExternalDeviceFRA/ExternalDeviceFRA.py b/Examples/ExternalDeviceFRA/ExternalDeviceFRA.py index fa30d91..fbe4d74 100644 --- a/Examples/ExternalDeviceFRA/ExternalDeviceFRA.py +++ b/Examples/ExternalDeviceFRA/ExternalDeviceFRA.py @@ -1,33 +1,28 @@ import sys from thales_remote.connection import ThalesRemoteConnection -from thales_remote.script_wrapper import PotentiostatMode,ThalesRemoteScriptWrapper +from thales_remote.script_wrapper import PotentiostatMode, ThalesRemoteScriptWrapper import time if __name__ == "__main__": zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm("localhost", "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() + zenniumConnection.connectToTerm("localhost", "ScriptRemote") zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() zahnerZennium.disableFraMode() - + zahnerZennium.setFraVoltageMinimum(0) zahnerZennium.setFraVoltageMaximum(18) zahnerZennium.setFraCurrentMinimum(0) zahnerZennium.setFraCurrentMaximum(220) - - zahnerZennium.setFraVoltageInputGain(18.0/5.0) - zahnerZennium.setFraVoltageOutputGain(18.0/5.0) - zahnerZennium.setFraCurrentInputGain(-220.0/5.0) - zahnerZennium.setFraCurrentOutputGain(-220.0/5.0) - + + zahnerZennium.setFraVoltageInputGain(18.0 / 5.0) + zahnerZennium.setFraVoltageOutputGain(18.0 / 5.0) + zahnerZennium.setFraCurrentInputGain(-220.0 / 5.0) + zahnerZennium.setFraCurrentOutputGain(-220.0 / 5.0) + zahnerZennium.setFraPotentiostatMode(PotentiostatMode.POTMODE_GALVANOSTATIC) zahnerZennium.enableFraMode() @@ -54,11 +49,10 @@ zahnerZennium.setUpperStepsPerDecade(3) zahnerZennium.setScanDirection("startToMax") zahnerZennium.setScanStrategy("single") - + zahnerZennium.measureEIS() - + zahnerZennium.disableFraMode() zenniumConnection.disconnectFromTerm() print("finish") - diff --git a/Examples/FileExchangeEIS/FileExchangeEIS.ipynb b/Examples/FileExchangeEIS/FileExchangeEIS.ipynb index 806fddb..02186ac 100644 --- a/Examples/FileExchangeEIS/FileExchangeEIS.ipynb +++ b/Examples/FileExchangeEIS/FileExchangeEIS.ipynb @@ -43,28 +43,15 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "if __name__ == \"__main__\":\n", " remoteIP = \"192.168.2.66\"\n", - "\n", + " \n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(remoteIP, \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", + " zenniumConnection.connectToTerm(remoteIP, \"ScriptRemote\")\n", " \n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()\n", @@ -175,7 +162,7 @@ "outputs": [], "source": [ " fileHandle = open(r\"D:\\myLocalDirectory\\asdf.ism\",\"wb\")\n", - " fileHandle.write(file[\"binary_data\"])\n", + " fileHandle.write(file.binaryData)\n", " fileHandle.close()" ] }, @@ -205,7 +192,7 @@ } ], "source": [ - " ismFile = IsmImport(file[\"binary_data\"])\n", + " ismFile = IsmImport(file.binaryData)\n", " \n", " impedanceFrequencies = ismFile.getFrequencyArray()\n", " \n", @@ -368,8 +355,8 @@ ], "source": [ " for file in fileInterface.getReceivedFiles():\n", - " ismFile = IsmImport(file[\"binary_data\"])\n", - " print(f\"{file['name']} measurement finished at {ismFile.getMeasurementEndDateTime()}\")" + " ismFile = IsmImport(file.binaryData)\n", + " print(f\"{file.name} measurement finished at {ismFile.getMeasurementEndDateTime()}\")" ] }, { @@ -424,11 +411,9 @@ } ], "metadata": { - "interpreter": { - "hash": "b89b5cfaba6639976dc87ff2fec6d58faec662063367e2c229c520fe71072417" - }, "kernelspec": { - "display_name": "Python 3.10.0 64-bit", + "display_name": "Python 3", + "language": "python", "name": "python3" }, "language_info": { @@ -441,9 +426,14 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.11.1 (tags/v3.11.1:a7a450f, Dec 6 2022, 19:58:39) [MSC v.1934 64 bit (AMD64)]" }, - "orig_nbformat": 4 + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "5238573367df39f7286bb46f9ff5f08f63a01a80960060ce41e3c79b190280fa" + } + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/Examples/FileExchangeEIS/FileExchangeEIS.py b/Examples/FileExchangeEIS/FileExchangeEIS.py index 43a02c3..45ff744 100644 --- a/Examples/FileExchangeEIS/FileExchangeEIS.py +++ b/Examples/FileExchangeEIS/FileExchangeEIS.py @@ -1,6 +1,6 @@ import sys from thales_remote.connection import ThalesRemoteConnection -from thales_remote.script_wrapper import PotentiostatMode,ThalesRemoteScriptWrapper +from thales_remote.script_wrapper import PotentiostatMode, ThalesRemoteScriptWrapper from thales_remote.file_interface import ThalesFileInterface from zahner_analysis.file_import.ism_import import IsmImport @@ -14,13 +14,8 @@ remoteIP = "192.168.2.66" zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm(remoteIP, "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() - + zenniumConnection.connectToTerm(remoteIP, "ScriptRemote") + zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() @@ -48,35 +43,41 @@ zahnerZennium.enablePotentiostat() zahnerZennium.measureEIS() - + zahnerZennium.disablePotentiostat() zahnerZennium.setAmplitude(0) - file = fileInterface.aquireFile(r"C:\THALES\temp\myeis.ism") - fileHandle = open(r"D:\myLocalDirectory\asdf.ism","wb") - fileHandle.write(file["binary_data"]) + fileHandle = open(r"D:\myLocalDirectory\asdf.ism", "wb") + fileHandle.write(file.binaryData) fileHandle.close() - ismFile = IsmImport(file["binary_data"]) - + ismFile = IsmImport(file.binaryData) + impedanceFrequencies = ismFile.getFrequencyArray() - + impedanceAbsolute = ismFile.getImpedanceArray() impedancePhase = ismFile.getPhaseArray() - + figBode, (impedanceAxis, phaseAxis) = plt.subplots(2, 1, sharex=True) figBode.suptitle("Bode") - - impedanceAxis.loglog(impedanceFrequencies, impedanceAbsolute, marker="+", markersize=5) + + impedanceAxis.loglog( + impedanceFrequencies, impedanceAbsolute, marker="+", markersize=5 + ) impedanceAxis.xaxis.set_major_formatter(EngFormatter(unit="Hz")) impedanceAxis.yaxis.set_major_formatter(EngFormatter(unit="$\Omega$")) impedanceAxis.set_xlabel(r"$f$") impedanceAxis.set_ylabel(r"$|Z|$") impedanceAxis.grid(which="both") - - phaseAxis.semilogx(impedanceFrequencies, np.abs(impedancePhase * (360 / (2 * np.pi))), marker="+", markersize=5) + + phaseAxis.semilogx( + impedanceFrequencies, + np.abs(impedancePhase * (360 / (2 * np.pi))), + marker="+", + markersize=5, + ) phaseAxis.xaxis.set_major_formatter(EngFormatter(unit="Hz")) phaseAxis.yaxis.set_major_formatter(EngFormatter(unit="$°$", sep="")) phaseAxis.set_xlabel(r"$f$") @@ -87,7 +88,7 @@ plt.show() localDirectory = r"D:\myLocalDirectory" - + # Delete the entire contents of the directory. for file in os.listdir(localDirectory): fileWithPath = os.path.join(localDirectory, file) @@ -99,34 +100,35 @@ except: pass - fileInterface.enableSaveReceivedFilesToDisk(path = localDirectory) + fileInterface.enableSaveReceivedFilesToDisk(path=localDirectory) fileInterface.enableKeepReceivedFilesInObject() fileInterface.enableAutomaticFileExchange() zahnerZennium.setEISNaming("counter") zahnerZennium.setEISCounter(13) zahnerZennium.setEISOutputFileName("spectra") - - zahnerZennium.setupPAD4(1,1,1) - zahnerZennium.setupPAD4(1,2,1) + + zahnerZennium.setupPAD4(1, 1, 1) + zahnerZennium.setupPAD4(1, 2, 1) zahnerZennium.enablePAD4() zahnerZennium.measureEIS() fileInterface.disableAutomaticFileExchange() zahnerZennium.measureEIS() - + fileInterface.enableAutomaticFileExchange() zahnerZennium.measureEIS() zahnerZennium.setAmplitude(0) for file in fileInterface.getReceivedFiles(): - ismFile = IsmImport(file["binary_data"]) - print(f"{file['name']} measurement finished at {ismFile.getMeasurementEndDateTime()}") + ismFile = IsmImport(file.binaryData) + print( + f"{file.name} measurement finished at {ismFile.getMeasurementEndDateTime()}" + ) for file in os.listdir(localDirectory): print(file) zenniumConnection.disconnectFromTerm() fileInterface.close() - diff --git a/Examples/HeartBeat/HeartBeat.ipynb b/Examples/HeartBeat/HeartBeat.ipynb index 5bc57af..a3e97c6 100644 --- a/Examples/HeartBeat/HeartBeat.ipynb +++ b/Examples/HeartBeat/HeartBeat.ipynb @@ -69,27 +69,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "if __name__ == \"__main__\":\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", - " \n", + " zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", + " \n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()\n", " \n", @@ -387,11 +374,9 @@ } ], "metadata": { - "interpreter": { - "hash": "ac59ebe37160ed0dfa835113d9b8498d9f09ceb179beaac4002f036b9467c963" - }, "kernelspec": { - "display_name": "Python 3.9.7 64-bit", + "display_name": "Python 3", + "language": "python", "name": "python3" }, "language_info": { @@ -404,9 +389,14 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.1 (tags/v3.11.1:a7a450f, Dec 6 2022, 19:58:39) [MSC v.1934 64 bit (AMD64)]" }, - "orig_nbformat": 4 + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "5238573367df39f7286bb46f9ff5f08f63a01a80960060ce41e3c79b190280fa" + } + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/Examples/HeartBeat/HeartBeat.py b/Examples/HeartBeat/HeartBeat.py index 2448ec8..ea4d821 100644 --- a/Examples/HeartBeat/HeartBeat.py +++ b/Examples/HeartBeat/HeartBeat.py @@ -1,6 +1,6 @@ import sys from thales_remote.connection import ThalesRemoteConnection -from thales_remote.script_wrapper import PotentiostatMode,ThalesRemoteScriptWrapper +from thales_remote.script_wrapper import PotentiostatMode, ThalesRemoteScriptWrapper import time import threading @@ -13,7 +13,7 @@ def watchThreadFunction(): global zenniumConnection global zahnerZennium global keepThreadRunning - + while keepThreadRunning: time.sleep(1) active = zahnerZennium.getTermIsActive() @@ -21,18 +21,14 @@ def watchThreadFunction(): if active: print("beat count: " + str(zahnerZennium.getWorkstationHeartBeat())) + if __name__ == "__main__": zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm("localhost", "ScriptRemote") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() - + zenniumConnection.connectToTerm("localhost", "ScriptRemote") + zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() - + zahnerZennium.calibrateOffsets() testThread = threading.Thread(target=watchThreadFunction) @@ -53,15 +49,15 @@ def watchThreadFunction(): zahnerZennium.setScanStrategy("single") zahnerZennium.enablePotentiostat() - + zahnerZennium.setFrequency(1) zahnerZennium.setAmplitude(10e-3) zahnerZennium.setNumberOfPeriods(3) - + print("measurement start") zahnerZennium.measureEIS() print("measurement end") - + zahnerZennium.disablePotentiostat() print("thread kill") @@ -71,4 +67,3 @@ def watchThreadFunction(): zenniumConnection.disconnectFromTerm() print("finish") - diff --git a/Examples/HeartBeatLiveData/HeartBeatLiveData.ipynb b/Examples/HeartBeatLiveData/HeartBeatLiveData.ipynb new file mode 100644 index 0000000..2653b0d --- /dev/null +++ b/Examples/HeartBeatLiveData/HeartBeatLiveData.ipynb @@ -0,0 +1,357 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Workstation Heartbeat and Live Data\n", + "\n", + "This example shows how in a separate thread from the term the HeartBeat can be queried with Python. The heartbeat is queried in a separate thread once a second. The HeartBeat represents how many milliseconds it has been since the term has received something from the Thales.\n", + "\n", + "This example also receives the live data. To receive the online display data, the Zahner online display must be switched off.\n", + "To do this, the following parameter must be adapted in the file C:/FLINK/usb.ini EnableODisplay=off." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "from thales_remote.connection import ThalesRemoteConnection\n", + "from thales_remote.script_wrapper import PotentiostatMode,ThalesRemoteScriptWrapper\n", + "import time\n", + "import threading" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is not a clean solution that the connections are global variables.\n", + "This was solved for a simpler example with global variables." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "zenniumConnection = None\n", + "zahnerZennium = None\n", + "zenniumConnectionLiveData = None\n", + "\n", + "keepThreadRunning = True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Watch Thread\n", + "The following function is used as a thread, in which the HearBeat is queried once a second. The HeartBeat time varies, for example, if EIS is measured at low frequencies, then this time is increased.\n", + "\n", + "The HeartBeat is queried once per second. A timeout of 2 seconds is used to query the HeartBeat. This ensures that the Term responds within 2 seconds, otherwise it can be assumed that the Term software has crashed." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def watchThreadFunction():\n", + " global keepThreadRunning\n", + " global zenniumConnection\n", + " global zahnerZennium\n", + " \n", + " print(\"watch thread started\")\n", + " while keepThreadRunning:\n", + " time.sleep(1)\n", + " try:\n", + " beat = zahnerZennium.getWorkstationHeartBeat(2)\n", + " except:\n", + " print(\"term error watch thread\")\n", + " keepThreadRunning = False\n", + " else:\n", + " print(\"Heartbeat: \" + str(beat) + \" ms\")\n", + " \n", + " print(\"watch thread left\")\n", + " return" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Live Data Thread\n", + "The following function is used as a thread which receives the live data instead of the online display.\n", + "\n", + "Only relevant packet types are output to the console. The relevant types are written as comments in the source code." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "def liveDataThreadFunction():\n", + " global keepThreadRunning\n", + " global zenniumConnectionLiveData\n", + " \n", + " print(\"live thread started\")\n", + " while keepThreadRunning:\n", + " try:\n", + " data = zenniumConnectionLiveData.waitForBinaryTelegram()\n", + " packetId = data[0]\n", + " data = data[1:]\n", + " '''\n", + " Type:\n", + " 1 = Init measurement begin\n", + " 2 = Measurement end\n", + " 4 = Measurement data names\n", + " 5 = Measurement data units\n", + " 6 = ASCII data\n", + " '''\n", + " if packetId in [1,2,4,5,6]:\n", + " print(data.decode(\"ASCII\"))\n", + " except:\n", + " '''\n", + " The connection to the term has an error or the socket has been closed.\n", + " '''\n", + " print(\"term error live thread\")\n", + " keepThreadRunning = False\n", + " \n", + " print(\"live thread left\")\n", + " return" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Main Program Sequence\n", + "\n", + "In the main program flow, the first thing that happens is that an additional connection to the term is established with the name \"Logging\". The live data comes via this connection.\n", + "\n", + "Then the thread is started, which receives the data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if __name__ == \"__main__\":\n", + " zenniumConnectionLiveData = ThalesRemoteConnection()\n", + " zenniumConnectionLiveData.connectToTerm(\"localhost\", \"Logging\")\n", + " \n", + " \n", + " liveThread = threading.Thread(target=liveDataThreadFunction)\n", + " liveThread.start()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the connection with the live data, the nominal connection is established, which sends the commands for measurement." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " zenniumConnection = ThalesRemoteConnection()\n", + " zenniumConnection.connectToTerm(\"localhost\", \"ScriptRemote\")\n", + " \n", + " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", + " zahnerZennium.forceThalesIntoRemoteScript()\n", + " \n", + " zahnerZennium.calibrateOffsets()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The watch thread uses the command interface to the Thales, so it is started after initializing this connection." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "watch thread started\n", + "measurement start\n", + "Heartbeat: 359 ms\n", + "3,Impedance Spectroscopy\n", + "frequency,impedance,phase,time,significance,voltage,current,\n", + "Hz,Ohm,rad,s, ,V,A,\n", + "Heartbeat: 250 ms\n", + "Heartbeat: 766 ms\n", + "Heartbeat: 1781 ms\n", + " 1.00940e+03, 7.65470e+02,-1.55905e-02, 0.00000e+00, 9.95000e-01, 2.52615e-02,-2.01349e-06,\n", + "Heartbeat: 0 ms\n", + "Heartbeat: 954 ms\n", + " 1.11450e+03, 7.56186e+02,-1.81091e-02, 2.13700e+00, 9.94000e-01, 2.52615e-02,-2.01331e-06,\n", + "Heartbeat: 594 ms\n", + " 1.23050e+03, 7.69415e+02,-1.18808e-02, 3.49100e+00, 9.97000e-01, 2.52615e-02,-2.01360e-06,\n", + "Heartbeat: 172 ms\n", + "Heartbeat: 1172 ms\n", + " 1.35860e+03, 7.77636e+02,-8.49820e-03, 4.90950e+00, 9.96000e-01, 2.52615e-02,-2.01254e-06,\n", + "Heartbeat: 829 ms\n", + " 1.50000e+03, 7.61770e+02,-2.04173e-02, 6.25850e+00, 9.96000e-01, 2.52615e-02,-2.01279e-06,\n", + "Heartbeat: 484 ms\n", + " 1.35860e+03, 7.59320e+02,-1.62248e-02, 7.60700e+00, 9.96000e-01, 2.52615e-02,-2.01160e-06,\n", + "Heartbeat: 140 ms\n", + "Heartbeat: 1140 ms\n", + " 1.23050e+03, 7.63513e+02,-1.44088e-02, 8.95800e+00, 9.95000e-01, 2.52615e-02,-2.01110e-06,\n", + "Heartbeat: 797 ms\n", + " 1.11450e+03, 7.68399e+02,-2.44866e-02, 1.03110e+01, 9.97000e-01, 2.52615e-02,-2.01219e-06,\n", + "Heartbeat: 453 ms\n", + " 1.00940e+03, 7.61323e+02,-3.11624e-02, 1.16665e+01, 9.94000e-01, 2.52615e-02,-2.01113e-06,\n", + "Heartbeat: 94 ms\n", + "Heartbeat: 1094 ms\n", + " 9.14260e+02, 7.53671e+02,-2.72892e-02, 1.30170e+01, 9.92000e-01, 2.52615e-02,-2.01273e-06,\n", + "Heartbeat: 750 ms\n", + " 8.28070e+02, 7.59486e+02,-1.29333e-02, 1.43625e+01, 9.91000e-01, 2.52615e-02,-2.01286e-06,\n", + "Heartbeat: 421 ms\n", + " 7.50000e+02, 7.49419e+02, 7.14310e-03, 1.57110e+01, 9.89000e-01, 2.52615e-02,-2.01536e-06,\n", + "Impedance Spectroscopy finished!\n", + "Heartbeat: 62 ms\n", + "Heartbeat: 16 ms\n", + "Heartbeat: 172 ms\n", + "Heartbeat: 47 ms\n", + "Heartbeat: 31 ms\n", + "Heartbeat: 47 ms\n", + "measurement end\n" + ] + }, + { + "data": { + "text/plain": [ + "'OK\\r'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + " watchThread = threading.Thread(target=watchThreadFunction)\n", + " watchThread.start()\n", + " \n", + " zahnerZennium.setPotentiostatMode(PotentiostatMode.POTMODE_POTENTIOSTATIC)\n", + " zahnerZennium.setAmplitude(10e-3)\n", + " zahnerZennium.setPotential(0)\n", + " zahnerZennium.setLowerFrequencyLimit(750)\n", + " zahnerZennium.setStartFrequency(1000)\n", + " zahnerZennium.setUpperFrequencyLimit(1500)\n", + " zahnerZennium.setLowerNumberOfPeriods(2)\n", + " zahnerZennium.setLowerStepsPerDecade(2)\n", + " zahnerZennium.setUpperNumberOfPeriods(2)\n", + " zahnerZennium.setUpperStepsPerDecade(20)\n", + " zahnerZennium.setScanDirection(\"startToMax\")\n", + " zahnerZennium.setScanStrategy(\"single\")\n", + " \n", + " zahnerZennium.enablePotentiostat()\n", + " \n", + " \n", + " zahnerZennium.setFrequency(1)\n", + " zahnerZennium.setAmplitude(10e-3)\n", + " zahnerZennium.setNumberOfPeriods(3)\n", + " \n", + " print(\"measurement start\")\n", + " \n", + " zahnerZennium.measureEIS()\n", + " for i in range(20):\n", + " zahnerZennium.getPotential()\n", + " zahnerZennium.setPotential(0)\n", + "\n", + " print(\"measurement end\")\n", + " \n", + " zahnerZennium.disablePotentiostat()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Closing the threads and then waiting until they are closed." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Heartbeat: 0 ms\n", + "set thread kill flag\n", + "disconnect connections\n", + "term error live thread\n", + "live thread left\n", + "term error watch thread\n", + "watch thread left\n", + "join the threads\n", + "finish\n" + ] + } + ], + "source": [ + " print(\"set thread kill flag\")\n", + " keepThreadRunning = False\n", + " \n", + " print(\"disconnect connections\")\n", + " zenniumConnection.disconnectFromTerm()\n", + " zenniumConnectionLiveData.disconnectFromTerm()\n", + " \n", + " print(\"join the threads\")\n", + " liveThread.join()\n", + " watchThread.join()\n", + " \n", + " print(\"finish\")" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "ac59ebe37160ed0dfa835113d9b8498d9f09ceb179beaac4002f036b9467c963" + }, + "kernelspec": { + "display_name": "Python 3.9.7 64-bit", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.7" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Examples/HeartBeatLiveData/HeartBeatLiveData.py b/Examples/HeartBeatLiveData/HeartBeatLiveData.py new file mode 100644 index 0000000..348d2d2 --- /dev/null +++ b/Examples/HeartBeatLiveData/HeartBeatLiveData.py @@ -0,0 +1,124 @@ +import sys +from thales_remote.connection import ThalesRemoteConnection +from thales_remote.script_wrapper import PotentiostatMode, ThalesRemoteScriptWrapper +import time +import threading + +zenniumConnection = None +zahnerZennium = None +zenniumConnectionLiveData = None + +keepThreadRunning = True + + +def watchThreadFunction(): + global keepThreadRunning + global zenniumConnection + global zahnerZennium + + print("watch thread started") + while keepThreadRunning: + time.sleep(1) + try: + beat = zahnerZennium.getWorkstationHeartBeat(2) + except: + print("term error watch thread") + keepThreadRunning = False + else: + print("Heartbeat: " + str(beat) + " ms") + + print("watch thread left") + return + + +def liveDataThreadFunction(): + global keepThreadRunning + global zenniumConnectionLiveData + + print("live thread started") + while keepThreadRunning: + try: + data = zenniumConnectionLiveData.waitForBinaryTelegram() + packetId = data[0] + data = data[1:] + """ + Type: + 1 = Init measurement begin + 2 = Measurement end + 4 = Measurement data names + 5 = Measurement data units + 6 = ASCII data + """ + if packetId in [1, 2, 4, 5, 6]: + print(data.decode("ASCII")) + except: + """ + The connection to the term has an error or the socket has been closed. + """ + print("term error live thread") + keepThreadRunning = False + + print("live thread left") + return + + +if __name__ == "__main__": + zenniumConnectionLiveData = ThalesRemoteConnection() + zenniumConnectionLiveData.connectToTerm("localhost", "Logging") + + liveThread = threading.Thread(target=liveDataThreadFunction) + liveThread.start() + + zenniumConnection = ThalesRemoteConnection() + zenniumConnection.connectToTerm("localhost", "ScriptRemote") + + zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) + zahnerZennium.forceThalesIntoRemoteScript() + + zahnerZennium.calibrateOffsets() + + watchThread = threading.Thread(target=watchThreadFunction) + watchThread.start() + + zahnerZennium.setPotentiostatMode(PotentiostatMode.POTMODE_POTENTIOSTATIC) + zahnerZennium.setAmplitude(10e-3) + zahnerZennium.setPotential(0) + zahnerZennium.setLowerFrequencyLimit(750) + zahnerZennium.setStartFrequency(1000) + zahnerZennium.setUpperFrequencyLimit(1500) + zahnerZennium.setLowerNumberOfPeriods(2) + zahnerZennium.setLowerStepsPerDecade(2) + zahnerZennium.setUpperNumberOfPeriods(2) + zahnerZennium.setUpperStepsPerDecade(20) + zahnerZennium.setScanDirection("startToMax") + zahnerZennium.setScanStrategy("single") + + zahnerZennium.enablePotentiostat() + + zahnerZennium.setFrequency(1) + zahnerZennium.setAmplitude(10e-3) + zahnerZennium.setNumberOfPeriods(3) + + print("measurement start") + + zahnerZennium.measureEIS() + for i in range(20): + zahnerZennium.getPotential() + zahnerZennium.setPotential(0) + + print("measurement end") + + zahnerZennium.disablePotentiostat() + + print("set thread kill flag") + keepThreadRunning = False + + print("disconnect connections") + zenniumConnection.disconnectFromTerm() + zenniumConnectionLiveData.disconnectFromTerm() + + print("join the threads") + liveThread.join() + watchThread.join() + + print("finish") diff --git a/Examples/ImpedanceMultiCellCycle/ImpedanceMultiCellCycle.ipynb b/Examples/ImpedanceMultiCellCycle/ImpedanceMultiCellCycle.ipynb new file mode 100644 index 0000000..311a9ff --- /dev/null +++ b/Examples/ImpedanceMultiCellCycle/ImpedanceMultiCellCycle.ipynb @@ -0,0 +1,486 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "9fd5451a-e27b-47f5-bc5f-f38407b0df0e", + "metadata": {}, + "source": [ + "# Multichannel operation with EIS\n", + "\n", + "This notebook is an example in which [external potentiostats](https://zahner.de/products#external-potentiostats) such as PP2x2, XPOT2 or EL1002 are controlled both standalone (SCPI) and as an EPC device on a [Zennium series instrument](https://zahner.de/products#potentiostats). Up to 16 external potentiostats can share one Zennium to use it for impedance measurements for example.\n", + "\n", + "**This notebook cannot be executed and has been created only for documentation and explanation of the source code, because Jupyter does not support loops over multiple cells.**\n", + "\n", + "Knowledge of all other notebooks of this repository, the [Remote2 manual](https://doc.zahner.de/manuals/remote2.pdf) and the [zahner_potentiostat package](https://github.com/Zahner-elektrik/Zahner-Remote-Python) is assumed as known.\n", + "\n", + "For this example a Zennium with [EPC card](https://zahner.de/products-details/addon-cards/epc42) is necessary, to this EPC card the external potentiostats must be connected with the appropriate cable. A maximum of 4 potentiostats per card and a maximum of 4 cards are possible. \n", + "The external potentiostats and the Zennium must also be connected to the computer via USB cable separately.\n", + "\n", + "In this example, a PP242 and an XPOT2 are used to cyclically charge and discharge capacitors, and after a defined number of cycles, impedance is measured with the Zennium via the EPC interface.\n", + "\n", + "The [ImpedanceRampHotSwap.ipynb](https://github.com/Zahner-elektrik/Thales-Remote-Python/tree/main/Examples/ImpedanceRampHotSwap/ImpedanceRampHotSwap.ipynb) example is a supplement to this example, there the potentiostat is not switched off when switching between EPC and SCPI.\n", + "\n", + "**Important Notes:**\n", + "\n", + "Each external potentiostat needs its own thread or process for the measurement. This requires a basic understanding of multithreading and thread synchronization. This example is solved with threads. Because of the [threading](https://docs.python.org/3/library/threading.html#module-threading) concept with the [Python Global Interpreter Lock (GIL)](https://docs.python.org/3/glossary.html#term-global-interpreter-lock) it can make sense or be necessary to use [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) instead of [threading](https://docs.python.org/3/library/threading.html#module-threading). If multiprocessing is used, then the used lock with which the shared Zennium is synchronized has to be changed to the [multiprocessing lock](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Lock) instead of the [threading lock](https://docs.python.org/3/library/threading.html#lock-objects).\n", + "\n", + "**Only one potentiostat, i.e. thread/process, can access the Zennium at a time.**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "267c073d-96d2-4b9f-8fdc-76f5013c6090", + "metadata": {}, + "outputs": [], + "source": [ + "from thales_remote.epc_scpi_handler import EpcScpiHandlerFactory,EpcScpiHandler\n", + "from thales_remote.script_wrapper import PotentiostatMode\n", + "from zahner_potentiostat.scpi_control.datahandler import DataManager\n", + "from zahner_potentiostat.display.onlinedisplay import OnlineDisplay\n", + "from zahner_potentiostat.scpi_control.datareceiver import TrackTypes\n", + "\n", + "import threading\n", + "from datetime import datetime" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "71e4ec65-791e-4e6d-9773-484759ecb257", + "metadata": {}, + "source": [ + "# Name creation function\n", + "\n", + "This function simplifies the naming of the cells/channels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a46c10df-c39b-4095-9e70-0011f524dd96", + "metadata": {}, + "outputs": [], + "source": [ + "def getFileName(channel,cycle):\n", + " time = str(datetime.now().time())\n", + " time = time.replace(\":\",\"\")\n", + " time = time.replace(\",\",\"\")\n", + " time = time.replace(\".\",\"\")\n", + " return f\"channel{channel}_cycle{cycle}\" # + time" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "99d01698-083e-4174-8ce0-829ff601acff", + "metadata": {}, + "source": [ + "# Thread for channel 1\n", + "\n", + "The following function is executed as a thread for channel one.\n", + "The same function could also be executed as a thread for each channel, but here different sequences are to be measured with each external potentiostat, therefore two different functions are used.\n", + "\n", + "Only the first thread is explained in more detail, the second thread is only slightly different from the first thread.\n", + "\n", + "The threads unfortunately have to be defined before the main function, where the initialization of the different devices takes place and [EpcScpiHandler](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandler) objects are created for each external potentiostat.\n", + "\n", + "An [EpcScpiHandler](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandler) object is then passed to the respective thread as a parameter, so that the thread can work with this object and the measurement process can be programmed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7fce540-9544-4eb1-9cfc-9180c0cf04d7", + "metadata": {}, + "outputs": [], + "source": [ + "def channel1Thread(deviceHandler, channel = 0):\n", + " deviceHandler.scpiInterface.setMaximumTimeParameter(15)\n", + " deviceHandler.scpiInterface.setParameterLimitCheckToleranceTime(0.1)\n", + " \n", + " configuration = {\n", + " \"figureTitle\":\"Online Display Channel 1\",\n", + " \"xAxisLabel\":\"Time\",\n", + " \"xAxisUnit\":\"s\",\n", + " \"xTrackName\":TrackTypes.TIME.toString(),\n", + " \"yAxis\":\n", + " [{\"label\": \"Voltage\", \"unit\": \"V\", \"trackName\":TrackTypes.VOLTAGE.toString()},\n", + " {\"label\": \"Current\", \"unit\": \"A\", \"trackName\":TrackTypes.CURRENT.toString()}]\n", + " }\n", + " \n", + " for i in range(3):\n", + " filename = getFileName(channel = channel, cycle = i) " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "8dd9502c-2dad-4bcd-8199-6a786bc50613", + "metadata": {}, + "source": [ + "First the device is in SCPI standalone mode and a capacitor is charged and discharged 2 times.\n", + "\n", + "During the measurement, the online display is also started for visualization of voltage and current." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d56cfb9e-1f56-491f-9aec-2c1d60d83dd5", + "metadata": {}, + "outputs": [], + "source": [ + " onlineDisplay = OnlineDisplay(deviceHandler.scpiInterface.getDataReceiver(), displayConfiguration=configuration)\n", + " \n", + " deviceHandler.scpiInterface.measureOCVScan()\n", + " \n", + " for n in range(2):\n", + " deviceHandler.scpiInterface.measureCharge(current = 1e-3,\n", + " stopVoltage = 2,\n", + " maximumTime = \"5 min\")\n", + " \n", + " deviceHandler.scpiInterface.measureDischarge(current = -1e-3,\n", + " stopVoltage = 0.5,\n", + " maximumTime = \"5 min\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "ed5eb36a-0cec-4bb9-97c5-03293d9230be", + "metadata": {}, + "source": [ + "After cycling, the EPC mode must be activated in order to control the potentiostat as an EPC device with the Zennium. For this purpose the lock must be aquired. \n", + "In order to be able to continue measuring the OCP, the lock is acquired with *blocking = False*, so if the Zennium is occupied and [acquireSharedZennium()](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandler.acquireSharedZennium) returns *False*, the OCP is measured again for 15 seconds. \n", + "This is repeated until the zennium is no longer used by another channel and is available." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1699a6cb-6bcf-484d-8927-3199157c791f", + "metadata": {}, + "outputs": [], + "source": [ + " deviceHandler.scpiInterface.setMaximumTimeParameter(15) \n", + " while deviceHandler.acquireSharedZennium(blocking = False) == False:\n", + " deviceHandler.scpiInterface.measureOCVScan()" + ] + }, + { + "cell_type": "markdown", + "id": "c355fae7-9a24-494a-ae85-2646f3dc603b", + "metadata": {}, + "source": [ + "Now that the Zennium is reserved for this measurement, the measurement data is saved as text and the online display is closed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e88e691-bb68-452b-a7a9-bc016afed25d", + "metadata": {}, + "outputs": [], + "source": [ + " dataManager = DataManager(deviceHandler.scpiInterface.getDataReceiver())\n", + " dataManager.saveDataAsText(filename + \".txt\")\n", + " \n", + " onlineDisplay.close()\n", + " del onlineDisplay" + ] + }, + { + "cell_type": "markdown", + "id": "d46d3b76-fd12-4f05-9ef6-3b67e20ee5f5", + "metadata": {}, + "source": [ + "To use the external potentiostat as an EPC device, the device must be switched to EPC mode as shown below.\n", + "\n", + "The change between SCPI and EPC interface must be initiated by the currently active controller, it is not possible to \"get back\" the control via SCPI in EPC mode." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0aed53b-cbd3-4fc7-ac17-17b609a96445", + "metadata": {}, + "outputs": [], + "source": [ + " deviceHandler.switchToEPC()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "cd286f44-c439-4f99-9b19-8bd2ff0f8009", + "metadata": {}, + "source": [ + "From now on the Zennium is controlled by Remote2 and the [Thales-Remote-Python library](https://github.com/Zahner-elektrik/Thales-Remote-Python), the [switchToEPC()](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandler.switchToEPC) function automatically selects the EPC channel to which the device is connected.\n", + "\n", + "The object *deviceHandler.scpiInterface* loses its validity after calling the function [switchToEPC()](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandler.switchToEPC) and **can not be used anymore**.\n", + "\n", + "In the [other example](https://github.com/Zahner-elektrik/Thales-Remote-Python/tree/main/Examples/ImpedanceRampHotSwap/ImpedanceRampHotSwap.ipynb) the parameter *keepPotentiostatState = True* is used to keep the potentiostat switched on when switching the operation mode." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b695e763-d86b-456c-abd0-6a6852b8f5a1", + "metadata": {}, + "outputs": [], + "source": [ + " deviceHandler.sharedZenniumInterface.setEISNaming(\"individual\")\n", + " deviceHandler.sharedZenniumInterface.setEISOutputPath(r\"C:\\THALES\\temp\\multichannel\")\n", + " deviceHandler.sharedZenniumInterface.setEISOutputFileName(filename)\n", + " \n", + " deviceHandler.sharedZenniumInterface.setPotentiostatMode(PotentiostatMode.POTMODE_POTENTIOSTATIC)\n", + " deviceHandler.sharedZenniumInterface.setAmplitude(10e-3)\n", + " deviceHandler.sharedZenniumInterface.setPotential(0)\n", + " deviceHandler.sharedZenniumInterface.setLowerFrequencyLimit(100)\n", + " deviceHandler.sharedZenniumInterface.setStartFrequency(500)\n", + " deviceHandler.sharedZenniumInterface.setUpperFrequencyLimit(1000)\n", + " deviceHandler.sharedZenniumInterface.setLowerNumberOfPeriods(5)\n", + " deviceHandler.sharedZenniumInterface.setLowerStepsPerDecade(2)\n", + " deviceHandler.sharedZenniumInterface.setUpperNumberOfPeriods(20)\n", + " deviceHandler.sharedZenniumInterface.setUpperStepsPerDecade(5)\n", + " deviceHandler.sharedZenniumInterface.setScanDirection(\"startToMax\")\n", + " deviceHandler.sharedZenniumInterface.setScanStrategy(\"single\")\n", + " \n", + " deviceHandler.sharedZenniumInterface.measureEIS()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "13fc14e4-e46e-4ec8-b8b3-a28dfc43b222", + "metadata": {}, + "source": [ + "After the measurement, the Zennium must be released that it can be used by other channels. This is realized with the method [switchToSCPIAndReleaseSharedZennium()](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandler.switchToSCPIAndReleaseSharedZennium). Afterwards the device is available for standalone SCPI measurements without Thales.\n", + "\n", + "The object *deviceHandler.sharedZenniumInterface* loses its validity after calling the function [switchToSCPIAndReleaseSharedZennium()](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandler.switchToSCPIAndReleaseSharedZennium) and **can not be used anymore**.\n", + "\n", + "In the [other example]((https://github.com/Zahner-elektrik/Thales-Remote-Python/tree/main/Examples/ImpedanceRampHotSwap/ImpedanceRampHotSwap.ipynb)) the parameter *keepPotentiostatState = True* is used to keep the potentiostat switched on when switching the operation mode." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d47c19bf-e5b4-435c-a837-d20502a606dc", + "metadata": {}, + "outputs": [], + "source": [ + " deviceHandler.switchToSCPIAndReleaseSharedZennium()\n", + " \n", + " return" + ] + }, + { + "cell_type": "markdown", + "id": "de430254-71e2-49ec-89f2-f75617c27c85", + "metadata": {}, + "source": [ + "# Thread for channel 2\n", + "\n", + "The following function is executed as a thread for channel 2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "577177a3-600c-4891-b66a-b58e2dfcc6bc", + "metadata": {}, + "outputs": [], + "source": [ + "def channel2Thread(deviceHandler):\n", + " deviceHandler.scpiInterface.setMaximumTimeParameter(15)\n", + " deviceHandler.scpiInterface.setParameterLimitCheckToleranceTime(0.1)\n", + " \n", + " configuration = {\n", + " \"figureTitle\":\"Online Display Channel 2\",\n", + " \"xAxisLabel\":\"Time\",\n", + " \"xAxisUnit\":\"s\",\n", + " \"xTrackName\":TrackTypes.TIME.toString(),\n", + " \"yAxis\":\n", + " [{\"label\": \"Voltage\", \"unit\": \"V\", \"trackName\":TrackTypes.VOLTAGE.toString()},\n", + " {\"label\": \"Current\", \"unit\": \"A\", \"trackName\":TrackTypes.CURRENT.toString()}]\n", + " }\n", + " \n", + " for i in range(2):\n", + " filename = getFileName(channel = 2, cycle = i)\n", + " \n", + " onlineDisplay = OnlineDisplay(deviceHandler.scpiInterface.getDataReceiver(), displayConfiguration=configuration)\n", + " \n", + " deviceHandler.scpiInterface.measureOCVScan()\n", + " \n", + " for n in range(2):\n", + " deviceHandler.scpiInterface.measureCharge(current = 4,\n", + " stopVoltage = 1,\n", + " maximumTime = \"5 min\")\n", + " \n", + " deviceHandler.scpiInterface.measureDischarge(current = -4,\n", + " stopVoltage = 0.6,\n", + " maximumTime = \"5 min\")\n", + " \n", + " dataManager = DataManager(deviceHandler.scpiInterface.getDataReceiver())\n", + " dataManager.saveDataAsText(filename + \".txt\")\n", + " \n", + " onlineDisplay.close()\n", + " del onlineDisplay" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "72bf967f-4520-48bd-a046-e055bd0386d4", + "metadata": {}, + "source": [ + "In this example we simply wait without measurements until the Zennium is available, therefore no parameters are necessary for [acquireSharedZennium()](https://doc.zahner.de/thales_remote/epc_scpi_handler.html?highlight=acquiresharedzennium#thales_remote.epc_scpi_handler.EpcScpiHandler.acquireSharedZennium). \n", + "The potentiostat should have been switched off before." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "744c4120-6882-4f7a-868d-0dcd4a32206e", + "metadata": {}, + "outputs": [], + "source": [ + " deviceHandler.acquireSharedZennium() \n", + " deviceHandler.switchToEPC()\n", + "\n", + " deviceHandler.sharedZenniumInterface.setEISNaming(\"individual\")\n", + " deviceHandler.sharedZenniumInterface.setEISOutputPath(r\"C:\\THALES\\temp\\multichannel\")\n", + " deviceHandler.sharedZenniumInterface.setEISOutputFileName(filename)\n", + " \n", + " deviceHandler.sharedZenniumInterface.setPotentiostatMode(PotentiostatMode.POTMODE_POTENTIOSTATIC)\n", + " deviceHandler.sharedZenniumInterface.setAmplitude(10e-3)\n", + " deviceHandler.sharedZenniumInterface.setPotential(0)\n", + " deviceHandler.sharedZenniumInterface.setLowerFrequencyLimit(100)\n", + " deviceHandler.sharedZenniumInterface.setStartFrequency(500)\n", + " deviceHandler.sharedZenniumInterface.setUpperFrequencyLimit(1000)\n", + " deviceHandler.sharedZenniumInterface.setLowerNumberOfPeriods(5)\n", + " deviceHandler.sharedZenniumInterface.setLowerStepsPerDecade(2)\n", + " deviceHandler.sharedZenniumInterface.setUpperNumberOfPeriods(20)\n", + " deviceHandler.sharedZenniumInterface.setUpperStepsPerDecade(5)\n", + " deviceHandler.sharedZenniumInterface.setScanDirection(\"startToMax\")\n", + " deviceHandler.sharedZenniumInterface.setScanStrategy(\"single\")\n", + " \n", + " deviceHandler.sharedZenniumInterface.measureEIS()\n", + " \n", + " deviceHandler.switchToSCPIAndReleaseSharedZennium()\n", + " \n", + " return" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "71163bbf-b562-4320-8d28-386e47416654", + "metadata": {}, + "source": [ + "# Initialization of the devices\n", + "\n", + "Before the two threads with the measurement tasks are executed, the device management objects of type [EpcScpiHandler](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandler) are created with the [EpcScpiHandlerFactory](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandlerFactory) class.\n", + "\n", + "The Zennium could also be connected to another computer via USB and you can control the Zennium over network, for this the default parameter *shared_zennium_target = \"localhost\"* of the constructor of [EpcScpiHandlerFactory](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandlerFactory) must be overwritten with the corresponding IP address.\n", + "The external potentiostats can currently only be controlled via USB." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2a1e8fd-2ac4-450a-91c3-90caf0e4cdfe", + "metadata": {}, + "outputs": [], + "source": [ + "if __name__ == \"__main__\": \n", + " handlerFactory = EpcScpiHandlerFactory()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "75ad88c3-55a5-4711-87f5-eddf7f57dffa", + "metadata": {}, + "source": [ + "With the method [createEpcScpiHandler()](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandlerFactory.createEpcScpiHandler) of the [EpcScpiHandlerFactory](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandlerFactory) object handlerFactory, a new [EpcScpiHandler](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandler) object can be created, which is used to control the devices as explained above.\n", + "\n", + "The method [createEpcScpiHandler()](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandlerFactory.createEpcScpiHandler) has two parameters: \n", + "* **epcChannel:** The number of the EPC channel to which the external potentiostat is connected.\n", + "* **serialNumber:** The serial number of the device to uniquely identify it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20a0d32c-7e21-4dac-87f9-0d79994fbdea", + "metadata": {}, + "outputs": [], + "source": [ + " XPOT2 = handlerFactory.createEpcScpiHandler(epcChannel=1, serialNumber=27000)\n", + " PP242 = handlerFactory.createEpcScpiHandler(epcChannel=4, serialNumber=35000)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f3e3bd5c-4b82-4068-a099-4a78e0c2bfec", + "metadata": {}, + "source": [ + "After the two devices are initialized, they are passed to the respective [threads](https://docs.python.org/3/library/threading.html#module-threading) and the threads are started. Then the main only waits until the two measurement threads are finished.\n", + "\n", + "For the first thread a channel number is passed, this would be necessary for the naming of the files, if the same function is executed as different threads." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a65d20e-caf1-49a8-bea4-36545d54b4d7", + "metadata": {}, + "outputs": [], + "source": [ + " channel1ThreadHandler = threading.Thread(target=channel1Thread, args=(XPOT2,1))\n", + " channel2ThreadHandler = threading.Thread(target=channel2Thread, args=(PP242,))\n", + " \n", + " channel1ThreadHandler.start()\n", + " channel2ThreadHandler.start()\n", + " \n", + " channel1ThreadHandler.join()\n", + " channel2ThreadHandler.join()\n", + " \n", + " handlerFactory.closeAll()\n", + " print(\"finish\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.11.1 64-bit", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1 (tags/v3.11.1:a7a450f, Dec 6 2022, 19:58:39) [MSC v.1934 64 bit (AMD64)]" + }, + "toc-autonumbering": false, + "toc-showtags": false, + "vscode": { + "interpreter": { + "hash": "5238573367df39f7286bb46f9ff5f08f63a01a80960060ce41e3c79b190280fa" + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Examples/ImpedanceMultiCellCycle/ImpedanceMultiCellCycle.py b/Examples/ImpedanceMultiCellCycle/ImpedanceMultiCellCycle.py new file mode 100644 index 0000000..af8aeac --- /dev/null +++ b/Examples/ImpedanceMultiCellCycle/ImpedanceMultiCellCycle.py @@ -0,0 +1,196 @@ +from thales_remote.epc_scpi_handler import EpcScpiHandlerFactory, EpcScpiHandler +from thales_remote.script_wrapper import PotentiostatMode +from zahner_potentiostat.scpi_control.datahandler import DataManager +from zahner_potentiostat.display.onlinedisplay import OnlineDisplay +from zahner_potentiostat.scpi_control.datareceiver import TrackTypes + +import threading +from datetime import datetime + + +def getFileName(channel, cycle): + time = str(datetime.now().time()) + time = time.replace(":", "") + time = time.replace(",", "") + time = time.replace(".", "") + return f"channel{channel}_cycle{cycle}" # + time + + +def channel1Thread(deviceHandler, channel=0): + deviceHandler.scpiInterface.setMaximumTimeParameter(15) + deviceHandler.scpiInterface.setParameterLimitCheckToleranceTime(0.1) + + configuration = { + "figureTitle": "Online Display Channel 1", + "xAxisLabel": "Time", + "xAxisUnit": "s", + "xTrackName": TrackTypes.TIME.toString(), + "yAxis": [ + { + "label": "Voltage", + "unit": "V", + "trackName": TrackTypes.VOLTAGE.toString(), + }, + { + "label": "Current", + "unit": "A", + "trackName": TrackTypes.CURRENT.toString(), + }, + ], + } + + for i in range(3): + filename = getFileName(channel=channel, cycle=i) + + onlineDisplay = OnlineDisplay( + deviceHandler.scpiInterface.getDataReceiver(), + displayConfiguration=configuration, + ) + + deviceHandler.scpiInterface.measureOCVScan() + + for n in range(2): + deviceHandler.scpiInterface.measureCharge( + current=1e-3, stopVoltage=2, maximumTime="5 min" + ) + + deviceHandler.scpiInterface.measureDischarge( + current=-1e-3, stopVoltage=0.5, maximumTime="5 min" + ) + + deviceHandler.scpiInterface.setMaximumTimeParameter(15) + while deviceHandler.acquireSharedZennium(blocking=False) == False: + deviceHandler.scpiInterface.measureOCVScan() + + dataManager = DataManager(deviceHandler.scpiInterface.getDataReceiver()) + dataManager.saveDataAsText(filename + ".txt") + + onlineDisplay.close() + del onlineDisplay + + deviceHandler.switchToEPC() + + deviceHandler.sharedZenniumInterface.setEISNaming("individual") + deviceHandler.sharedZenniumInterface.setEISOutputPath( + r"C:\THALES\temp\multichannel" + ) + deviceHandler.sharedZenniumInterface.setEISOutputFileName(filename) + + deviceHandler.sharedZenniumInterface.setPotentiostatMode( + PotentiostatMode.POTMODE_POTENTIOSTATIC + ) + deviceHandler.sharedZenniumInterface.setAmplitude(10e-3) + deviceHandler.sharedZenniumInterface.setPotential(0) + deviceHandler.sharedZenniumInterface.setLowerFrequencyLimit(100) + deviceHandler.sharedZenniumInterface.setStartFrequency(500) + deviceHandler.sharedZenniumInterface.setUpperFrequencyLimit(1000) + deviceHandler.sharedZenniumInterface.setLowerNumberOfPeriods(5) + deviceHandler.sharedZenniumInterface.setLowerStepsPerDecade(2) + deviceHandler.sharedZenniumInterface.setUpperNumberOfPeriods(20) + deviceHandler.sharedZenniumInterface.setUpperStepsPerDecade(5) + deviceHandler.sharedZenniumInterface.setScanDirection("startToMax") + deviceHandler.sharedZenniumInterface.setScanStrategy("single") + + deviceHandler.sharedZenniumInterface.measureEIS() + + deviceHandler.switchToSCPIAndReleaseSharedZennium() + + return + + +def channel2Thread(deviceHandler): + deviceHandler.scpiInterface.setMaximumTimeParameter(15) + deviceHandler.scpiInterface.setParameterLimitCheckToleranceTime(0.1) + + configuration = { + "figureTitle": "Online Display Channel 2", + "xAxisLabel": "Time", + "xAxisUnit": "s", + "xTrackName": TrackTypes.TIME.toString(), + "yAxis": [ + { + "label": "Voltage", + "unit": "V", + "trackName": TrackTypes.VOLTAGE.toString(), + }, + { + "label": "Current", + "unit": "A", + "trackName": TrackTypes.CURRENT.toString(), + }, + ], + } + + for i in range(2): + filename = getFileName(channel=2, cycle=i) + + onlineDisplay = OnlineDisplay( + deviceHandler.scpiInterface.getDataReceiver(), + displayConfiguration=configuration, + ) + + deviceHandler.scpiInterface.measureOCVScan() + + for n in range(2): + deviceHandler.scpiInterface.measureCharge( + current=4, stopVoltage=1, maximumTime="5 min" + ) + + deviceHandler.scpiInterface.measureDischarge( + current=-4, stopVoltage=0.6, maximumTime="5 min" + ) + + dataManager = DataManager(deviceHandler.scpiInterface.getDataReceiver()) + dataManager.saveDataAsText(filename + ".txt") + + onlineDisplay.close() + del onlineDisplay + + deviceHandler.acquireSharedZennium() + deviceHandler.switchToEPC() + + deviceHandler.sharedZenniumInterface.setEISNaming("individual") + deviceHandler.sharedZenniumInterface.setEISOutputPath( + r"C:\THALES\temp\multichannel" + ) + deviceHandler.sharedZenniumInterface.setEISOutputFileName(filename) + + deviceHandler.sharedZenniumInterface.setPotentiostatMode( + PotentiostatMode.POTMODE_POTENTIOSTATIC + ) + deviceHandler.sharedZenniumInterface.setAmplitude(10e-3) + deviceHandler.sharedZenniumInterface.setPotential(0) + deviceHandler.sharedZenniumInterface.setLowerFrequencyLimit(100) + deviceHandler.sharedZenniumInterface.setStartFrequency(500) + deviceHandler.sharedZenniumInterface.setUpperFrequencyLimit(1000) + deviceHandler.sharedZenniumInterface.setLowerNumberOfPeriods(5) + deviceHandler.sharedZenniumInterface.setLowerStepsPerDecade(2) + deviceHandler.sharedZenniumInterface.setUpperNumberOfPeriods(20) + deviceHandler.sharedZenniumInterface.setUpperStepsPerDecade(5) + deviceHandler.sharedZenniumInterface.setScanDirection("startToMax") + deviceHandler.sharedZenniumInterface.setScanStrategy("single") + + deviceHandler.sharedZenniumInterface.measureEIS() + + deviceHandler.switchToSCPIAndReleaseSharedZennium() + + return + + +if __name__ == "__main__": + handlerFactory = EpcScpiHandlerFactory() + + XPOT2 = handlerFactory.createEpcScpiHandler(epcChannel=1, serialNumber=27000) + PP242 = handlerFactory.createEpcScpiHandler(epcChannel=4, serialNumber=35000) + + channel1ThreadHandler = threading.Thread(target=channel1Thread, args=(XPOT2, 1)) + channel2ThreadHandler = threading.Thread(target=channel2Thread, args=(PP242,)) + + channel1ThreadHandler.start() + channel2ThreadHandler.start() + + channel1ThreadHandler.join() + channel2ThreadHandler.join() + + handlerFactory.closeAll() + print("finish") diff --git a/Examples/ImpedanceRampHotSwap/ImpedanceRampHotSwap.ipynb b/Examples/ImpedanceRampHotSwap/ImpedanceRampHotSwap.ipynb new file mode 100644 index 0000000..c2a8c16 --- /dev/null +++ b/Examples/ImpedanceRampHotSwap/ImpedanceRampHotSwap.ipynb @@ -0,0 +1,407 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# SCPI ramps and Thales EIS without turning off the potentiostat\n", + "\n", + "All other examples are needed as a precognition for this example. Especially the [Ramps.ipynb](https://github.com/Zahner-elektrik/Zahner-Remote-Python/blob/main/Examples/Ramps/Ramps.ipynb) and [ImpedanceMultiCellCycle.ipynb](https://github.com/Zahner-elektrik/Thales-Remote-Python/blob/main/Examples/ImpedanceMultiCellCycle/ImpedanceMultiCellCycle.ipynb) are important and must be understand before.\n", + "\n", + "In this example, different DC currents are set with ramps. The ramp is followed by a polarization phase, which is followed by an impedance spectrum at this current setting. The DC current settings can be parameterized variably.\n", + "\n", + "**This notebook cannot be executed and has been created only for documentation and explanation of the source code, because Jupyter does not support loops over multiple cells.**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from thales_remote.epc_scpi_handler import EpcScpiHandlerFactory,EpcScpiHandler\n", + "from thales_remote.script_wrapper import PotentiostatMode\n", + "from zahner_potentiostat.scpi_control.datahandler import DataManager\n", + "from zahner_potentiostat.display.onlinedisplay import OnlineDisplay\n", + "from zahner_potentiostat.scpi_control.datareceiver import TrackTypes" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Definition of utiltiy functions\n", + "\n", + "A class is defined, which contains the parameters for the measurement. This simplifies the process and makes it easier to change the parameters.\n", + "\n", + "Two functions are also programmed to show that the same parameters are measured before and after the operating mode change." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class TargetCurrents:\n", + " def __init__(self, dc, amplitude, scanrate):\n", + " self.dc = dc\n", + " self.amplitude = amplitude\n", + " self.scanrate = scanrate\n", + " return\n", + "\n", + "def measure_UI_EPC(deviceHandler):\n", + " for _ in range(3):\n", + " print(f\"EPC-Potential:\\t{deviceHandler.sharedZenniumInterface.getPotential():>10.6f} V\")\n", + " print(f\"EPC-Current:\\t{deviceHandler.sharedZenniumInterface.getCurrent():>10.3e} A\")\n", + " return\n", + "\n", + "def measure_UI_SCPI(deviceHandler):\n", + " for _ in range(3):\n", + " print(f\"SCPI-Potential:\\t{deviceHandler.scpiInterface.getPotential():>10.6f} V\")\n", + " print(f\"SCPI-Current:\\t{deviceHandler.scpiInterface.getCurrent():>10.3e} A\")\n", + " return" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Initialization\n", + "\n", + "In the example [ImpedanceMultiCellCycle.ipynb](https://github.com/Zahner-elektrik/Thales-Remote-Python/blob/main/Examples/ImpedanceMultiCellCycle/ImpedanceMultiCellCycle.ipynb) the initialization is explained step by step.\n", + "\n", + "For switching between EPC and SCPI operation, it is important that both instruments have warmed up for 30 minutes and that the calibration routine has been performed in both operating modes. This ensures that the DC differences are minimized." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if __name__ == \"__main__\": \n", + " startCurrent = 0.0\n", + " handlerFactory = EpcScpiHandlerFactory(\"192.168.2.94\")\n", + " deviceHandler = handlerFactory.createEpcScpiHandler(epcChannel=1, serialNumber=33021)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After all devices have been warmed up for 30 minutes, the first step is to calibrate for SCPI mode after initialization.\n", + "\n", + "Calibration is also performed for the EPC mode." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " deviceHandler.scpiInterface.calibrateOffsets()\n", + " \n", + " deviceHandler.acquireSharedZennium(blocking = True)\n", + " deviceHandler.switchToEPC()\n", + " \n", + " deviceHandler.sharedZenniumInterface.calibrateOffsets()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After everything has been calibrated, the potentiostat can be switched on." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " deviceHandler.sharedZenniumInterface.setPotentiostatMode(PotentiostatMode.POTMODE_GALVANOSTATIC)\n", + " deviceHandler.sharedZenniumInterface.setCurrent(startCurrent)\n", + " deviceHandler.sharedZenniumInterface.enablePotentiostat()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After switching on in EPC mode, current and voltage are measured in EPC mode and output on the console.\n", + "\n", + "Then [switchToSCPIAndReleaseSharedZennium(keepPotentiostatState = True)](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandler.switchToSCPIAndReleaseSharedZennium) switches to SCPI mode without switching off the potentiostat. With this function also the Zennium is released that it could be used by other parallel channels as in the other example.\n", + "\n", + "A current and voltage measurement on the SCPI side follows." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " measure_UI_EPC(deviceHandler)\n", + " deviceHandler.switchToSCPIAndReleaseSharedZennium(keepPotentiostatState = True)\n", + " measure_UI_SCPI(deviceHandler)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# EIS and ramps with different currents\n", + "\n", + "The class definied in the previous is now used to define the steps for the measurement. For each step the target DC current, the EIS amplitude and the scanrate to set the DC current is defined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " measurementSettings = [\n", + " TargetCurrents(dc = 1, amplitude = 0.1, scanrate=0.1),\n", + " TargetCurrents(dc = 2, amplitude = 0.1, scanrate=0.1),\n", + " TargetCurrents(dc = 4, amplitude = 0.2, scanrate=0.5)\n", + " ]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next step is to iterate over the specified steps with a for loop.\n", + "\n", + "In each iteration, exactly the same measurement is performed, but with different parameters from the objects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " for setting in measurementSettings:" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ramp starts at the actual current value therfore the actual current is read and set as value.\n", + "\n", + "An online display for the SCPI measurements is also configured and started." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " deviceHandler.scpiInterface.setCurrentValue(deviceHandler.scpiInterface.getCurrent())\n", + "\n", + " configuration = {\n", + " \"figureTitle\":f\"Online Display Targetcurrent: {setting.dc} Scanrate: {setting.scanrate}\",\n", + " \"xAxisLabel\":\"Time\",\n", + " \"xAxisUnit\":\"s\",\n", + " \"xTrackName\":TrackTypes.TIME.toString(),\n", + " \"yAxis\":\n", + " [{\"label\": \"Voltage\", \"unit\": \"V\", \"trackName\":TrackTypes.VOLTAGE.toString()},\n", + " {\"label\": \"Current\", \"unit\": \"A\", \"trackName\":TrackTypes.CURRENT.toString()}]\n", + " }\n", + " onlineDisplay = OnlineDisplay(deviceHandler.scpiInterface.getDataReceiver(), displayConfiguration=configuration)\n", + " " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The values from the current object of the iterator are now entered as parameters for the ramp. Then the ramp is executed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " deviceHandler.scpiInterface.setCurrentParameter(setting.dc)\n", + " deviceHandler.scpiInterface.setScanRateParameter(setting.scanrate)\n", + " deviceHandler.scpiInterface.measureRampValueInScanRate()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Followed by the ramp, the DC current is held for at least 5 seconds or until the common Zennium is available.\n", + "\n", + "In this example, no other devices use the zennium, so it is always available." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " deviceHandler.scpiInterface.setMaximumTimeParameter(5)\n", + " deviceHandler.scpiInterface.measurePolarization()\n", + " while deviceHandler.acquireSharedZennium(blocking = False) == False:\n", + " deviceHandler.scpiInterface.measurePolarization()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the Zennium is acquired, the measurement data is saved with the target current and scan rate as the filename." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " dataManager = DataManager(deviceHandler.scpiInterface.getDataReceiver())\n", + " dataManager.saveDataAsText(f\"ramp_to{setting.dc}a_{setting.scanrate}apers.txt\")\n", + " \n", + " onlineDisplay.close()\n", + " del onlineDisplay\n", + " del dataManager" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To switch to EPC control with activated potentiostat for EIS measurement, the method [switchToEPC()](https://doc.zahner.de/thales_remote/epc_scpi_handler.html#thales_remote.epc_scpi_handler.EpcScpiHandler.switchToSCPI) with the parameter *keepPotentiostatState = True* is used.\n", + "\n", + "When switching to EPC, it may be the case that the potentiostat is switched off for about 50 ms.\n", + "\n", + "Before and after the operation mode switch current and voltage are displayed on the console." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " measure_UI_SCPI(deviceHandler)\n", + " deviceHandler.switchToEPC(keepPotentiostatState = True)\n", + " measure_UI_EPC(deviceHandler)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Afterwards an EIS measurement is setup and executed.\n", + "\n", + "The measurement is saved with DC current and amplitude in the filename and is stored by Thales on the hard disk." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " deviceHandler.sharedZenniumInterface.setEISNaming(\"individual\")\n", + " deviceHandler.sharedZenniumInterface.setEISOutputPath(r\"C:\\THALES\\temp\")\n", + " deviceHandler.sharedZenniumInterface.setEISOutputFileName(f\"{setting.dc}adc_{setting.amplitude}aac\".replace(\".\",\"\"))\n", + "\n", + " deviceHandler.sharedZenniumInterface.setPotentiostatMode(PotentiostatMode.POTMODE_GALVANOSTATIC)\n", + " deviceHandler.sharedZenniumInterface.setPotential(setting.dc)\n", + " deviceHandler.sharedZenniumInterface.setAmplitude(setting.amplitude)\n", + " \n", + " deviceHandler.sharedZenniumInterface.setLowerFrequencyLimit(100)\n", + " deviceHandler.sharedZenniumInterface.setStartFrequency(250)\n", + " deviceHandler.sharedZenniumInterface.setUpperFrequencyLimit(500)\n", + " deviceHandler.sharedZenniumInterface.setLowerNumberOfPeriods(5)\n", + " deviceHandler.sharedZenniumInterface.setLowerStepsPerDecade(10)\n", + " deviceHandler.sharedZenniumInterface.setUpperNumberOfPeriods(20)\n", + " deviceHandler.sharedZenniumInterface.setUpperStepsPerDecade(10)\n", + " deviceHandler.sharedZenniumInterface.setScanDirection(\"startToMax\")\n", + " deviceHandler.sharedZenniumInterface.setScanStrategy(\"single\")\n", + " \n", + " deviceHandler.sharedZenniumInterface.enablePotentiostat()\n", + " deviceHandler.sharedZenniumInterface.measureEIS()\n", + " deviceHandler.sharedZenniumInterface.setAmplitude(0)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At the end, the system switches back to SCPI mode and the potentiostat is switched off." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " measure_UI_EPC(deviceHandler)\n", + " deviceHandler.switchToSCPIAndReleaseSharedZennium(keepPotentiostatState = True)\n", + " measure_UI_SCPI(deviceHandler)\n", + " \n", + " deviceHandler.scpiInterface.setPotentiostatEnabled(False)\n", + " handlerFactory.closeAll()\n", + " print(\"finish\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.11.1 64-bit", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1 (tags/v3.11.1:a7a450f, Dec 6 2022, 19:58:39) [MSC v.1934 64 bit (AMD64)]" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "5238573367df39f7286bb46f9ff5f08f63a01a80960060ce41e3c79b190280fa" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Examples/ImpedanceRampHotSwap/ImpedanceRampHotSwap.py b/Examples/ImpedanceRampHotSwap/ImpedanceRampHotSwap.py new file mode 100644 index 0000000..c6aa88e --- /dev/null +++ b/Examples/ImpedanceRampHotSwap/ImpedanceRampHotSwap.py @@ -0,0 +1,145 @@ +from thales_remote.epc_scpi_handler import EpcScpiHandlerFactory, EpcScpiHandler +from thales_remote.script_wrapper import PotentiostatMode +from zahner_potentiostat.scpi_control.datahandler import DataManager +from zahner_potentiostat.display.onlinedisplay import OnlineDisplay +from zahner_potentiostat.scpi_control.datareceiver import TrackTypes + + +class TargetCurrents: + def __init__(self, dc, amplitude, scanrate): + self.dc = dc + self.amplitude = amplitude + self.scanrate = scanrate + return + + +def measure_UI_EPC(deviceHandler): + for _ in range(3): + print( + f"EPC-Potential:\t{deviceHandler.sharedZenniumInterface.getPotential():>10.6f} V" + ) + print( + f"EPC-Current:\t{deviceHandler.sharedZenniumInterface.getCurrent():>10.3e} A" + ) + return + + +def measure_UI_SCPI(deviceHandler): + for _ in range(3): + print(f"SCPI-Potential:\t{deviceHandler.scpiInterface.getPotential():>10.6f} V") + print(f"SCPI-Current:\t{deviceHandler.scpiInterface.getCurrent():>10.3e} A") + return + + +if __name__ == "__main__": + startCurrent = 0.0 + handlerFactory = EpcScpiHandlerFactory("192.168.2.94") + deviceHandler = handlerFactory.createEpcScpiHandler( + epcChannel=1, serialNumber=33021 + ) + + deviceHandler.scpiInterface.calibrateOffsets() + + deviceHandler.acquireSharedZennium(blocking=True) + deviceHandler.switchToEPC() + + deviceHandler.sharedZenniumInterface.calibrateOffsets() + + deviceHandler.sharedZenniumInterface.setPotentiostatMode( + PotentiostatMode.POTMODE_GALVANOSTATIC + ) + deviceHandler.sharedZenniumInterface.setCurrent(startCurrent) + deviceHandler.sharedZenniumInterface.enablePotentiostat() + + measure_UI_EPC(deviceHandler) + deviceHandler.switchToSCPIAndReleaseSharedZennium(keepPotentiostatState=True) + measure_UI_SCPI(deviceHandler) + + measurementSettings = [ + TargetCurrents(dc=1, amplitude=0.1, scanrate=0.1), + TargetCurrents(dc=2, amplitude=0.1, scanrate=0.1), + TargetCurrents(dc=4, amplitude=0.2, scanrate=0.5), + ] + + for setting in measurementSettings: + + deviceHandler.scpiInterface.setCurrentValue( + deviceHandler.scpiInterface.getCurrent() + ) + + configuration = { + "figureTitle": f"Online Display Targetcurrent: {setting.dc} Scanrate: {setting.scanrate}", + "xAxisLabel": "Time", + "xAxisUnit": "s", + "xTrackName": TrackTypes.TIME.toString(), + "yAxis": [ + { + "label": "Voltage", + "unit": "V", + "trackName": TrackTypes.VOLTAGE.toString(), + }, + { + "label": "Current", + "unit": "A", + "trackName": TrackTypes.CURRENT.toString(), + }, + ], + } + onlineDisplay = OnlineDisplay( + deviceHandler.scpiInterface.getDataReceiver(), + displayConfiguration=configuration, + ) + + deviceHandler.scpiInterface.setCurrentParameter(setting.dc) + deviceHandler.scpiInterface.setScanRateParameter(setting.scanrate) + deviceHandler.scpiInterface.measureRampValueInScanRate() + + deviceHandler.scpiInterface.setMaximumTimeParameter(5) + deviceHandler.scpiInterface.measurePolarization() + while deviceHandler.acquireSharedZennium(blocking=False) == False: + deviceHandler.scpiInterface.measurePolarization() + + dataManager = DataManager(deviceHandler.scpiInterface.getDataReceiver()) + dataManager.saveDataAsText(f"ramp_to{setting.dc}a_{setting.scanrate}apers.txt") + + onlineDisplay.close() + del onlineDisplay + del dataManager + + measure_UI_SCPI(deviceHandler) + deviceHandler.switchToEPC(keepPotentiostatState=True) + measure_UI_EPC(deviceHandler) + + deviceHandler.sharedZenniumInterface.setEISNaming("individual") + deviceHandler.sharedZenniumInterface.setEISOutputPath(r"C:\THALES\temp") + deviceHandler.sharedZenniumInterface.setEISOutputFileName( + f"{setting.dc}adc_{setting.amplitude}aac".replace(".", "") + ) + + deviceHandler.sharedZenniumInterface.setPotentiostatMode( + PotentiostatMode.POTMODE_GALVANOSTATIC + ) + deviceHandler.sharedZenniumInterface.setPotential(setting.dc) + deviceHandler.sharedZenniumInterface.setAmplitude(setting.amplitude) + + deviceHandler.sharedZenniumInterface.setLowerFrequencyLimit(100) + deviceHandler.sharedZenniumInterface.setStartFrequency(250) + deviceHandler.sharedZenniumInterface.setUpperFrequencyLimit(500) + deviceHandler.sharedZenniumInterface.setLowerNumberOfPeriods(5) + deviceHandler.sharedZenniumInterface.setLowerStepsPerDecade(10) + deviceHandler.sharedZenniumInterface.setUpperNumberOfPeriods(20) + deviceHandler.sharedZenniumInterface.setUpperStepsPerDecade(10) + deviceHandler.sharedZenniumInterface.setScanDirection("startToMax") + deviceHandler.sharedZenniumInterface.setScanStrategy("single") + + deviceHandler.sharedZenniumInterface.enablePotentiostat() + deviceHandler.sharedZenniumInterface.measureEIS() + deviceHandler.sharedZenniumInterface.setAmplitude(0) + + measure_UI_EPC(deviceHandler) + deviceHandler.switchToSCPIAndReleaseSharedZennium(keepPotentiostatState=True) + measure_UI_SCPI(deviceHandler) + + deviceHandler.scpiInterface.setPotentiostatEnabled(False) + handlerFactory.closeAll() + print("finish") diff --git a/Examples/LoadWithExternalSource/LoadWithExternalSource.ipynb b/Examples/LoadWithExternalSource/LoadWithExternalSource.ipynb index ff8077d..e83da78 100644 --- a/Examples/LoadWithExternalSource/LoadWithExternalSource.ipynb +++ b/Examples/LoadWithExternalSource/LoadWithExternalSource.ipynb @@ -51,27 +51,14 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "connection successfull\n" - ] - } - ], + "outputs": [], "source": [ "if __name__ == \"__main__\":\n", " zenniumConnection = ThalesRemoteConnection()\n", - " connectionSuccessful = zenniumConnection.connectToTerm(\"localhost\")\n", - " if connectionSuccessful:\n", - " print(\"connection successfull\")\n", - " else:\n", - " print(\"connection not possible\")\n", - " sys.exit()\n", - " \n", + " zenniumConnection.connectToTerm(\"localhost\")\n", + " \n", " zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection)\n", " zahnerZennium.forceThalesIntoRemoteScript()\n", " zahnerZennium.calibrateOffsets()\n", @@ -300,7 +287,7 @@ } ], "source": [ - " ismFile = IsmImport(fileInterface.getLatestReceivedFile()[\"binary_data\"])\n", + " ismFile = IsmImport(fileInterface.getLatestReceivedFile().binaryData)\n", "\n", " impedanceFrequencies = ismFile.getFrequencyArray()\n", " impedanceAbsolute = ismFile.getImpedanceArray()\n", diff --git a/Examples/LoadWithExternalSource/LoadWithExternalSource.py b/Examples/LoadWithExternalSource/LoadWithExternalSource.py index 10c6b74..34196c6 100644 --- a/Examples/LoadWithExternalSource/LoadWithExternalSource.py +++ b/Examples/LoadWithExternalSource/LoadWithExternalSource.py @@ -2,10 +2,10 @@ import time from delta_remote.connection import DeltaConnection -from delta_remote.script_wrapper import DeltaSCPIWrapper,DeltaSources +from delta_remote.script_wrapper import DeltaSCPIWrapper, DeltaSources from thales_remote.connection import ThalesRemoteConnection -from thales_remote.script_wrapper import PotentiostatMode,ThalesRemoteScriptWrapper +from thales_remote.script_wrapper import PotentiostatMode, ThalesRemoteScriptWrapper from thales_remote.file_interface import ThalesFileInterface from zahner_analysis.file_import.ism_import import IsmImport @@ -15,26 +15,21 @@ if __name__ == "__main__": zenniumConnection = ThalesRemoteConnection() - connectionSuccessful = zenniumConnection.connectToTerm("localhost") - if connectionSuccessful: - print("connection successfull") - else: - print("connection not possible") - sys.exit() - + zenniumConnection.connectToTerm("localhost") + zahnerZennium = ThalesRemoteScriptWrapper(zenniumConnection) zahnerZennium.forceThalesIntoRemoteScript() zahnerZennium.calibrateOffsets() - + fileInterface = ThalesFileInterface("localhost") fileInterface.enableKeepReceivedFilesInObject() fileInterface.enableAutomaticFileExchange() deltaConnection = DeltaConnection() - deltaConnection.connect(ip = "192.168.2.73", port = 8462) - + deltaConnection.connect(ip="192.168.2.73", port=8462) + deltaSM18_220 = DeltaSCPIWrapper(deltaConnection) - + print(f"IDN:\t{deltaSM18_220.IDN()}") deltaSM18_220.setProgSourceVoltage(DeltaSources.eth) @@ -43,7 +38,7 @@ zahnerZennium.setEISOutputPath(r"C:\THALES\temp") zahnerZennium.setEISNaming("individual") zahnerZennium.setEISOutputFileName("spectra_5aac") - + zahnerZennium.selectPotentiostat(1) zahnerZennium.setPotentiostatMode(PotentiostatMode.POTMODE_GALVANOSTATIC) zahnerZennium.setAmplitude(0) @@ -59,52 +54,78 @@ zahnerZennium.setCurrent(0) zahnerZennium.enablePotentiostat() - + time.sleep(1.0) - + deltaSM18_220.setTargetVoltage(6) deltaSM18_220.setTargetCurrent(20) deltaSM18_220.enableOutput() - + time.sleep(1) print(f"Measured current\tEL1002:\t\t{zahnerZennium.getCurrent():>10.3f} A") - - print(f"Measured current\tDelta SM18-220:\t{deltaSM18_220.getMeasuredCurrent():>10.3f} A") - print(f"Measured voltage\tDelta SM18-220:\t{deltaSM18_220.getMeasuredVoltage():>10.3f} V") - print(f"Measured power\t\tDelta SM18-220:\t{deltaSM18_220.getMeasuredPower():>10.3f} W") + + print( + f"Measured current\tDelta SM18-220:\t{deltaSM18_220.getMeasuredCurrent():>10.3f} A" + ) + print( + f"Measured voltage\tDelta SM18-220:\t{deltaSM18_220.getMeasuredVoltage():>10.3f} V" + ) + print( + f"Measured power\t\tDelta SM18-220:\t{deltaSM18_220.getMeasuredPower():>10.3f} W" + ) zahnerZennium.setAmplitude(5) zahnerZennium.measureEIS() zahnerZennium.setAmplitude(0) deltaSM18_220.disableOutput() - + time.sleep(1) - + zahnerZennium.disablePotentiostat() - ismFile = IsmImport(fileInterface.getLatestReceivedFile()["binary_data"]) + ismFile = IsmImport(fileInterface.getLatestReceivedFile().binaryData) impedanceFrequencies = ismFile.getFrequencyArray() impedanceAbsolute = ismFile.getImpedanceArray() impedancePhase = ismFile.getPhaseArray() impedanceComplex = ismFile.getComplexImpedanceArray() - + figBode, (impedanceAxis) = plt.subplots(1, 1) - + phaseAxis = impedanceAxis.twinx() - - impedanceAxis.loglog(impedanceFrequencies, impedanceAbsolute, linestyle='dashed', linewidth=1, marker="o", markersize=5, fillstyle = "none", color = "blue") + + impedanceAxis.loglog( + impedanceFrequencies, + impedanceAbsolute, + linestyle="dashed", + linewidth=1, + marker="o", + markersize=5, + fillstyle="none", + color="blue", + ) impedanceAxis.xaxis.set_major_formatter(EngFormatter(unit="Hz")) impedanceAxis.yaxis.set_major_formatter(EngFormatter(unit="$\Omega$")) impedanceAxis.set_xlabel(r"f") impedanceAxis.set_ylabel(r"|Z|") impedanceAxis.yaxis.label.set_color("blue") impedanceAxis.grid(which="both", linestyle="dashed", linewidth=0.5) - impedanceAxis.set_xlim([min(impedanceFrequencies)*0.8, max(impedanceFrequencies)*1.2]) - - phaseAxis.semilogx(impedanceFrequencies, np.abs(impedancePhase * (360 / (2 * np.pi))), linestyle='dashed', linewidth=1, marker="o", markersize=5, fillstyle = "none", color = "red") + impedanceAxis.set_xlim( + [min(impedanceFrequencies) * 0.8, max(impedanceFrequencies) * 1.2] + ) + + phaseAxis.semilogx( + impedanceFrequencies, + np.abs(impedancePhase * (360 / (2 * np.pi))), + linestyle="dashed", + linewidth=1, + marker="o", + markersize=5, + fillstyle="none", + color="red", + ) phaseAxis.yaxis.set_major_formatter(EngFormatter(unit="$°$", sep="")) phaseAxis.xaxis.set_major_formatter(EngFormatter(unit="Hz")) phaseAxis.set_xlabel(r"f") @@ -121,6 +142,5 @@ deltaSM18_220.setProgSourceVoltage(DeltaSources.front) deltaSM18_220.setProgSourceCurrent(DeltaSources.front) deltaConnection.disconnect() - - print("finish") + print("finish") diff --git a/Examples/jupyter_utils.py b/Examples/jupyter_utils.py new file mode 100644 index 0000000..8bbd5d1 --- /dev/null +++ b/Examples/jupyter_utils.py @@ -0,0 +1,107 @@ +""" + ____ __ __ __ __ _ __ + /_ / ___ _/ / ___ ___ ___________ / /__ / /__/ /_____(_) /__ + / /_/ _ `/ _ \/ _ \/ -_) __/___/ -_) / -_) '_/ __/ __/ / '_/ + /___/\_,_/_//_/_//_/\__/_/ \__/_/\__/_/\_\\__/_/ /_/_/\_\ + +Copyright 2023 Zahner-Elektrik GmbH & Co. KG + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +import json +import os +import pathlib + +""" +This module contains internally required functions to automatically convert Jupyter notebooks into +pure python source code files. This way the user does not have to manually extract all the python +code from the jupyter code blocks. + +Also a function has been implemented to detect if the source code is executed with python or in a +jupyter environment. +""" + + +def notebookCodeToPython(jupyterNotebookName): + """Convert jupyter-notebook to python. + + This function extracts all code lines from a jupyter notebook and saves them as a file. + With the standard jupyter export the whole documentation is inserted as a comment, but this is + not desired. + + The code is then saved to a .py file of equal name. + + :param jupyterNotebookName: The notebook file name. + """ + notebookText = "" + with open(jupyterNotebookName, "r", encoding="utf-8") as f: + notebookJson = json.load(f) + for cell in notebookJson["cells"]: + if "code" in cell["cell_type"]: + for line in cell["source"]: + notebookText += line.rstrip("\n") + "\n" + notebookText += "\n" + + jupyterName = jupyterNotebookName.replace("ipynb", "py") + with open(jupyterName, "wb") as f: + f.write(notebookText.encode(encoding="UTF-8")) + + os.system(f"python -m black {jupyterName}") + return + + +def executionInNotebook(): + """Check if the code is executed in jupyter enviroment. + + This function checks if the execution of the code is done in python or in jupyter. + The recognition is done via IPython and Python, it was only tested that it works on the + development system with normal Python and the notebooks. It may be that an IPython environment + is also recognised as a notebook. + + **This function is only needed for automation. The user does not need to use it.** + + :returns: True if the execution takes place in the jupyter enviroment. + :rtype: bool + """ + retval = False + try: + shell = get_ipython().__class__.__name__ + if "ZMQInteractiveShell" in shell: + retval = True + except NameError: + pass + + return retval + + +if __name__ == "__main__": + + notebooks = [ + str(file) + for file in list( + pathlib.Path(os.path.dirname(os.path.realpath(__file__))).rglob("*.ipynb") + ) + if "checkpoint" not in str(file) + ] + + for notebook in notebooks: + print(notebook) + notebookCodeToPython(notebook) + + print("finish") diff --git a/README.md b/README.md index 629ce69..08c9ea8 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Or add the path of the directory [thales_remote](thales_remote) on your computer # 🔬 Measurement Data Analysis -There is a [separate Python package on GitHub](https://github.com/Zahner-elektrik/Zahner-Analysis-Python) for analyzing measurement data. +There is a separate Python package on [GitHub](https://github.com/Zahner-elektrik/Zahner-Analysis-Python) and [PyPI/pip](https://pypi.org/project/zahner-analysis/) for analyzing measurement data. In this repository there are examples of how to fit equivalent electrical circuit models to electrochemical impedance spectra, also known as EIS equivalent circuit fitting. The model parameters can be further processed after the fit with Python, for example for the comparison of serial measurements. @@ -107,7 +107,8 @@ In the examples only one method is explained and parameterized at a time for bet * Measure cylic voltammetry measurement * Setting output file naming for CV measurements * Parametrizing an CV measurement -* Measurement with an external potentiostat (EPC-Device) +* Importing the measurement results from the isc file into Python +* **Acquiring the measurement files with Python via network** ## [CVImportPlot.ipynb](Examples/CVImportPlot/CVImportPlot.ipynb) @@ -128,13 +129,13 @@ In the examples only one method is explained and parameterized at a time for bet * The [Zahner sequencer](https://doc.zahner.de/manuals/sequencer.pdf) outputs current and voltage curves defined in a text file. * Setting output file naming for sequence measurements * Parametrizing an sequence measurement -* Measurement with an external potentiostat (EPC-Device) +* Measurement with an [external potentiostat](https://zahner.de/products#external-potentiostats) or [external load](https://zahner.de/products#electronic-loads) ([EPC-Device](https://zahner.de/products-details/addon-cards/epc42)) ## [EISCVLaTeX.ipynb](Examples/EISCVLaTeX/EISCVLaTeX.ipynb) * Measure impedance specta and cyclic voltammetry * Plotting the measurement data. -* Create a PDF with the measurement data using LaTeX. +* Create a PDF with the measurement data using [LaTeX](https://www.latex-project.org/zah) ## [EISPad4.ipynb](Examples/EISPad4/EISPad4.ipynb) @@ -161,20 +162,26 @@ In the examples only one method is explained and parameterized at a time for bet * **Acquiring the measurement files with Python via network** * **Plotting the spectrum in bode representation with the matplotlib library** -## [ImpedanceMultiCellCycle.ipynb](https://github.com/Zahner-elektrik/Zahner-Remote-Python/blob/master/Examples/ImpedanceMultiCellCycle/ImpedanceMultiCellCycle.ipynb) +## [ImpedanceMultiCellCycle.ipynb](Examples/ImpedanceMultiCellCycle/ImpedanceMultiCellCycle.ipynb) -* Multichannel operation with several external potentiostats, of the latest generation, type **PP212, PP222, PP242 or XPOT2**. -* Control of standalone operation of external potentiostats with the [zahner_potentiostat](https://github.com/Zahner-elektrik/zahner_potentiostat) library. -* Shared [Zennium series](https://zahner.de/products#potentiostats) device for impedance measurements. +* Multichannel operation with several external potentiostats, of the latest generation, type **PP2x2, XPOT2 or EL1002** +* Shared [Zennium series](https://zahner.de/products#potentiostats) device for impedance measurements +* Operation of the power potentiostats standalone without thales with the Python package [zahner_potentiostat](https://github.com/Zahner-elektrik/zahner_potentiostat) + +## [ImpedanceRampHotSwap.ipynb](Examples/ImpedanceRampHotSwap/ImpedanceRampHotSwap.ipynb) + +* Switch between Thales/EPC and SCPI/standalone operation of the external potentiostats (PP2x2, XPOT2 or EL1002) **without switching off the potentiostat** +* Shared [Zennium series](https://zahner.de/products#potentiostats) device for impedance measurements +* Operation of the power potentiostats standalone without thales with the Python package [zahner_potentiostat](https://github.com/Zahner-elektrik/zahner_potentiostat) ## [BCMuxInterface.ipynb](Examples/BCMuxInterface/BCMuxInterface.ipynb) -* Remote control of the BC-MUX. -* Class which realizes the remote control. +* Remote control of the BC-MUX +* Class which realizes the remote control # 📧 Haveing a question? -Send an mail to our support team. +Send a [mail](mailto:support@zahner.de?subject=Thales-Remote-Python%20Question&body=Your%20Message) to our support team. # ⁉️ Found a bug or missing a specific feature? @@ -188,5 +195,7 @@ Programming is done with the latest Python version at the time of commit. For the [thales_remote](thales_remote) package only the Python standard library was used. If measurement data are imported and plotted, the package [zahner_analysis](https://github.com/Zahner-elektrik/Zahner-Analysis-Python) is used. +For standalone communication without Thales with the PP2x2, XPOT2 or EL1002 devices the [zahner_potentiostat](https://github.com/Zahner-elektrik/zahner_potentiostat) package is used. + The packages [matplotlib](https://matplotlib.org/), [SciPy](https://scipy.org/) and [NumPy](https://numpy.org/) are used in some examples to display the measurement data graphically. Jupyter is not necessary, since each example is also available as a Python file. diff --git a/delta_remote/connection.py b/delta_remote/connection.py index 77ee16e..c49cf73 100644 --- a/delta_remote/connection.py +++ b/delta_remote/connection.py @@ -4,7 +4,7 @@ / /_/ _ `/ _ \/ _ \/ -_) __/___/ -_) / -_) '_/ __/ __/ / '_/ /___/\_,_/_//_/_//_/\__/_/ \__/_/\__/_/\_\\__/_/ /_/_/\_\ -Copyright 2022 Zahner-Elektrik GmbH & Co. KG +Copyright 2023 Zahner-Elektrik GmbH & Co. KG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), diff --git a/delta_remote/script_wrapper.py b/delta_remote/script_wrapper.py index 9f84ab8..e929f44 100644 --- a/delta_remote/script_wrapper.py +++ b/delta_remote/script_wrapper.py @@ -4,7 +4,7 @@ / /_/ _ `/ _ \/ _ \/ -_) __/___/ -_) / -_) '_/ __/ __/ / '_/ /___/\_,_/_//_/_//_/\__/_/ \__/_/\__/_/\_\\__/_/ /_/_/\_\ -Copyright 2022 Zahner-Elektrik GmbH & Co. KG +Copyright 2023 Zahner-Elektrik GmbH & Co. KG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), diff --git a/pyproject.toml b/pyproject.toml index 34aeac5..29a6d18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,18 +1,20 @@ +[build-system] +requires = ["setuptools>=61.0", "setuptools-scm"] +build-backend = "setuptools.build_meta" + [project] name = "thales_remote" -version = "5.7.0" authors = [{ name = "Maximilian Krapp", email = "maximilian.krapp@zahner.de" }] -description = "library to control Zahner Zennium potentiostats" +description = "Library to control Zahner Zennium potentiostats" keywords = [ - "potentiostat, electrochemistry, chemistry, eis, cyclic voltammetry, fuel-cell, battery", + "potentiostat", "electrochemistry", "chemistry", "eis", "cyclic voltammetry", "fuel-cell", "battery", ] readme = "README.md" license = { file = "LICENSE" } -requires_python = ">=3.9" +requires-python = ">=3.9" dependencies = [ - "matplotlib", - "pyserial", - "numpy" + "zahner_analysis", + "zahner_potentiostat", ] classifiers = [ "Development Status :: 5 - Production/Stable", @@ -28,17 +30,23 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] +dynamic = ["version"] [project.urls] -"Documentation"= "https://doc.zahner.de/zahner_potentiostat/index.html" -"Examples"= "https://github.com/Zahner-elektrik/Zahner-Remote-Python" -"Source Code"= "https://github.com/Zahner-elektrik/zahner_potentiostat" -"Bug Tracker"= "https://github.com/Zahner-elektrik/zahner_potentiostat/issues" +"Documentation"= "https://doc.zahner.de/thales_remote/" +"Examples"= "https://github.com/Zahner-elektrik/Thales-Remote-Python" +"Source Code"= "https://github.com/Zahner-elektrik/Thales-Remote-Python" +"Bug Tracker"= "https://github.com/Zahner-elektrik/Thales-Remote-Python/issues" "Homepage" = "https://zahner.de/" -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +[tool.setuptools_scm] + +[tool.setuptools.packages.find] +where = ["."] # list of folders that contain the packages (["."] by default) +include = ["thales_remote*"] # package names should match these glob patterns (["*"] by default) + + diff --git a/thales_remote/connection.py b/thales_remote/connection.py index b4019a8..574f783 100644 --- a/thales_remote/connection.py +++ b/thales_remote/connection.py @@ -4,7 +4,7 @@ / /_/ _ `/ _ \/ _ \/ -_) __/___/ -_) / -_) '_/ __/ __/ / '_/ /___/\_,_/_//_/_//_/\__/_/ \__/_/\__/_/\_\\__/_/ /_/_/\_\ -Copyright 2022 Zahner-Elektrik GmbH & Co. KG +Copyright 2023 Zahner-Elektrik GmbH & Co. KG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -33,6 +33,8 @@ from _socket import SHUT_RD from thales_remote.error import TermConnectionError +from datetime import datetime + class ThalesRemoteConnection(object): """ @@ -61,7 +63,7 @@ def __init__(self): self._queuesForChannels[channel] = queue.Queue() self._connectionName = "" - + # methods for context handler # documentation: https://docs.python.org/3/reference/datamodel.html#context-managers def __enter__(self): @@ -77,9 +79,6 @@ def connectToTerm( """ Connect to Term Software. - TODO: - - should raise exception on failure – returning bools is C-style error handling - :param address: hostname or ip-address of the host running "Term" application :param connection_name: name of the connection ScriptRemote for Remote and Logging as Online Display :returns: True on success, False on failure @@ -88,8 +87,7 @@ def connectToTerm( try: self._socket_handle.connect((address, self._term_port)) except: - self._socket_handle = None - return False + raise TermConnectionError("Connection to the Term not possible.") self._startTelegramListener() diff --git a/thales_remote/epc_scpi_handler.py b/thales_remote/epc_scpi_handler.py new file mode 100644 index 0000000..a8aef8b --- /dev/null +++ b/thales_remote/epc_scpi_handler.py @@ -0,0 +1,350 @@ +from thales_remote.connection import ThalesRemoteConnection +from thales_remote.script_wrapper import PotentiostatMode, ThalesRemoteScriptWrapper +from thales_remote.error import TermConnectionError, ThalesRemoteError + +from zahner_potentiostat.scpi_control.searcher import SCPIDeviceSearcher +from zahner_potentiostat.scpi_control.serial_interface import ( + SerialCommandInterface, + SerialDataInterface, +) +from zahner_potentiostat.scpi_control.control import * +from zahner_potentiostat.scpi_control.datahandler import DataManager + +import threading +import time +from dataclasses import dataclass + + +class EpcScpiHandler: + """Class for the control objects. + + This class manages the object composed of a cennium and the external potentiostat. + The object contains an instance of a :class:`~zahner_potentiostat.scpi_control.control.SCPIDevice` + and the shared common :class:`~thales_remote.script_wrapper.ThalesRemoteScriptWrapper` object. + + The SCPI object is invalid when the device is in EPC mode. + The device must be manually switched from EPC to SCPI before the SCPI object can be used again. + + :param sharedZennium: Zennium object. + :type sharedZennium: :class:`~thales_remote.script_wrapper.ThalesRemoteScriptWrapper` + :param epcChannel: Number of the EPC channel to which the device is connected via EPC cable. + If a Rmux card is plugged in then the numbers have an offset. + :param serialNumber: Serial number of the external potentiostat. + """ + + zenniumMutex = threading.Lock() # class variable + sharedZenniumInterface: ThalesRemoteScriptWrapper + _epcId: int + _serialNumber: int + _isInEPC: bool + _commandInterface: Union[SerialCommandInterface, None] + scpiInterface: Union[SCPIDevice, None] + + def __init__( + self, + sharedZennium: ThalesRemoteScriptWrapper, + epcPotentiostatId: int, + serialNumber: int, + ): + self.sharedZenniumInterface = sharedZennium + self._epcId = epcPotentiostatId + self._serialNumber = serialNumber + self._isInEPC = True + + self._commandInterface = None + self.scpiInterface = None + return + + def isSharedZenniumAvailable(self) -> bool: + """Check if the zennium is available. + + The method checks if the threading.lock for synchronizing access to the Zennium is available. + + :returns: True if the zennium is not locked and available. + """ + return EpcScpiHandler.zenniumMutex.locked() == False + + def acquireSharedZennium(self, blocking: bool = True, timeout: int = -1) -> bool: + """Check if the Zennium is available. + + Wrapper for the aquire method of the `Python lock object `_. + The parameters and return values are simply passed through. + + :param blocking: When invoked with the blocking argument set to True (the default), + block until the lock is unlocked, then set it to locked and return True. + :param timeout: When invoked with the floating-point timeout argument set to a positive value, + block for at most the number of seconds specified by timeout and as long as the lock cannot + be acquired. A timeout argument of -1 specifies an unbounded wait. It is forbidden to + specify a timeout when blocking is false. + :returns: The return value is True if the lock is acquired successfully, False if not + (for example if the timeout expired). + """ + return EpcScpiHandler.zenniumMutex.acquire(blocking, timeout) + + def releaseSharedZennium(self) -> None: + """Release the Zennium object. + + Wrapper for the aquire method of the `Python lock object `_. + + Release a lock. This can be called from any thread, not only the thread which has acquired the lock. + When the lock is locked, reset it to unlocked, and return. If any other threads are blocked + waiting for the lock to become unlocked, allow exactly one of them to proceed. + + When invoked on an unlocked lock, a RuntimeError is raised. + """ + EpcScpiHandler.zenniumMutex.release() + return + + def connectSCPIDevice(self) -> None: + """Establish connection to the potentiostat. + + This method establishes the connection to the potentiostat (PP2x2, XPOT2 and EL1002) and passes it to the internal data structure. + When invoked on an unlocked lock, a RuntimeError is raised. + """ + deviceSearcher = SCPIDeviceSearcher() + deviceSearcher.searchZahnerDevices() + commandSerial, dataSerial = deviceSearcher.selectDevice(self._serialNumber) + self._commandInterface = SerialCommandInterface(commandSerial) + + self.scpiInterface = SCPIDevice( + self._commandInterface, SerialDataInterface(dataSerial) + ) + return + + def switchToSCPIAndReleaseSharedZennium( + self, keepPotentiostatState: bool = False + ) -> None: + """Switch from EPC to SCPI mode of the potentiostat and release the Zennium. + + The switch from EPC to SCPI must be made from the EPC operation, both control options can + only release control but cannot take control away from each other. + + After the control is released, the Zennium is released. + + :param keepPotentiostatState: If this parameter is True, + the potentiostat is not switched off when switching from EPC to SCPI. + """ + self.sharedZenniumInterface.selectPotentiostat(self._epcId) + + if keepPotentiostatState: + self.sharedZenniumInterface.switchToSCPIControlWithoutPotentiostatStateChange() + else: + self.sharedZenniumInterface.switchToSCPIControl() + + self._isInEPC = False + self.releaseSharedZennium() + + """ + It takes some time for the operating system to recognize the USB device. + + It tries to find the USB device 3 times every 3 seconds. If this fails, an exception is thrown. + """ + maxTry = 3 + for i in range(maxTry): + try: + time.sleep(3) + self.connectSCPIDevice() + except Exception as e: + if i == (maxTry - 1): + raise e + else: + break # for loop + return + + def switchToSCPI(self, keepPotentiostatState: bool = False) -> None: + """Switch from EPC to SCPI mode. + + It is recommended to use :class:`~thales_remote.epc_scpi_handler.EpcScpiHandler.switchToSCPIAndReleaseSharedZennium` instead of this function. + + Before calling this method, the Zennium must have been released, since these methods call + aquire and release themselves. + If the Zennium was locked before this function will block. + + :param keepPotentiostatState: If this parameter is True, + the potentiostat is not switched off when switching from EPC to SCPI. + """ + self.acquireSharedZennium() + + if keepPotentiostatState: + self.sharedZenniumInterface.selectPotentiostatWithoutPotentiostatStateChange( + self._epcId + ) + self.sharedZenniumInterface.switchToSCPIControlWithoutPotentiostatStateChange() + else: + self.sharedZenniumInterface.selectPotentiostat(self._epcId) + self.sharedZenniumInterface.switchToSCPIControl() + + self._isInEPC = False + self.releaseSharedZennium() + """ + Wait a little so that windows recognizes the new usb device when the potentiostat logs on again. + """ + time.sleep(3) + self.connectSCPIDevice() + return + + def switchToEPC(self, keepPotentiostatState: bool = False) -> None: + """Switch from SCPI to EPC mode. + + Before calling this method the Zennium must be locked with aquire. + + This method is used to switch from SCPI to EPC operation. After this method is called, the + scpiInterface object is destroyed because the USB connection is closed. + + This method automatically selects the correct EPC channel. + + :param keepPotentiostatState: If this parameter is True, + the potentiostat is not switched off when switching from SCPI to EPC. + """ + try: + if keepPotentiostatState: + self.scpiInterface.switchToEPCControlWithoutPotentiostatStateChange() + else: + self.scpiInterface.switchToEPCControl() + self.scpiInterface.close() + except: + pass + finally: + self.scpiInterface = None + self._isInEPC = True + + """ + Wait a little for the change to EPC. + """ + time.sleep(2) + if keepPotentiostatState: + self.sharedZenniumInterface.selectPotentiostatWithoutPotentiostatStateChange( + self._epcId + ) + else: + self.sharedZenniumInterface.selectPotentiostat(self._epcId) + return + + def getSerialNumber(self): + return self._serialNumber + + def close(self): + """Close the SCPI connection. + + The function is not required in epc mode. + """ + if self._isInEPC is False: + self.scpiInterface.close() + return + + +@dataclass +class HandlerDataItem: + serialNumber: int + epcIndex: int + handlerObject: EpcScpiHandler + + +class EpcScpiHandlerFactory: + """Class for creating the control objects. + + This class initializes the connection to the zennium. + The :func:`~epc_scpi_handler.EpcScpiHandlerFactory.createEpcScpiHandler` method can then be used + to create a control object for the corresponding device. + + :param shared_zennium_target: IP address at which the Zennium can be reached. Default is "localhost". + """ + + _zenniumConnection: ThalesRemoteConnection + sharedZenniumInterface: ThalesRemoteScriptWrapper + _handlerList: list[HandlerDataItem] + + def __init__(self, shared_zennium_target="127.0.0.1"): + self._zenniumConnection = ThalesRemoteConnection() + connectionSuccessful = self._zenniumConnection.connectToTerm( + shared_zennium_target, "ScriptRemote" + ) + if connectionSuccessful is False: + raise TermConnectionError("connection to zennium not possible") + + self.sharedZenniumInterface = ThalesRemoteScriptWrapper(self._zenniumConnection) + self.sharedZenniumInterface.forceThalesIntoRemoteScript() + self._handlerList = [] + return + + def getSharedZennium(self) -> ThalesRemoteScriptWrapper: + """Returns the zennium object. + + Returns the Zennium object, which contains the Remote2 commands as methods. + + :returns: Object with the Remote2 wrapper. + """ + return self.sharedZenniumInterface + + def getZenniumConnection(self) -> ThalesRemoteConnection: + """Returns the zennium connection object. + + Returns the object that manages the connection to the zennium. + + :returns: Object with the connection to the zennium. + """ + return self._zenniumConnection + + def createEpcScpiHandler( + self, epcChannel: int, serialNumber: int + ) -> EpcScpiHandler: + """Returns the zennium connection object. + + This method initializes the external potentiostats and creates the objects. + + The objects are in SCPI mode after calling this function. + For compatibility, the devices always start in EPC mode when connected to EPC, then they must + be switched to SCPI standalone mode via Remote2. It is only possible to switch to SCPI mode via Remote2. + + :param epcChannel: Number of the EPC channel to which the device is connected via EPC cable. + If a Rmux card is plugged in then the numbers have an offset. + :param serialNumber: Serial number of the external potentiostat. + :returns: Object with the external potentiostat. + """ + newDevice = EpcScpiHandler(self.getSharedZennium(), epcChannel, serialNumber) + + deviceSearcher = SCPIDeviceSearcher() + deviceSearcher.searchZahnerDevices() + commandSerial: SerialCommandInterface = None + dataSerial: SerialDataInterface = None + + try: + commandSerial, dataSerial = deviceSearcher.selectDevice(serialNumber) + except: + pass + + """ + If the device is not found, then it is checked whether it is found as an EPC device. + If it is found as an EPC device, it is switched to SCPI mode. + """ + if commandSerial is None and dataSerial is None: + newDevice.acquireSharedZennium() + newDevice.sharedZenniumInterface.selectPotentiostat(epcChannel) + name, serial = newDevice.sharedZenniumInterface.getDeviceInformation() + newDevice.releaseSharedZennium() + + if serial not in str(serialNumber): + raise ThalesRemoteError("Potentiostat is not found on the EPC channel.") + + newDevice.switchToSCPI() + else: + newDevice.connectSCPIDevice() + + listItem = HandlerDataItem(serialNumber, epcChannel, newDevice) + + self._handlerList.append(listItem) + + return newDevice + + def closeAll(self) -> None: + """Close connections to all devices. + + This command closes all connections to the external potentiostats and to the Zennium. + """ + for element in self._handlerList: + element.handlerObject.close() + + self._handlerList = [] + + self._zenniumConnection.disconnectFromTerm() + return diff --git a/thales_remote/error.py b/thales_remote/error.py index 5f28945..17cb9dd 100644 --- a/thales_remote/error.py +++ b/thales_remote/error.py @@ -4,7 +4,7 @@ / /_/ _ `/ _ \/ _ \/ -_) __/___/ -_) / -_) '_/ __/ __/ / '_/ /___/\_,_/_//_/_//_/\__/_/ \__/_/\__/_/\_\\__/_/ /_/_/\_\ -Copyright 2022 Zahner-Elektrik GmbH & Co. KG +Copyright 2023 Zahner-Elektrik GmbH & Co. KG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), diff --git a/thales_remote/file_interface.py b/thales_remote/file_interface.py index f321612..97a8181 100644 --- a/thales_remote/file_interface.py +++ b/thales_remote/file_interface.py @@ -4,7 +4,7 @@ / /_/ _ `/ _ \/ _ \/ -_) __/___/ -_) / -_) '_/ __/ __/ / '_/ /___/\_,_/_//_/_//_/\__/_/ \__/_/\__/_/\_\\__/_/ /_/_/\_\ -Copyright 2022 Zahner-Elektrik GmbH & Co. KG +Copyright 2023 Zahner-Elektrik GmbH & Co. KG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -28,9 +28,17 @@ import threading import time from queue import Empty -from typing import Optional, Union +from typing import Optional, Union, ByteString from thales_remote.connection import ThalesRemoteConnection +from dataclasses import dataclass + + +@dataclass +class FileData: + name: str + path: str + binaryData: ByteString class ThalesFileInterface(object): @@ -53,7 +61,7 @@ class ThalesFileInterface(object): _receiver_is_running: bool _automatic_file_exchange: bool _files_to_skip: list[str] - receivedFiles: list[dict[str, Union[str, bytearray]]] + receivedFiles: list[FileData] pathToSave: str _save_received_files_to_disk: bool _keep_files_in_object: bool @@ -95,14 +103,12 @@ def enableAutomaticFileExchange( the received files as an array with :func:`~thales_remote.file_interface.ThalesFileInterface.getReceivedFiles`. The standard setting is that the files remain in the object. - TODO: - - The `fileExtensions` parameter looks flimsy; consider using sth. more robust - :param enable: Enable automatic file exchange. Default = True. :param fileExtensions: File extensions that will be exchanged. You can see the default paramter. But you can also specify only one type of file or more. :type fileExtensions: string :returns: The response string from the device. + :rtype: string """ if enable: retval = self.remoteConnection.sendStringAndWaitForReplyString( @@ -130,10 +136,7 @@ def appendFilesToSkip(self, file: Union[str, list[str]]): Files with these names are not saved to disk by Python and do not remain in the object. - TODO: - - not sure if the `+=` operator works for both `str` and `list[str]` - - :param file: filename or list with filenames to be filtered + :param file: Filename or list with filenames to be filtered. """ if isinstance(file, list): self._files_to_skip.extend(file) @@ -148,7 +151,7 @@ def disableAutomaticFileExchange(self) -> str: """ return self.enableAutomaticFileExchange(False) - def aquireFile(self, filename: str) -> Optional[dict[str, Union[str, bytearray]]]: + def aquireFile(self, filename: str) -> Union[FileData, None]: """ transfer a single file @@ -157,17 +160,10 @@ def aquireFile(self, filename: str) -> Optional[dict[str, Union[str, bytearray]] The parameter filename is used to specify the full path of the file, on the computer running the Thales software, to be transferred e.g. r"C:\\THALES\\temp\\test1\\myeis.ism". - The function returns the file as dictionary. The dictionary has the following keys: - - * "name": Filename without path. - * "path": Filename with path on the Thales computer. - * "binary_data": Data as bytearray. - - If the file does not exist, the key "binary_data" contains an empty byte array. + The function returns the file as dataclass. :param filename: Filename with path on the Thales computer. - :type filename: string - :returns: A dictionary with the file or None if this command is called when automatic file + :returns: A dataclass with the file or None if this command is called when automatic file exchange is activated. """ if self._receiver_is_running: @@ -186,7 +182,6 @@ def setSavePath(self, path: str) -> None: The path must be accessible by Python, otherwise there are no restrictions on the path. :param path: Path where the files should be saved. For example r"D:\\myLocalDirectory". - :type path: string """ self.pathToSave = path os.makedirs(self.pathToSave, exist_ok=True) @@ -205,11 +200,11 @@ def enableSaveReceivedFilesToDisk( :param enable: Enable automatic file saving to the hard disk. Default = True. :param path: Optional path where the files should be saved. For example r"D:\\myLocalDirectory". - :type path: string """ if path is not None: self.setSavePath(path) self._save_received_files_to_disk = enable + return def disableSaveReceivedFilesToDisk(self) -> None: """ @@ -217,7 +212,7 @@ def disableSaveReceivedFilesToDisk(self) -> None: """ return self.enableSaveReceivedFilesToDisk(False) - def enableKeepReceivedFilesInObject(self, enable: bool = True): + def enableKeepReceivedFilesInObject(self, enable: bool = True) -> None: """Enable that the files remain in the Python object. If you perform many measurements, the Python object would grow larger and larger due to the @@ -235,7 +230,7 @@ def disableKeepReceivedFilesInObject(self) -> None: """ return self.enableKeepReceivedFilesInObject(False) - def getReceivedFiles(self) -> list[dict[str, Union[str, bytearray]]]: + def getReceivedFiles(self) -> list[FileData]: """ read all files from the Python object @@ -246,7 +241,7 @@ def getReceivedFiles(self) -> list[dict[str, Union[str, bytearray]]]: """ return self.receivedFiles - def getLatestReceivedFile(self) -> dict[str, Union[str, bytearray]]: + def getLatestReceivedFile(self) -> FileData: """ read the latest files from the Python object @@ -264,34 +259,22 @@ def deleteReceivedFiles(self) -> None: # The following methods should not be called by the user. # They are marked with the prefix '_' after the Python convention for proteced. - def _saveReceivedFile(self, fileToWrite: str) -> None: + def _saveReceivedFile(self, fileToWrite: FileData) -> None: """ - saves the passed file to disk ONLY IF self._save_received_files_to_disk is enabled - - TODO: - - if not self._save_received_files_to_disk, then this method seems to fail silently + saves the passed file to disk. """ - if self._save_received_files_to_disk: - fileNameWithPath = os.path.join(self.pathToSave, fileToWrite["name"]) + fileNameWithPath = os.path.join(self.pathToSave, fileToWrite.name) - with open(fileNameWithPath, "wb") as file: - file.write(fileToWrite["binary_data"]) - file.close() + with open(fileNameWithPath, "wb") as file: + file.write(fileToWrite.binaryData) return - def _receiveFile( - self, timeout: Optional[float] = None - ) -> dict[str, Union[str, bytearray]]: + def _receiveFile(self, timeout: Optional[float] = None) -> Union[FileData, None]: """ receive one file with optional timeout - TODO: - - do not return a dict but a struct; this will prevent bugs in the future - - :returns: a dict with these key-value-pairs: - - "name": file name as `str` - - "path": file path as `str` - - "binary_data": binary data as `bytearray` + :param timeout: receive timeout + :returns: structure with the file """ try: @@ -310,10 +293,10 @@ def _receiveFile( fileData += receivedBytes bytesToReceive -= len(receivedBytes) - file_split = filePath.split("\\") - file_name = file_split[-1] + fileSplit = filePath.split("\\") + fileName = fileSplit[-1] - return {"name": file_name, "path": filePath, "binary_data": fileData} + return FileData(fileName, filePath, fileData) def _startWorker(self) -> None: """ @@ -334,7 +317,7 @@ def _stopWorker(self) -> None: self.receivingWorker.join() return - def _fileReceiverJob(self): + def _fileReceiverJob(self) -> None: """ method running in a separate thread; manages the received files """ @@ -342,7 +325,7 @@ def _fileReceiverJob(self): try: file = self._receiveFile(1) if file is not None: - if file["name"] not in self._files_to_skip: + if file.name not in self._files_to_skip: if self._keep_files_in_object: self.receivedFiles.append(file) diff --git a/thales_remote/script_wrapper.py b/thales_remote/script_wrapper.py index c1eadef..3f68ea9 100644 --- a/thales_remote/script_wrapper.py +++ b/thales_remote/script_wrapper.py @@ -4,7 +4,7 @@ / /_/ _ `/ _ \/ _ \/ -_) __/___/ -_) / -_) '_/ __/ __/ / '_/ /___/\_,_/_//_/_//_/\__/_/ \__/_/\__/_/\_\\__/_/ /_/_/\_\ -Copyright 2022 Zahner-Elektrik GmbH & Co. KG +Copyright 2023 Zahner-Elektrik GmbH & Co. KG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -30,8 +30,8 @@ import os from typing import Optional, Union, Any -from .error import ThalesRemoteError -from .connection import ThalesRemoteConnection +from thales_remote.error import ThalesRemoteError +from thales_remote.connection import ThalesRemoteConnection class PotentiostatMode(Enum): @@ -44,6 +44,78 @@ class PotentiostatMode(Enum): POTMODE_PSEUDOGALVANOSTATIC = 3 +class ScanStrategy(Enum): + """ + options for the EIS scan strategy. + + * SINGLE_SINE: single frequency sweep + * MULTI_SINE: multi sine + * TABLE: frequency table + """ + + SINGLE_SINE = 0 + MULTI_SINE = 1 + TABLE = 2 + + @classmethod + def stringToEnum(cls, string: str): + stringEnumMap = { + "single": ScanStrategy.SINGLE_SINE, + "multi": ScanStrategy.MULTI_SINE, + "table": ScanStrategy.TABLE, + } + if not string in stringEnumMap: + raise ValueError("invalid string: " + string) + return stringEnumMap.get(string) + + +class ScanDirection(Enum): + """ + set the scan direction for EIS measurements. + + * START_TO_MAX: from the start frequency to the maximum frequency + * START_TO_MIN: from the start to the minimum frequency + """ + + START_TO_MAX = 0 + START_TO_MIN = 1 + + @classmethod + def stringToEnum(cls, string: str): + stringEnumMap = { + "startToMax": ScanDirection.START_TO_MAX, + "startToMin": ScanDirection.START_TO_MIN, + } + if not string in stringEnumMap: + raise ValueError("invalid string: " + string) + return stringEnumMap.get(string) + + +class FileNaming(Enum): + """ + options for the file names in Thales. + + * DATE_TIME: naming with time stamp + * INDIVIDUAL: only the specified filename without extension + * COUNTER: consecutive number + """ + + DATE_TIME = 0 + INDIVIDUAL = 1 + COUNTER = 2 + + @classmethod + def stringToEnum(cls, string: str): + stringEnumMap = { + "dateTime": FileNaming.DATE_TIME, + "individual": FileNaming.INDIVIDUAL, + "counter": FileNaming.COUNTER, + } + if not string in stringEnumMap: + raise ValueError("invalid string: " + string) + return stringEnumMap.get(string) + + class ThalesRemoteScriptWrapper(object): """ Wrapper that uses the ThalesRemoteConnection class. @@ -51,7 +123,6 @@ class ThalesRemoteScriptWrapper(object): In the document you can also find a table with error numbers which are returned. :param remoteConnection: The connection object to the Thales software. - :type remoteConnection: :class:`~thales_remote.connection.ThalesRemoteConnection` """ undefindedStandardErrorString: str = "" @@ -135,9 +206,6 @@ def setShuntIndex(self, shunt: int) -> None: Fixes the shunt to the passed index. - TODO: - - handle errors / response strings - :param shunt: The number of the shunt. :returns: reponse string from the device """ @@ -172,6 +240,20 @@ def selectPotentiostat(self, device: int) -> str: """ return self.setValue("DEV%", device) + def selectPotentiostatWithoutPotentiostatStateChange(self, device: int) -> str: + """ + select device onto which all subsequent calls to set* methods are forwarded + + Device which is to be selected, on which the settings are output. + First, the device must be selected. + Only then can devices other than the internal main potentiostat be configured. + The potentiostat is not turned off. + + :param device: Number of the device. 0 = Main. 1 = EPC channel 1 and so on. + :returns: The response string from the device. + """ + return self.setValue("DEVHOT%", device) + def switchToSCPIControl(self) -> str: """ change away from operation as EPC device to SCPI operation @@ -185,6 +267,38 @@ def switchToSCPIControl(self) -> str: """ return self.executeRemoteCommand("SETUSB") + def switchToSCPIControlWithoutPotentiostatStateChange(self) -> str: + """Change away from operation as EPC device to SCPI operation. + + This command works only with external potentiostats of the latest generation XPOT2, PP2x2, EL1002. + This requires a device firmware with at least version 1.0.4. + After this command they are no longer accessible with the EPC interface. + Then you can connect to the potentiostat with USB via the Comports. + The change back to EPC operation is also done explicitly from the USB side. + + This function leaves the potentiostat in its current operating state and then switches to USB mode. + This should only be used when it is really necessary to leave the potentiostat on, + because between the change of control no quantities like current and voltage are monitored. + + To ensure that the switch between Thales and Python/SCPI is interference-free, the following procedure should be followed. + This is necessary to ensure that both Thales and Python/SCPI have calibrated offsets, otherwise jumps may occur when switching modes: + + 1. Connect Zennium with USB and EPC-device/power potentiostat (XPOT2, PP2x2, EL1002) with USB to the computer. As well as Zennium to power potentiostat by EPC cable. + 2. Switch on all devices. + 3. Allow the equipment to warm up for at least 30 minutes. + 4. Select and calibrate the EPC-device in Thales (with Remote2). + 5. Switching the EPC-device to SCPI mode via Remote2 command. + 6. Performing the offset calibration with Python/SCPI. + 7. Then it is possible to switch between Thales and Python/SCPI with the potentiostat switched on. + + With Thales, the DC operating point must first be set. + When changing the EPC device then measures current and voltage and sets the size internally. + When switching back to Thales, the same DC operating point must be set as when switching from Thales to USB. + + :returns: The response string from the device. + """ + return self.executeRemoteCommand("HOT2USB") + def getSerialNumber(self) -> str: """ get the serial number of the active device @@ -208,13 +322,6 @@ def getDeviceInformation(self) -> tuple[str, str]: match = re.search("(.*);(.*);(.*);([0-9]*)", reply) return match.group(3), match.group(4) - def getSetup(self) -> str: - """ - TODO: - - documentation - """ - return self.executeRemoteCommand("SENDSETUP") - def getDeviceName(self) -> str: """ get the name of the active device @@ -243,11 +350,7 @@ def enablePotentiostat(self, enabled: bool = True) -> str: :param enabled: Switches the potentiostat on if True and off otherwise :returns: response string from the device """ - if enabled: - reply = self.executeRemoteCommand("Pot=-1") - else: - reply = self.executeRemoteCommand("Pot=0") - return reply + return self.executeRemoteCommand("Pot=" + ("-1" if enabled else "0")) def disablePotentiostat(self) -> str: """ @@ -329,7 +432,6 @@ def enablePAD4(self, state: bool = True) -> str: :param state: If state = True PAD4 channels are enabled. :returns: reponse string from the device - :rtype: string """ return self.setValue("PAD4ENA", 1 if state else 0) @@ -346,9 +448,7 @@ def readPAD4Setup(self) -> str: read the currently set parameters Reading the set PAD4 configuration. - - TODO: - - document return value + A string containing the configuration is returned. :returns: reponse string from the device """ @@ -467,28 +567,22 @@ def setLowerNumberOfPeriods(self, periods: int) -> str: """ return self.setValue("Nwl", periods) - def setScanStrategy(self, strategy: str) -> str: + def setScanStrategy(self, strategy: Union[ScanStrategy, str]) -> str: """Set the scan strategy for EIS measurements. strategy = "single": single sine. strategy = "multi": multi sine. strategy = "table": frequency table. - TODO: - - change parameter `strategy` to an enum - :param strategy: the scan strategy to set for EIS measurements :returns: reponse string from the device """ - if strategy == "multi": - strategy = 1 - elif strategy == "table": - strategy = 2 - else: - strategy = 0 - return self.setValue("ScanStrategy", strategy) + strat = strategy + if isinstance(strategy, str): + strat = ScanStrategy.stringToEnum(strategy) + return self.setValue("ScanStrategy", strat.value) - def setScanDirection(self, direction: str) -> str: + def setScanDirection(self, direction: Union[ScanDirection, str]) -> str: """Set the scan direction for EIS measurements. direction = "startToMax": Scan at first from start to maximum frequency. @@ -497,13 +591,11 @@ def setScanDirection(self, direction: str) -> str: :param direction: The scan direction for EIS measurements. :type direction: string :returns: reponse string from the device - :rtype: string """ - if direction == "startToMin": - direction = 1 - else: - direction = 0 - return self.setValue("ScanDirection", direction) + dir = direction + if isinstance(direction, str): + dir = ScanDirection.stringToEnum(direction) + return self.setValue("ScanDirection", dir.value) def getImpedance( self, @@ -545,7 +637,7 @@ def getImpedance( match = re.search("impedance=\\s*(.*?),\\s*(.*?)$", reply) return complex(float(match.group(1)), float(match.group(2))) - def setEISNaming(self, naming: str) -> str: + def setEISNaming(self, naming: Union[str, FileNaming]) -> str: """ set the EIS measurement naming rule @@ -553,16 +645,13 @@ def setEISNaming(self, naming: str) -> str: naming = "counter": extend the :func:`~thales_remote.script_wrapper.ThalesRemoteScriptWrapper.setEISOutputFileName` with an sequential number. naming = "individual": the file is named like :func:`~thales_remote.script_wrapper.ThalesRemoteScriptWrapper.setEISOutputFileName`. - :param naming: the EIS measurement naming rule to set + :param naming: the EIS measurement naming rule to set. :returns: reponse string from the device """ - if naming == "dateTime": - naming = 0 - elif naming == "individual": - naming = 2 - else: - naming = 1 - return self.setValue("EIS_MOD", naming) + nameingType = naming + if isinstance(naming, str): + nameingType = FileNaming.stringToEnum(naming) + return self.setValue("EIS_MOD", nameingType.value) def setEISCounter(self, number: int) -> str: """ @@ -572,7 +661,6 @@ def setEISCounter(self, number: int) -> str: :param number: the next measurement number to set :returns: reponse string from the device - :rtype: string """ return self.setValue("EIS_NUM", number) @@ -771,7 +859,6 @@ def enableCVAutoRestartAtCurrentUnderflow(self, state: bool = True) -> str: :param state: If state = True the auto restart is enabled. :returns: reponse string from the device - :rtype: string """ return self.setValue("CV_AutoScale", 1 if state else 0) @@ -783,7 +870,7 @@ def disableCVAutoRestartAtCurrentUnderflow(self) -> str: """ return self.enableCVAutoRestartAtCurrentUnderflow(False) - def enableCVAnalogFunctionGenerator(self, state: bool = True): + def enableCVAnalogFunctionGenerator(self, state: bool = True) -> str: """ switch on the analog function generator (AFG) @@ -803,7 +890,7 @@ def disableCVAnalogFunctionGenerator(self) -> str: """ return self.enableCVAnalogFunctionGenerator(False) - def setCVNaming(self, naming: str) -> str: + def setCVNaming(self, naming: Union[str, FileNaming]) -> str: """Set the CV measurement naming rule. naming = "dateTime": extend the :func:`~thales_remote.script_wrapper.ThalesRemoteScriptWrapper.setCVOutputFileName` with date and time. @@ -813,13 +900,10 @@ def setCVNaming(self, naming: str) -> str: :param naming: CV measurement naming rule to set :returns: reponse string from the device """ - if naming == "dateTime": - naming = 0 - elif naming == "individual": - naming = 2 - else: - naming = 1 - return self.setValue("CV_MOD", naming) + nameingType = naming + if isinstance(naming, str): + nameingType = FileNaming.stringToEnum(naming) + return self.setValue("CV_MOD", nameingType.value) def setCVCounter(self, number: int) -> str: """ @@ -868,9 +952,6 @@ def checkCVSetup(self) -> str: With the error number the wrong parameter can be found. The error numbers are listed in the Remote2 manual. - TODO: - - document return value meaning - :returns: reponse string from the device """ reply = self.executeRemoteCommand("CHECKCV") @@ -886,9 +967,7 @@ def readCVSetup(self) -> str: read the set parameters After checking with checkCVSetup() the parameters can be read back from the workstation. - - TODO: - - document return value meaning + This command returns a list of all set parameters. :returns: reponse string from the device """ @@ -908,9 +987,6 @@ def measureCV(self) -> str: Before starting the measurement, all parameters must be checked with :func:`~thales_remote.script_wrapper.ThalesRemoteScriptWrapper.checkCVSetup`. - TODO: - - document return value meaning - :returns: reponse string from the device """ reply = self.executeRemoteCommand("CV") @@ -924,43 +1000,39 @@ def measureCV(self) -> str: # Section with settings for IE measurements. # Additional informations can be found in the IE manual. - def setIEFirstEdgePotential(self, potential): + def setIEFirstEdgePotential(self, potential: float) -> str: """Set the first edge potential. :param potential: The potential of the first edge in V. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_EckPot1", potential) - def setIESecondEdgePotential(self, potential): + def setIESecondEdgePotential(self, potential: float) -> str: """Set the second edge potential. :param potential: The potential of the second edge in V. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_EckPot2", potential) - def setIEThirdEdgePotential(self, potential): + def setIEThirdEdgePotential(self, potential: float) -> str: """Set the third edge potential. :param potential: The potential of the third edge in V. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_EckPot3", potential) - def setIEFourthEdgePotential(self, potential): + def setIEFourthEdgePotential(self, potential: float) -> str: """Set the fourth edge potential. :param potential: The potential of the fourth edge in V. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_EckPot4", potential) - def setIEFirstEdgePotentialRelation(self, relation): + def setIEFirstEdgePotentialRelation(self, relation: float) -> str: """Set the relation of the first edge potential. relation = "absolute": Absolute relation of the potential. @@ -968,7 +1040,6 @@ def setIEFirstEdgePotentialRelation(self, relation): :param relation: The relation of the edge potential absolute or relative. :returns: reponse string from the device - :rtype: string """ if relation == "relative": relation = -1 @@ -976,7 +1047,7 @@ def setIEFirstEdgePotentialRelation(self, relation): relation = 0 return self.setValue("IE_EckPot1rel", relation) - def setIESecondEdgePotentialRelation(self, relation): + def setIESecondEdgePotentialRelation(self, relation) -> str: """Set the relation of the second edge potential. relation = "absolute": Absolute relation of the potential. @@ -984,7 +1055,6 @@ def setIESecondEdgePotentialRelation(self, relation): :param relation: The relation of the edge potential absolute or relative. :returns: reponse string from the device - :rtype: string """ if relation == "relative": relation = -1 @@ -992,7 +1062,7 @@ def setIESecondEdgePotentialRelation(self, relation): relation = 0 return self.setValue("IE_EckPot2rel", relation) - def setIEThirdEdgePotentialRelation(self, relation): + def setIEThirdEdgePotentialRelation(self, relation) -> str: """Set the relation of the third edge potential. relation = "absolute": Absolute relation of the potential. @@ -1000,7 +1070,6 @@ def setIEThirdEdgePotentialRelation(self, relation): :param relation: The relation of the edge potential absolute or relative. :returns: reponse string from the device - :rtype: string """ if relation == "relative": relation = -1 @@ -1008,7 +1077,7 @@ def setIEThirdEdgePotentialRelation(self, relation): relation = 0 return self.setValue("IE_EckPot3rel", relation) - def setIEFourthEdgePotentialRelation(self, relation): + def setIEFourthEdgePotentialRelation(self, relation) -> str: """Set the relation of the fourth edge potential. relation = "absolute": Absolute relation of the potential. @@ -1016,7 +1085,6 @@ def setIEFourthEdgePotentialRelation(self, relation): :param relation: The relation of the edge potential absolute or relative. :returns: reponse string from the device - :rtype: string """ if relation == "relative": relation = -1 @@ -1024,18 +1092,17 @@ def setIEFourthEdgePotentialRelation(self, relation): relation = 0 return self.setValue("IE_EckPot4rel", relation) - def setIEPotentialResolution(self, resolution): + def setIEPotentialResolution(self, resolution: float) -> str: """Set the potential resolution. The potential step size for IE measurements in V. :param relation: The resolution for the measurement in V. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_Resolution", resolution) - def setIEMinimumWaitingTime(self, time): + def setIEMinimumWaitingTime(self, time: float) -> str: """Set the minimum waiting time. The minimum waiting time on each step of the IE measurement. @@ -1043,11 +1110,10 @@ def setIEMinimumWaitingTime(self, time): :param time: The waiting time in seconds. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_WZmin", time) - def setIEMaximumWaitingTime(self, time): + def setIEMaximumWaitingTime(self, time: float) -> str: """Set the maximum waiting time. The maximum waiting time on each step of the IE measurement. @@ -1056,11 +1122,10 @@ def setIEMaximumWaitingTime(self, time): :param time: The waiting time in seconds. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_WZmax", time) - def setIERelativeTolerance(self, tolerance): + def setIERelativeTolerance(self, tolerance: float) -> str: """Set the relative tolerance criteria. This parameter is only used in sweep mode steady state. @@ -1069,11 +1134,10 @@ def setIERelativeTolerance(self, tolerance): :param tolerance: The tolerance to wait until break, 0.01 = 1%. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_Torel", tolerance) - def setIEAbsoluteTolerance(self, tolerance): + def setIEAbsoluteTolerance(self, tolerance: float) -> str: """Set the absolute tolerance criteria. This parameter is only used in sweep mode steady state. @@ -1082,20 +1146,18 @@ def setIEAbsoluteTolerance(self, tolerance): :param tolerance: The tolerance to wait until break, 0.01 = 1%. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_Toabs", tolerance) - def setIEOhmicDrop(self, ohmicdrop): + def setIEOhmicDrop(self, ohmicdrop: float) -> str: """Set the ohmic drop for IE measurement. :param ohmicdrop: The ohmic drop for measurement. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_Odrop", ohmicdrop) - def setIESweepMode(self, mode): + def setIESweepMode(self, mode) -> str: """Set the sweep mode. The explanation of the modes can be found in the IE manual. @@ -1105,7 +1167,6 @@ def setIESweepMode(self, mode): :param mode: The sweep mode for measurement. :returns: reponse string from the device - :rtype: string """ if mode == "steady state": mode = 0 @@ -1115,7 +1176,7 @@ def setIESweepMode(self, mode): mode = 1 return self.setValue("IE_SweepMode", mode) - def setIEScanRate(self, scanRate): + def setIEScanRate(self, scanRate: float) -> str: """Set the scan rate. This parameter is only used in sweep mode dynamic scan. @@ -1123,33 +1184,30 @@ def setIEScanRate(self, scanRate): :param scanRate: The scan rate in V/s. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_Srate", scanRate) - def setIEMaximumCurrent(self, current): + def setIEMaximumCurrent(self, current: float) -> str: """Set the maximum current. The maximum positive current at which the measurement is interrupted. :param current: The maximum current for measurement in A. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_Ima", current) - def setIEMinimumCurrent(self, current): + def setIEMinimumCurrent(self, current: float) -> str: """Set the minimum current. The maximum negative current at which the measurement is interrupted. :param current: The minimum current for measurement in A. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_Imi", current) - def setIENaming(self, naming): + def setIENaming(self, naming: Union[str, FileNaming]) -> str: """Set the IE measurement naming rule. naming = "dateTime": extend the :func:`~thales_remote.script_wrapper.ThalesRemoteScriptWrapper.setIEOutputFileName` with date and time. @@ -1159,28 +1217,23 @@ def setIENaming(self, naming): :param naming: The IE measurement naming rule. :type naming: string :returns: reponse string from the device - :rtype: string """ - if naming == "dateTime": - naming = 0 - elif naming == "individual": - naming = 2 - else: - naming = 1 - return self.setValue("IE_MOD", naming) + nameingType = naming + if isinstance(naming, str): + nameingType = FileNaming.stringToEnum(naming) + return self.setValue("IE_MOD", nameingType.value) - def setIECounter(self, number): + def setIECounter(self, number: int) -> str: """Set the current number of IE measurement for filename. Current number for the file name for EIS measurements which is used next and then incremented. :param number: The next measurement number. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_NUM", number) - def setIEOutputPath(self, path): + def setIEOutputPath(self, path: str) -> str: """Set the path where the IE measurements should be stored. The results must be stored on the C hard disk. If an error occurs test an alternative path or c:\THALES\temp. @@ -1188,12 +1241,11 @@ def setIEOutputPath(self, path): :param path: The path to the directory. :returns: reponse string from the device - :rtype: string """ path = path.lower() # c: has to be lower case return self.setValue("IE_PATH", path) - def setIEOutputFileName(self, name): + def setIEOutputFileName(self, name: str) -> str: """Set the basic IE output filename. The basic name of the file, which is extended by a sequential number or the date and time. @@ -1204,18 +1256,16 @@ def setIEOutputFileName(self, name): :param name: The basic name of the file. :returns: reponse string from the device - :rtype: string """ return self.setValue("IE_ROOT", name) - def checkIESetup(self): + def checkIESetup(self) -> str: """Check the set parameters. With the error number the wrong parameter can be found. The error numbers are listed in the Remote2 manual. :returns: reponse string from the device - :rtype: string """ reply = self.executeRemoteCommand("CHECKIE") if reply.find("ERROR") >= 0: @@ -1225,13 +1275,12 @@ def checkIESetup(self): ) return reply - def readIESetup(self): + def readIESetup(self) -> str: """Read the set parameters. After checking with :func:`~thales_remote.script_wrapper.ThalesRemoteScriptWrapper.checkIESetup` the parameters can be read back from the workstation. :returns: reponse string from the device - :rtype: string """ reply = self.executeRemoteCommand("SENDIESETUP") if reply.find("ERROR") >= 0: @@ -1241,13 +1290,12 @@ def readIESetup(self): ) return reply - def measureIE(self): + def measureIE(self) -> str: """Measure IE. Before measurement, all parameters must be checked with :func:`~thales_remote.script_wrapper.ThalesRemoteScriptWrapper.checkIESetup`. :returns: reponse string from the device - :rtype: string """ reply = self.executeRemoteCommand("IE") if reply.find("ERROR") >= 0: @@ -1264,7 +1312,7 @@ def measureIE(self): For instructions on how the sequencer file is structured, please refer to the manual of the sequencer. """ - def selectSequence(self, number): + def selectSequence(self, number: int) -> str: """Select the sequence to run with runSequence(). The sequences must be stored under "C:\THALES\script\sequencer\sequences". @@ -1273,7 +1321,6 @@ def selectSequence(self, number): :param number: The number of the sequence. :returns: reponse string from the device - :rtype: string """ reply = self.executeRemoteCommand("SELSEQ=" + str(number)) if reply != "SELOK\r": @@ -1282,7 +1329,7 @@ def selectSequence(self, number): ) return reply - def setSequenceNaming(self, naming): + def setSequenceNaming(self, naming: Union[str, FileNaming]) -> str: """Set the sequence measurement naming rule. naming = "dateTime": extend the setSequenceOutputFileName(name) with date and time. @@ -1292,28 +1339,23 @@ def setSequenceNaming(self, naming): :param naming: The naming rule. :type naming: string :returns: reponse string from the device - :rtype: string """ - if naming == "dateTime": - naming = 0 - elif naming == "individual": - naming = 2 - else: - naming = 1 - return self.setValue("SEQ_MOD", naming) + nameingType = naming + if isinstance(naming, str): + nameingType = FileNaming.stringToEnum(naming) + return self.setValue("SEQ_MOD", nameingType.value) - def setSequenceCounter(self, number): + def setSequenceCounter(self, number: int) -> str: """Set the current number of sequence measurement for filename. Current number for the file name for EIS measurements which is used next and then incremented. :param number: The next measurement number :returns: reponse string from the device - :rtype: string """ return self.setValue("SEQ_NUM", number) - def setSequenceOutputPath(self, path): + def setSequenceOutputPath(self, path: str) -> str: """Set the path where the sequence measurements should be stored. The results must be stored on the C hard disk. If an error occurs test an alternative path or c:\THALES\temp. @@ -1321,12 +1363,11 @@ def setSequenceOutputPath(self, path): :param path: The path to the directory. :returns: reponse string from the device - :rtype: string """ path = path.lower() # c: has to be lower case return self.setValue("SEQ_PATH", path) - def setSequenceOutputFileName(self, name): + def setSequenceOutputFileName(self, name: str) -> str: """Set the basic sequence output filename. The basic name of the file, which is extended by a sequential number or the date and time. @@ -1337,17 +1378,15 @@ def setSequenceOutputFileName(self, name): :param name: The basic name of the file. :returns: reponse string from the device - :rtype: string """ return self.setValue("SEQ_ROOT", name) - def runSequence(self): + def runSequence(self) -> str: """Run the selected sequence. This command executes the selected sequence between 0 and 9. :returns: reponse string from the device - :rtype: string """ reply = self.executeRemoteCommand("DOSEQ") if reply != "SEQ DONE\r": @@ -1357,7 +1396,7 @@ def runSequence(self): ) return reply - def enableFraMode(self, state=True): + def enableFraMode(self, state: bool = True) -> str: """Enables the use of the FRA probe. With the FRA Probe, external power potentiostats, signal generators, sources, sinks can be @@ -1375,7 +1414,6 @@ def enableFraMode(self, state=True): :param state: If state = True FRA mode is enabled. :returns: reponse string from the device - :rtype: string """ if state == True: state = 1 @@ -1383,114 +1421,105 @@ def enableFraMode(self, state=True): state = 0 return self.setValue("FRA", state) - def disableFraMode(self): + def disableFraMode(self) -> str: return self.enableFraMode(False) - def setFraVoltageInputGain(self, value): + def setFraVoltageInputGain(self, value: float) -> str: """Sets the input voltage gain. :param value: The value to set. :returns: reponse string from the device - :rtype: string """ return self.setValue("FRA_POT_IN", value) - def setFraVoltageOutputGain(self, value): + def setFraVoltageOutputGain(self, value: float) -> str: """Sets the output voltage gain. :param value: The value to set. :returns: reponse string from the device - :rtype: string """ return self.setValue("FRA_POT_OUT", value) - def setFraVoltageMinimum(self, value): + def setFraVoltageMinimum(self, value: float) -> str: """Sets the minimum voltage. Sets the minimum voltage of the FRA device. :param value: The value to set. :returns: reponse string from the device - :rtype: string """ return self.setValue("FRA_POT_MIN", value) - def setFraVoltageMaximum(self, value): + def setFraVoltageMaximum(self, value: float) -> str: """Sets the maximum voltage. Sets the maximum voltage of the FRA device. :param value: The value to set. :returns: reponse string from the device - :rtype: string """ return self.setValue("FRA_POT_MAX", value) - def setFraCurrentInputGain(self, value): + def setFraCurrentInputGain(self, value: float) -> str: """Sets the input current gain. :param value: The value to set. :returns: reponse string from the device - :rtype: string """ return self.setValue("FRA_CUR_IN", value) - def setFraCurrentOutputGain(self, value): + def setFraCurrentOutputGain(self, value: float) -> str: """Sets the output current gain. :param value: The value to set. :returns: reponse string from the device - :rtype: string """ return self.setValue("FRA_CUR_OUT", value) - def setFraCurrentMinimum(self, value): + def setFraCurrentMinimum(self, value: float) -> str: """Sets the minimum current. Sets the minimum current of the FRA device. :param value: The value to set. :returns: reponse string from the device - :rtype: string """ return self.setValue("FRA_CUR_MIN", value) - def setFraCurrentMaximum(self, value): + def setFraCurrentMaximum(self, value: float) -> str: """Sets the maximum current. Sets the maximum current of the FRA device. :param value: The value to set. :returns: reponse string from the device - :rtype: string """ return self.setValue("FRA_CUR_MAX", value) - def setFraPotentiostatMode(self, potentiostatMode: PotentiostatMode): + def setFraPotentiostatMode(self, potentiostatMode: PotentiostatMode) -> str: """Set the coupling of the FRA mode. - This can be PotentiostatMode.POTMODE_POTENTIOSTATIC or PotentiostatMode.POTMODE_GALVANOSTATIC or - PotentiostatMode.POTMODE_PSEUDOGALVANOSTATIC. + This can be PotentiostatMode.POTMODE_POTENTIOSTATIC or PotentiostatMode.POTMODE_GALVANOSTATIC :param potentiostatMode: The coupling of the FRA mode - :type potentiostatMode: :class:`~thales_remote.script_wrapper.PotentiostatMode` :returns: reponse string from the device - :rtype: string """ if potentiostatMode == PotentiostatMode.POTMODE_POTENTIOSTATIC: command = "FRAGAL=0" elif potentiostatMode == PotentiostatMode.POTMODE_GALVANOSTATIC: command = "FRAGAL=1" else: - return "" + return ValueError( + "PotentiostatMode.POTMODE_POTENTIOSTATIC or PotentiostatMode.POTMODE_GALVANOSTATIC" + ) return self.executeRemoteCommand(command) def runSequenceFile( self, - filepath, - sequence_folder="C:/THALES/script/sequencer/sequences", - sequence_number=9, - ): + filepath: str, + sequence_folder: str = "C:/THALES/script/sequencer/sequences", + sequence_number: int = 9, + ) -> str: """Run the sequence at filepath. The file from the specified path is copied as sequence sequence_number=9 to the correct location in the Thales directory and then selected and executed. @@ -1502,15 +1531,11 @@ def runSequenceFile( In this case the path to the sequences folder in sequence_folder must be set to "C:/THALES/script/sequencer/sequences" on the computer with the zennium. :param filepath: Filepath of the sequence. - :type filepath: string :param sequence_folder: The filepath to the THALES sequence folder. Does not normally need to be transferred and changed. Explanation see in the text before. - :type sequence_folder: string :param sequence_number: The number in the THALES sequence directory. Does not normally need to be transferred and changed. - :type sequence_number: int :returns: reponse string from the device - :rtype: string """ if sequence_number > 9 or sequence_number < 0: raise ThalesRemoteError("Wrong sequence number.") @@ -1547,18 +1572,17 @@ def setValue(self, name: str, value: Union[int, float, str, Any]) -> str: ) return reply - def executeRemoteCommand(self, command): + def executeRemoteCommand(self, command: str) -> str: """Directly execute a query to Remote Script. :param command: The command query string, e.g. "IMPEDANCE" or "Pset=0". :returns: reponse string from the device - :rtype: string """ return self._remote_connection.sendStringAndWaitForReplyString( "1:" + command + ":", 2 ) - def forceThalesIntoRemoteScript(self): + def forceThalesIntoRemoteScript(self) -> str: """Prompts Thales to start the Remote Script. Will switch a running Thales from anywhere like the main menu after @@ -1572,7 +1596,6 @@ def forceThalesIntoRemoteScript(self): probably be a save bet. :returns: reponse string from the device - :rtype: string """ self._remote_connection.sendStringAndWaitForReplyString( f"3,{self._remote_connection.getConnectionName()},0,OFF", 128 @@ -1581,7 +1604,7 @@ def forceThalesIntoRemoteScript(self): f"2,{self._remote_connection.getConnectionName()}", 128 ) - def getWorkstationHeartBeat(self, timeout=None): + def getWorkstationHeartBeat(self, timeout: Optional[float] = None) -> float: """Query the heartbeat time from the Term software for the workstation and the Thales software accordingly. The complete documentation can be found in the `DevCli-Manual `_ Page 8. @@ -1597,9 +1620,9 @@ def getWorkstationHeartBeat(self, timeout=None): retval = self._remote_connection.sendStringAndWaitForReplyString( f"1,{self._remote_connection.getConnectionName()}", 128, timeout ) - return retval.split(",")[2] + return float(retval.split(",")[2]) - def getSerialNumberFromTerm(self): + def getSerialNumberFromTerm(self) -> str: """Get the serial number of the workstation via the Term software. The serial number of the active potentiostat with EPC devices can be read with the @@ -1607,14 +1630,13 @@ def getSerialNumberFromTerm(self): This function returns only the serial number of the workstation, which is determined by the Term software. :returns: The workstation serial number. - :rtype: string """ retval = self._remote_connection.sendStringAndWaitForReplyString( f"3,{self._remote_connection.getConnectionName()},6", 128 ) return retval.split(",")[2] - def getTermIsActive(self, timeout=2): + def getTermIsActive(self, timeout: float = 2) -> bool: """Check if the Term still responds to requests. Whether the term is still active is checked by sending a heartbeat command. @@ -1629,7 +1651,6 @@ def getTermIsActive(self, timeout=2): :param timeout: Time in seconds in which the term must provide the answer, default 2 seconds. :returns: True or False if the Term is Active. - :rtype: bool """ active = True try: @@ -1645,7 +1666,7 @@ def getTermIsActive(self, timeout=2): They are marked with the prefix '_' after the Python convention for proteced. """ - def _requestValueAndParseUsingRegexp(self, command, pattern): + def _requestValueAndParseUsingRegexp(self, command: str, pattern: str) -> float: reply = self.executeRemoteCommand(command) if reply.find("ERROR") >= 0: raise ThalesRemoteError(