diff --git a/README.md b/README.md index cb71594..26b81c2 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,7 @@ This package offers the serial interface for communication with an EIT device fr If you have any ideas or other suggestions, please don't hesitate to contact me. Email: jacob.thoenes@uni-rostock.de + +___ + +- FTDI Driver installation: https://www.ftdichip.com/old2020/Drivers/D2XX.htm \ No newline at end of file diff --git a/citation.cff b/citation.cff index dce4e54..4a35a5f 100644 --- a/citation.cff +++ b/citation.cff @@ -13,6 +13,11 @@ authors: email: jacob.thoenes@uni-rostock.de affiliation: Universität Rostock orcid: 'https://orcid.org/0000-0003-2826-5281' + - given-names: Patricia + family-names: Fuchs + email: pat.fuchs@uni-rostock.de + affiliation: Universität Rostock + orcid: 'https://orcid.org/0009-0006-4647-633X - given-names: Oveys family-names: Javanmardtilaki email: oveys@javanmardtilaki@uni-rostock.de @@ -23,6 +28,6 @@ keywords: - EIT - Sciospec license: MIT -version: 0.8.0 +version: 0.9.0 doi: 10.5281/zenodo.7766305 date-released: '2024-11-14' diff --git a/docs/conf.py b/docs/conf.py index c4611c7..c9abb4c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,7 +7,7 @@ project = "sciopy" author = "Jacob P. Thönes" -release = "0.8.2.2" +release = "0.8.2.3" extensions = [ "sphinx.ext.autodoc", diff --git a/examples/EIT-16-256-MessageParser.ipynb b/examples/EIT-16-256-MessageParser.ipynb new file mode 100644 index 0000000..ac9e9f3 --- /dev/null +++ b/examples/EIT-16-256-MessageParser.ipynb @@ -0,0 +1,300 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": "# Example code for connecting a Sciospec EIT device", + "id": "8e6ea1591f787a86" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "This script gives an example how to use this updated version of sciopy. \n", + " \n", + "Central new feature is an automated USB messaage parser, that parses incoming messages immediately and upon request saves data frames while running. For burst measurements AND continuous measurements with burst_count = 0" + ], + "id": "e443e180ad6c1dfd" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# Initialization\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import time\n", + "from sciopy.sciopy import EIT_16_32_64_128, EitMeasurementSetup, available_serial_ports\n", + "from sciopy.sciopy import make_results_folder\n", + "\n", + "# create a 'sciospec' EIT device\n", + "n_el = 16\n", + "sciospec = EIT_16_32_64_128(n_el)\n", + "# connect device via USB-FS port\n", + "print(available_serial_ports())\n", + "sciospec.connect_device_FS(\"COM3\")\n", + "savepath = \"../../data/\"" + ], + "id": "91983d17281c7ff1" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# read system message buffer\n", + "sciospec.SystemMessageCallback()" + ], + "id": "1a8832c9510b72c9" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# create a measurement setup\n", + "setup = EitMeasurementSetup(\n", + " burst_count=4,\n", + " n_el=n_el,\n", + " exc_freq=125_000,\n", + " framerate=3,\n", + " amplitude=0.01,\n", + " inj_skip=n_el // 2,\n", + " gain=1,\n", + " adc_range=1,\n", + ")\n", + "sciospec.SetMeasurementSetup(setup)\n", + "# look inside the docstring of the function and manual\n", + "sciospec.GetMeasurementSetup(2)" + ], + "id": "c1dbc04960e403ab" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Usage of the \"old\" measure data command \n", + "\n", + "Here, the message parser is not utilized, technically faster processing of incoming messages. Messages are solely appended and processed into EITFrames and bursts" + ], + "id": "890b0096ca6b8bb" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "data = sciospec.StartStopMeasurementFast(return_as=\"hex\")\n", + "print(data.shape)" + ], + "id": "initial_id" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "### New measure-data function\n" + ], + "id": "3301812b2d53b956" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# 1) With burstcount\n", + "sciospec.update_BurstCount(3)\n", + "data = sciospec.StartStopMeasurement(\n", + " return_as=\"eitframe\",\n", + " bSaveData=False,\n", + " bDeleteData=False,\n", + " sSavePath=savepath,\n", + " bResultsFolder=False,\n", + ")\n", + "print(data.shape)\n", + "\n", + "# 2) Continuous measurement\n", + "sciospec.update_BurstCount(0)\n", + "data = sciospec.StartStopMeasurement(\n", + " timeout=5,\n", + " return_as=\"eitframe\",\n", + " bSaveData=False,\n", + " bDeleteData=False,\n", + " sSavePath=savepath,\n", + " bResultsFolder=False,\n", + ") # measured for five seconds\n", + "print(data.shape)\n", + "\n", + "\n", + "# 3) Save data in real time and create an additional folder for storage\n", + "data = sciospec.StartStopMeasurement(\n", + " timeout=5,\n", + " return_as=\"eitframe\",\n", + " bSaveData=True,\n", + " bDeleteData=False,\n", + " sSavePath=savepath,\n", + " bResultsFolder=True,\n", + ") # measured for five seconds\n", + "print(data.shape)\n", + "\n", + "\n", + "# 4) For continuous saving results in the same folder, create a folder before and then pass it along\n", + "sCurrPath = make_results_folder(\n", + " bCreateResultsFolder=True, bSaveData=True, sSavePath=savepath\n", + ")\n", + "sciospec.StartStopMeasurement(\n", + " timeout=5,\n", + " return_as=\"eitframe\",\n", + " bSaveData=True,\n", + " bDeleteData=True,\n", + " sSavePath=sCurrPath,\n", + " bResultsFolder=False,\n", + ") # measured for five seconds\n", + "time.sleep(3)\n", + "sciospec.StartStopMeasurement(\n", + " timeout=5,\n", + " return_as=\"eitframe\",\n", + " bSaveData=True,\n", + " bDeleteData=True,\n", + " sSavePath=sCurrPath,\n", + " bResultsFolder=False,\n", + ") # measured for five seconds\n", + "time.sleep(3)\n", + "sciospec.StartStopMeasurement(\n", + " timeout=5,\n", + " return_as=\"eitframe\",\n", + " bSaveData=True,\n", + " bDeleteData=True,\n", + " sSavePath=sCurrPath,\n", + " bResultsFolder=False,\n", + ") # measured for five seconds\n", + "\n", + "\n", + "# Arguments:\n", + "# bDeleteData: Data is not returned but if bSaveData=True saved, and is deleted from an internal buffer. For False, data is returned according to return_as\n", + "# bSaveData: if Data should be saved in-time with the measurements. Data is saved at sSavePath," + ], + "id": "143a513757acfe71" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Example on how to change the measurement mode with boundary conditions is updated\n", + "\n", + "Recommended is for every change of the measurement mode\n", + "1. Restart Device\n", + "2. Set new measurement mode" + ], + "id": "e48b23b1ed247a39" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# Standard setting \"singleended\" -> potential measurement\n", + "sciospec.GetMeasurementSetup(2)\n", + "sciospec.update_BurstCount(3)\n", + "data = sciospec.StartStopMeasurement(\n", + " return_as=\"eitframe\",\n", + " bSaveData=False,\n", + " bDeleteData=False,\n", + " sSavePath=savepath,\n", + " bResultsFolder=False,\n", + ")\n", + "data_pot = np.abs(data[2])" + ], + "id": "5db0b9e7255bf10b" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# For updated setting, restart device\n", + "sciospec.SoftwareReset()\n", + "sciospec = EIT_16_32_64_128(16)\n", + "print(available_serial_ports())\n", + "sciospec.connect_device_FS(\"COM3\")\n", + "sciospec.SetMeasurementSetup(setup)\n", + "sciospec.update_measurement_mode(\"skip4\", \"internal\")\n", + "sciospec.GetMeasurementSetup(2)\n", + "# look inside the docstring of the function and manual\n", + "\n", + "sciospec.update_BurstCount(3)\n", + "data = sciospec.StartStopMeasurement(\n", + " return_as=\"eitframe\",\n", + " bSaveData=False,\n", + " bDeleteData=False,\n", + " sSavePath=savepath,\n", + " bResultsFolder=False,\n", + ")\n", + "data_skip4 = np.abs(data[2])\n", + "\n", + "\n", + "fig, ax = plt.subplots(ncols=2)\n", + "ax[0].imshow(data_pot, cmap=\"viridis\")\n", + "ax[0].set_title(\"Singleended\")\n", + "ax[1].imshow(data_skip4, cmap=\"viridis\")\n", + "ax[1].set_title(\"Skip4\")\n", + "plt.tight_layout()\n", + "plt.show()" + ], + "id": "72e8b4e384c52b7" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# Alternatively,\n", + "setup = EitMeasurementSetup(\n", + " burst_count=4,\n", + " n_el=n_el,\n", + " exc_freq=125_000,\n", + " framerate=3,\n", + " amplitude=0.01,\n", + " inj_skip=n_el // 2,\n", + " gain=1,\n", + " adc_range=1,\n", + " mea_mode=\"skip2\",\n", + " mea_mode_boundary=\"external\",\n", + ")\n", + "sciospec.SetMeasurementSetup(setup)" + ], + "id": "a026d514146eb1f0" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/ISX-3v2.ipynb b/examples/ISX-3v2.ipynb new file mode 100644 index 0000000..35c8a10 --- /dev/null +++ b/examples/ISX-3v2.ipynb @@ -0,0 +1,153 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2727ee29", + "metadata": {}, + "source": [ + "# Example code for connecting the Sciospec ISX-3 device" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "18bd09f0", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "from sciopy import ISX_3, EisMeasurementSetup" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "88778348", + "metadata": {}, + "outputs": [], + "source": [ + "isx = ISX_3()" + ] + }, + { + "cell_type": "markdown", + "id": "ec641683", + "metadata": {}, + "source": [ + "### WIP" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c21b7642", + "metadata": {}, + "outputs": [ + { + "ename": "SerialException", + "evalue": "[Errno 2] could not open port COM1: [Errno 2] No such file or directory: 'COM1'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", + "File \u001b[0;32m~/miniconda3/lib/python3.12/site-packages/serial/serialposix.py:322\u001b[0m, in \u001b[0;36mSerial.open\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 321\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 322\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mfd \u001b[38;5;241m=\u001b[39m \u001b[43mos\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mopen\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mportstr\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mos\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mO_RDWR\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m|\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mos\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mO_NOCTTY\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m|\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mos\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mO_NONBLOCK\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 323\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mOSError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m msg:\n", + "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'COM1'", + "\nDuring handling of the above exception, another exception occurred:\n", + "\u001b[0;31mSerialException\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[3], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# connect to the device (adjust the port as necessary)\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43misx\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconnect_device_USB2\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mCOM1\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Schreibtisch/Uni/Forschung/PyPI_Packages/sciopy/sciopy/ISX_3.py:63\u001b[0m, in \u001b[0;36mISX_3.connect_device_USB2\u001b[0;34m(self, port, baudrate, timeout)\u001b[0m\n\u001b[1;32m 61\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 62\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mserial_protocol \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mUSB-FS\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m---> 63\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdevice \u001b[38;5;241m=\u001b[39m \u001b[43mserial\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mSerial\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 64\u001b[0m \u001b[43m \u001b[49m\u001b[43mport\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mport\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 65\u001b[0m \u001b[43m \u001b[49m\u001b[43mbaudrate\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbaudrate\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 66\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtimeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 67\u001b[0m \u001b[43m \u001b[49m\u001b[43mparity\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mserial\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mPARITY_NONE\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 68\u001b[0m \u001b[43m \u001b[49m\u001b[43mstopbits\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mserial\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mSTOPBITS_ONE\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 69\u001b[0m \u001b[43m \u001b[49m\u001b[43mbytesize\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mserial\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mEIGHTBITS\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 70\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 71\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mConnection to\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdevice\u001b[38;5;241m.\u001b[39mname, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mis established.\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "File \u001b[0;32m~/miniconda3/lib/python3.12/site-packages/serial/serialutil.py:244\u001b[0m, in \u001b[0;36mSerialBase.__init__\u001b[0;34m(self, port, baudrate, bytesize, parity, stopbits, timeout, xonxoff, rtscts, write_timeout, dsrdtr, inter_byte_timeout, exclusive, **kwargs)\u001b[0m\n\u001b[1;32m 241\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124munexpected keyword arguments: \u001b[39m\u001b[38;5;132;01m{!r}\u001b[39;00m\u001b[38;5;124m'\u001b[39m\u001b[38;5;241m.\u001b[39mformat(kwargs))\n\u001b[1;32m 243\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m port \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m--> 244\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mopen\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/lib/python3.12/site-packages/serial/serialposix.py:325\u001b[0m, in \u001b[0;36mSerial.open\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 323\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mOSError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m msg:\n\u001b[1;32m 324\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mfd \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m--> 325\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m SerialException(msg\u001b[38;5;241m.\u001b[39merrno, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcould not open port \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m: \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_port, msg))\n\u001b[1;32m 326\u001b[0m \u001b[38;5;66;03m#~ fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # set blocking\u001b[39;00m\n\u001b[1;32m 328\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpipe_abort_read_r, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpipe_abort_read_w \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m, \u001b[38;5;28;01mNone\u001b[39;00m\n", + "\u001b[0;31mSerialException\u001b[0m: [Errno 2] could not open port COM1: [Errno 2] No such file or directory: 'COM1'" + ] + } + ], + "source": [ + "# connect to the device (adjust the port as necessary)\n", + "isx.connect_device_USB2(\"COM1\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa54e5a6", + "metadata": {}, + "outputs": [], + "source": [ + "# reset the system (clear previous settings)\n", + "isx.ResetSystem()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "28c54b12", + "metadata": {}, + "outputs": [], + "source": [ + "setup = EisMeasurementSetup(\n", + " start=10,\n", + " stop=100000,\n", + " step=20,\n", + " stepmode=\"log\",\n", + " avg=1,\n", + " amplitude=100,\n", + " precision=1,\n", + " measurement_time=1,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8cf6f6b", + "metadata": {}, + "outputs": [], + "source": [ + "isx.SetMeasurementSetup(setup)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2a8c956e", + "metadata": {}, + "outputs": [], + "source": [ + "# disconnect device from serial port\n", + "isx.disconnect_device_USB2()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1cf1c447", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "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.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index fc4d0f7..0c47f9b 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -5,7 +5,6 @@ except ImportError: print("Could not import module: serial") - from .com_util import ( clTbt_dp, clTbt_sp, @@ -17,7 +16,6 @@ import numpy as np from pyftdi.ftdi import Ftdi - msg_dict = { "0x01": "No message inside the message buffer", "0x02": "Timeout: Communication-timeout (less data than expected)", @@ -31,6 +29,12 @@ } from .sciopy_dataclasses import EitMeasurementSetup +from sciopydev.sciopy.usb_message_parser import ( + MessageParser, + make_eitframes_hex, + get_data_as_matrix, + make_results_folder, +) class EIT_16_32_64_128: @@ -48,10 +52,11 @@ def __init__(self, n_el: int) -> None: number of electrodes used for measurement. """ self.n_el = n_el - self.channel_group = self.init_channel_group() self.print_msg = True self.ret_hex_int = None + self.cMessageParser = None + self.setup = None def init_channel_group(self): """ @@ -108,6 +113,7 @@ def connect_device_HS(self, url: str = "ftdi://ftdi:232h/1", baudrate: int = 900 serial.STOP_BIT_1 serial.set_baudrate(baudrate) self.device = serial + self.cMessageParser = MessageParser(self.device, devicetype="HS") def connect_device_FS(self, port: str, baudrate: int = 9600, timeout: int = 1): """ @@ -140,6 +146,7 @@ def connect_device_FS(self, port: str, baudrate: int = 9600, timeout: int = 1): ) print("Connection to", self.device.name, "is established.") + self.cMessageParser = MessageParser(self.device, devicetype="FS") def disconnect_device(self): """ @@ -149,7 +156,7 @@ def disconnect_device(self): """ self.device.close() - def SystemMessageCallback_usb_fs(self): + def send_message(self, message): """ Reads data from a USB device, processes received messages, and returns the data in the specified format. @@ -165,48 +172,19 @@ def SystemMessageCallback_usb_fs(self): tuple: Both integer and hexadecimal lists if `self.ret_hex_int == "both"`. None: If `self.ret_hex_int` is None. """ - timeout_count = 0 - received = [] - received_hex = [] - data_count = 0 - - while True: - buffer = self.device.read() - if buffer: - received.extend(buffer) - data_count += len(buffer) - timeout_count = 0 - continue - timeout_count += 1 - if timeout_count >= 1: - # Break if we haven't received any data - break + if self.serial_protocol == "HS": + self.device.write_data(message) + elif self.serial_protocol == "FS": + self.device.write(message) - received = "".join(str(received)) # If you need all the data - received_hex = [hex(receive) for receive in received] - try: - msg_idx = received_hex.index("0x18") - if self.print_msg: - print(msg_dict[received_hex[msg_idx + 2]]) - except BaseException: - if self.print_msg: - print(msg_dict["0x01"]) - # self.print_msg = False - if self.print_msg: - print("message buffer:\n", received_hex) - print("message length:\t", data_count) + def read_message(self): + """ + Wrapper function to read single bytes from the device. Communication method is based on the defined serial + protocol. - if self.ret_hex_int is None: - return - elif self.ret_hex_int == "hex": - return received_hex - elif self.ret_hex_int == "int": - return received - elif self.ret_hex_int == "both": - return received, received_hex + Return: + A single byte read from the device. - def SystemMessageCallback_usb_hs(self): - """ Reads data from a USB high-speed device, processes received messages, and returns the data in various formats. The method continuously reads data from the device until no more data is received. It converts the received bytes to hexadecimal format, @@ -228,7 +206,7 @@ def SystemMessageCallback_usb_hs(self): data_count = 0 while True: - buffer = self.device.read_data_bytes(size=1024, attempt=150) + buffer = self.read_message() if buffer: received.extend(buffer) data_count += len(buffer) @@ -293,11 +271,11 @@ def write_command_string(self, command): Raises: AttributeError: If `self.device` does not have the required method for the selected protocol. """ - if self.serial_protocol == "HS": - self.device.write_data(command) - elif self.serial_protocol == "FS": - self.device.write(command) - self.SystemMessageCallback() + self.cMessageParser.bPrintMessages = self.print_msg + self.send_message(command) + self.cMessageParser.read_usb_till_timeout( + bSaveData=False, bDeleteDataFrame=True + ) # --- sciospec device commands @@ -326,6 +304,7 @@ def update_BurstCount(self, burst_count): """ self.setup.burst_count = burst_count + self.print_msg = True self.write_command_string( bytearray([0xB0, 0x03, 0x02, 0x00, self.setup.burst_count, 0xB0]) ) @@ -357,6 +336,32 @@ def update_FrameRate(self, framerate): ) self.print_msg = False + def update_ExcitationFrequency(self, exc_freq): + """ + update_ExcitationFrequencies _summary_ + + Parameters + ---------- + exc_freq int + frequency to be set from 100 Hz to 1 MHz + """ + # Set frequencies: + # [CT] 0C 04 [fmin] [fmax] [fcount] [ftype] [CT] + self.print_msg = True + f_min = clTbt_sp(exc_freq) + f_max = clTbt_sp(exc_freq) + f_count = [0, 1] + f_type = [0] # linear/log + # bytearray + self.write_command_string( + bytearray( + list( + np.concatenate([[176, 12, 4], f_min, f_max, f_count, f_type, [176]]) + ) + ) + ) + self.print_msg = False + def SetMeasurementSetup(self, setup: EitMeasurementSetup): """ Configures the ScioSpec device measurement setup according to the provided EitMeasurementSetup dataclass. @@ -385,6 +390,7 @@ def SetMeasurementSetup(self, setup: EitMeasurementSetup): """ self.setup = setup + self.cMessageParser.set_measurement_setup(self.setup) self.print_msg = False self.ResetMeasurementSetup() @@ -430,8 +436,9 @@ def SetMeasurementSetup(self, setup: EitMeasurementSetup): elif setup.gain == 1_000: self.write_command_string(bytearray([0xB0, 0x03, 0x09, 0x01, 0x03, 0xB0])) - # Single ended mode: - self.write_command_string(bytearray([0xB0, 0x03, 0x08, 0x01, 0x01, 0xB0])) + # Single ended mode as standard setup, if else configured, skip patterns are possible: + self.update_measurement_mode(setup.mea_mode, boundary=setup.mea_mode_boundary) + # self.write_command_string(bytearray([0xB0, 0x03, 0x08, 0x01, 0x01, 0xB0])) # Excitation switch type: self.write_command_string(bytearray([0xB0, 0x02, 0x0C, 0x01, 0xB0])) @@ -498,7 +505,9 @@ def ResetMeasurementSetup(self): self.write_command_string(bytearray([0xB0, 0x01, 0x01, 0xB0])) self.print_msg = False - def GetMeasurementSetup(self, setup_of: str): + def update_measurement_mode( + self, meamode: str = "singleended", boundary: str = "internal" + ): """ Retrieves and configures the measurement setup for the device based on the specified setup option. @@ -565,11 +574,11 @@ def StartStopMeasurement(self, return_as="pot_mat"): self.ret_hex_int = "hex" self.print_msg = False - data = self.SystemMessageCallback_usb_fs() + data = self.SystemMessageCallback() - self.device.write(bytearray([0xB4, 0x01, 0x00, 0xB4])) - self.ret_hex_int = None - self.SystemMessageCallback() + self.send_message(bytearray([0xB4, 0x01, 0x00, 0xB4])) + self.ret_hex_int = None + self.SystemMessageCallback() data = del_hex_in_list(data) data = reshape_full_message_in_bursts(data, self.setup) @@ -581,6 +590,85 @@ def StartStopMeasurement(self, return_as="pot_mat"): elif return_as == "pot_mat": return self.get_data_as_matrix() + def StartStopMeasurement( + self, + timeout: int = 0, + return_as="pot_mat", + bSaveData: bool = False, + bDeleteData: bool = False, + sSavePath: str = "C/", + bResultsFolder=False, + ): + """ + Starts and stops a measurement process using the configured serial protocol (HS or FS). + Sends appropriate commands to the device to initiate and terminate measurement. + If a timeout is specified, data is measrued for timeout seconds (burst_count==0). Else, a burst count needs to + be specified, and all measured data is received. + Processes the received data by removing hexadecimal values, reshaping messages into bursts, + and splitting bursts into frames. Stores the processed data in NPZ format at sSavepath + + Args: + timeout (int): Specifies the timeout in seconds. + return_as (str, optional): Specifies the format of the returned data. + - "hex": Returns the processed data as a list of hexadecimal values. + - "pot_mat": Returns the processed data as a matrix using `get_data_as_matrix()`. + Default is "pot_mat". + - else: data is only stored + bSaveData (bool): Specifies if the measured data is saved in NPZ format + bDeleteData (bool): Specifies if the measured data is deleted out of memory after each EITframe, with + bSaveData=True, measured data is saved and then removed from RAM + sSavePath (str): Specifies the sPath where the measured data is saved. + bResultsFolder (bool): Specifies if additionally a folder in sSavePath is created to store the data in + + Returns: + list or matrix: The measurement data in the format specified by `return_as`. + """ + + # Start measurement + self.cMessageParser.clear_out_data() + sCurrentPath = make_results_folder( + bResultsFolder, bSaveData, sSavePath + ) # No new path is created if bResultsFolder=False + + self.send_message(bytearray([0xB4, 0x01, 0x01, 0xB4])) + self.cMessageParser.bPrintMessages = False + if timeout != 0: + self.cMessageParser.read_usb_for_seconds( + timeout, + bSaveData=bSaveData, + bDeleteDataFrame=bDeleteData, + sSavePath=sCurrentPath, + ) + else: + if self.setup.burst_count == 0: + print("Burst count for this setup needs to be >=1") + return + self.cMessageParser.read_usb_till_timeout( + bSaveData=bSaveData, + bDeleteDataFrame=bDeleteData, + sSavePath=sCurrentPath, + ) + + # Stop measurement + self.send_message(bytearray([0xB4, 0x01, 0x00, 0xB4])) + # All data is returned if wanted + data = self.cMessageParser.read_usb_till_timeout( + bSaveData=bSaveData, + bDeleteDataFrame=bDeleteData, + sSavePath=sCurrentPath, + bStartReset=False, + ) + + self.cMessageParser.clear_out_data() + if bDeleteData: + return + if return_as == "hex": + return make_eitframes_hex(data) + elif return_as == "pot_mat": + return get_data_as_matrix(data) + elif return_as == "eitframe": + return data + def get_data_as_matrix(self): """ Converts the raw EIT data into a 3D matrix of potentials. @@ -610,7 +698,7 @@ def get_data_as_matrix(self): row += 1 el_signs = list() for ch in range(16): - el_signs.append(frame.__dict__[f"ch_{ch+1}"]) + el_signs.append(frame.__dict__[f"ch_{ch + 1}"]) el_signs = np.array(el_signs) start_idx = (curr_grp - 1) * 16 diff --git a/sciopy/ISX_3.py b/sciopy/ISX_3.py index cc4f3f9..f8bfd6b 100644 --- a/sciopy/ISX_3.py +++ b/sciopy/ISX_3.py @@ -1,6 +1,8 @@ """Module for interfacing with the Sciospec ISX-3 EIT device via serial communication""" try: + from pyftdi.ftdi import Ftdi + from pyftdi.usbtools import UsbTools import serial except ImportError: print("Could not import module: serial") @@ -9,7 +11,6 @@ from .sciopy_dataclasses import EisMeasurementSetup - msg_dict = { "0x01": "No message inside the message buffer", "0x02": "Timeout: Communication-timeout (less data than expected)", diff --git a/sciopy/__init__.py b/sciopy/__init__.py index 1bda5a2..fa99657 100644 --- a/sciopy/__init__.py +++ b/sciopy/__init__.py @@ -7,7 +7,6 @@ from .EIT_16_32_64_128 import EIT_16_32_64_128, EitMeasurementSetup from .ISX_3 import ISX_3, EisMeasurementSetup - __all__ = [ "available_serial_ports", "EIT_16_32_64_128", diff --git a/sciopy/com_util.py b/sciopy/com_util.py index 0477c19..89851e1 100644 --- a/sciopy/com_util.py +++ b/sciopy/com_util.py @@ -13,6 +13,7 @@ import struct import sys from glob import glob +from .datatype_conversion import * def available_serial_ports() -> list: @@ -66,28 +67,6 @@ def clTbt_dp(val: float) -> list: return [int(ele) for ele in struct.pack(">d", val)] -def del_hex_in_list(lst: list) -> np.ndarray: - """ - Delete the hexadecimal 0x python notation. - - Parameters - ---------- - lst : list - list of hexadecimals - - Returns - ------- - np.ndarray - cleared message - """ - return np.array( - [ - "0" + ele.replace("0x", "") if len(ele) == 1 else ele.replace("0x", "") - for ele in lst - ] - ) - - def reshape_full_message_in_bursts(lst: list, ssms: EitMeasurementSetup) -> np.ndarray: """ Takes the full message buffer and splits this message depeding on the measurement configuration into the @@ -131,82 +110,6 @@ def length_correction(array: list) -> list: return np.array(split_list) -def single_hex_to_int(str_num: str) -> int: - """ - Delete the hexadecimal 0x python notation. - - Parameters - ---------- - str_num : str - single hexadecimal string - - Returns - ------- - int - integer number - """ - if len(str_num) == 1: - str_num = f"0x0{str_num}" - else: - str_num = f"0x{str_num}" - return int(str_num, 16) - - -def bytesarray_to_float(bytes_array: np.ndarray) -> float: - """ - Converts a bytes array to a float number. - - Parameters - ---------- - bytes_array : np.ndarray - array of bytes - - Returns - ------- - float - double precision float - """ - bytes_array = [int(b, 16) for b in bytes_array] - bytes_array = bytes(bytes_array) - return struct.unpack("!f", bytes(bytes_array))[0] - - -def bytesarray_to_byteslist(bytes_array: np.ndarray) -> list: - """ - Converts a bytes array to a list of bytes. - - Parameters - ---------- - bytes_array : np.ndarray - array of bytes - - Returns - ------- - list - list of bytes - """ - bytes_array = [int(b, 16) for b in bytes_array] - return bytes(bytes_array) - - -def bytesarray_to_int(bytes_array: np.ndarray) -> int: - """ - Converts a bytes array to int number. - - Parameters - ---------- - bytes_array : np.ndarray - array of bytes - - Returns - ------- - int - integer number - """ - bytes_array = bytesarray_to_byteslist(bytes_array) - return int.from_bytes(bytes_array, "big") - - def parse_single_frame(lst_ele: np.ndarray) -> SingleFrame: """ Parse single data to the class SingleFrame. @@ -256,6 +159,7 @@ def split_bursts_in_frames( channel depending burst frames """ msg_len = 140 # Constant + iC = 0 frame = [] # Channel group depending frame burst_frame = [] # single burst count frame with channel depending frame subframe_length = split_list.shape[1] // msg_len @@ -266,6 +170,10 @@ def split_bursts_in_frames( # Select the right channel group data if parsed_sgl_frame.channel_group in channel_group: frame.append(parsed_sgl_frame) + + else: + iC += 1 burst_frame.append(frame) frame = [] # Reset channel depending single burst frame + print("UNUSED CG " + str(iC)) return np.array(burst_frame) diff --git a/sciopy/datatype_conversion.py b/sciopy/datatype_conversion.py new file mode 100644 index 0000000..c177847 --- /dev/null +++ b/sciopy/datatype_conversion.py @@ -0,0 +1,214 @@ +""" +Project :sciopy +Directory: sciopy/sciopy +File : datatype_conversion.py +Author :Patricia Fuchs +Date :03.12.2025 14:02 +""" + +import struct +import numpy as np + +TWOPOWER24 = 16777216 +TWOPOWER16 = 65536 +TWOPOWER8 = 256 + + +# -------------------------------------------------------------------------------------------------------------------- # +# -------------------------------------------------------------------------------------------------------------------- # +def del_hex_in_list(lst: list) -> np.ndarray: + """ + Delete the hexadecimal 0x python notation. + + Parameters + ---------- + lst : list + list of hexadecimals + + Returns + ------- + np.ndarray + cleared message + """ + return np.array( + [ + "0" + ele.replace("0x", "") if len(ele) == 1 else ele.replace("0x", "") + for ele in lst + ] + ) + + +# -------------------------------------------------------------------------------------------------------------------- # +def single_hex_to_int(str_num: str) -> int: + """ + Delete the hexadecimal 0x python notation. + + Parameters + ---------- + str_num : str + single hexadecimal string + + Returns + ------- + int + integer number + """ + if len(str_num) == 1: + str_num = f"0x0{str_num}" + else: + str_num = f"0x{str_num}" + return int(str_num, 16) + + +# -------------------------------------------------------------------------------------------------------------------- # +def bytesarray_to_float(bytes_array: np.ndarray) -> float: + """ + Converts a bytes array to a float number. + + Parameters + ---------- + bytes_array : np.ndarray + array of bytes + + Returns + ------- + float + single precision float + """ + bytes_array = [int(b, 16) for b in bytes_array] + bytes_array = bytes(bytes_array) + return struct.unpack("!f", bytes(bytes_array))[0] + + +# -------------------------------------------------------------------------------------------------------------------- # +def byteintarray_to_float(bytes_array: np.ndarray) -> float: + """ + Converts a bytes array to a float number. Array is array of integers representing bytes. + + Parameters + ---------- + bytes_array : np.ndarray + array of integers former being bytes + + Returns + ------- + float + single precision float + """ + return struct.unpack("!f", bytes(bytes_array))[0] + + +# -------------------------------------------------------------------------------------------------------------------- # +def bytesarray_to_double(bytes_array: np.ndarray) -> float: + """ + Converts a bytes array to a float number. + + Parameters + ---------- + bytes_array : np.ndarray + array of bytes + + Returns + ------- + float + double precision float + """ + bytes_array = [int(b, 16) for b in bytes_array] + bytes_array = bytes(bytes_array) + return struct.unpack("!d", bytes(bytes_array))[0] + + +# -------------------------------------------------------------------------------------------------------------------- # +def bytesarray_to_byteslist(bytes_array: np.ndarray) -> list: + """ + Converts a bytes array to a list of bytes. + + Parameters + ---------- + bytes_array : np.ndarray + array of bytes + + Returns + ------- + list + list of bytes + """ + bytes_array = [int(b, 16) for b in bytes_array] + return bytes(bytes_array) + + +# -------------------------------------------------------------------------------------------------------------------- # +def bytesarray_to_int(bytes_array: np.ndarray) -> int: + """ + Converts a bytes array to int number. + + Parameters + ---------- + bytes_array : np.ndarray + array of bytes + + Returns + ------- + int + integer number + """ + bytes_array = bytesarray_to_byteslist(bytes_array) + return int.from_bytes(bytes_array, "big") + + +# -------------------------------------------------------------------------------------------------------------------- # +def four_byte_to_int(bytelist): + """ + Converts a list of 4 integers representing bytes to int. + + Parameters + ---------- + bytelist : np.ndarray/list of integers representing bytes MSB first + + Returns + ------- + int + integer number + """ + return ( + TWOPOWER24 * bytelist[0] + + TWOPOWER16 * bytelist[1] + + TWOPOWER8 * bytelist[2] + + bytelist[3] + ) + + +# -------------------------------------------------------------------------------------------------------------------- # +def two_byte_to_int(bytelist): + """ + Converts a list of 2 integers representing bytes to int. + + Parameters + ---------- + bytelist : np.ndarray/list of integers representing bytes MSB first + + Returns + ------- + int + integer number + """ + return TWOPOWER8 * bytelist[0] + bytelist[1] + + +# -------------------------------------------------------------------------------------------------------------------- # +def bytelist_to_int(bytelist): + """ + Converts a list of integers representing bytes MSB first to int. + Parameters + ---------- + bytelist : np.ndarray/list of integers representing bytes MSB first + + Returns + ------- + int + integer number + """ + r = bytelist[-1] + for j in range(2, len(bytelist)): + r += bytelist[-j] * 2 ** ((j - 1) * 8) + return r diff --git a/sciopy/device_interface.py b/sciopy/device_interface.py new file mode 100644 index 0000000..a753f0e --- /dev/null +++ b/sciopy/device_interface.py @@ -0,0 +1,55 @@ +""" +Project :sciopy +Directory: sciopy/sciopy +File : device_interface.py +Author :Patricia Fuchs +Date :26.11.2025 14:04 +""" + +try: + import serial +except ImportError: + print("Could not import module: serial") + +# -------------------------------------------------------------------------------------------------------------------- # +# -------------------------------------------------------------------------------------------------------------------- # + + +class DeviceInterface: + def __init__(self): + self.sProtocol = "None" + + def send_data(self, data): + pass + + def read_data(self): + return None + + +import serial + + +class USB_FS_Device(DeviceInterface): + def __init__(self, port: str, baudrate: int = 9600, timeout: int = 9000): + super().__init__() + self.sProtocol = "FS" + self.device = serial.Serial( + port=port, + baudrate=baudrate, + timeout=timeout, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + bytesize=serial.EIGHTBITS, + ) + self.name = self.device.name + + def send_data(self, data): + self.device.write(data) + + def read_data(self): + return self.device.read() + + +class USB_HS_Device(DeviceInterface): + def __init__(self, port: str, baudrate: int = 9600, timeout: int = 9000): + super().__init__() diff --git a/sciopy/doteit.py b/sciopy/doteit.py index 021c4c3..1de5478 100644 --- a/sciopy/doteit.py +++ b/sciopy/doteit.py @@ -5,7 +5,6 @@ import pickle from .sciopy_dataclasses import SingleEitFrame - header_keys = [ "number_of_header", "file_version_number", diff --git a/sciopy/sciopy_dataclasses.py b/sciopy/sciopy_dataclasses.py index ee2c079..400fef2 100644 --- a/sciopy/sciopy_dataclasses.py +++ b/sciopy/sciopy_dataclasses.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from typing import List, Tuple, Union +import numpy.typing as npt @dataclass @@ -28,6 +29,8 @@ class EitMeasurementSetup: inj_skip: Union[int, list] gain: int adc_range: int + mea_mode: str = "singleended" + mea_mode_boundary: str = "internal" # TBD: lin/log/sweep @@ -209,3 +212,34 @@ class PreperationConfig: lpath: str spath: str n_samples: int + + +# -------------------------------------------------------------------------------------------------------------------- # +@dataclass +class EITFrame: + """ + This class is for parsing the whole EIT excitation stages (and frequency stages). + Defined by Sciospec:"EIT -16,32,64,128", Chapter 5.4.1 + + Parameters + ---------- + n_el = Number of used electrodes + excitation_stgs : np.array [[int1, int2]] , Features the [ESout, ESin] injection electrodes + frequency_stgs : List[str] # todo + timestamp1 : int Timestamp of the very first measured channel group in this frame, milli seconds? + timestamp2 : int Timestamp of the very last measured channel group in this frame, milli seconds? + timestamp_pc : int Timestamp of the receiving computer for further data synchronisation from datetime.now(). + timestamp() + ppcData : np.array [[Complex measured data]] + for e in used excitations stages, + for f in used frequency stages: + for c in all used channels: -> insert complex value + """ + + n_el: int # Number of used electrodes + excitation_stgs: npt.NDArray[int] # Num Excitation Settings X 2 + frequency_stgs: npt.NDArray[int] # List of Frequency-Sweep Settings, + timestamp1: int # [ms] + timestamp2: int + timestamp_pc: int + ppcData: npt.NDArray[complex] # Channels 1-(64) all channel groups combined diff --git a/sciopy/usb_message_parser.py b/sciopy/usb_message_parser.py new file mode 100644 index 0000000..f4aa298 --- /dev/null +++ b/sciopy/usb_message_parser.py @@ -0,0 +1,496 @@ +""" +Project :sciopy +Directory: sciopy/sciopy +File : usb_message_parser.py +Author :Patricia Fuchs +Date :17.11.2025 09:04 +""" + +import numpy as np +import time + +from dataclasses import dataclass +from typing import List, Tuple, Union +import numpy.typing as npt +import os +from pandas.core.interchange import dataframe +import struct +from .sciopy_dataclasses import EitMeasurementSetup, EITFrame +from .com_util import bytesarray_to_float, byteintarray_to_float, two_byte_to_int +from datetime import datetime + +# -------------------------------------------------------------------------------------------------------------------- # +# -------------------------------------------------------------------------------------------------------------------- # +msg_dict = { + "0x01": "No message inside the message buffer", + "0x02": "Timeout: Communication-timeout (less data than expected)", + "0x04": "Wake-Up Message: System boot ready", + "0x11": "TCP-Socket: Valid TCP client-socket connection", + "0x81": "Not-Acknowledge: Command has not been executed", + "0x82": "Not-Acknowledge: Command could not be recognized", + "0x83": "Command-Acknowledge: Command has been executed successfully", + "0x84": "System-Ready Message: System is operational and ready to receive data", + "0x92": "Data holdup: Measurement data could not be sent via the master interface", +} + + +# -------------------------------------------------------------------------------------------------------------------- # +def byte_parser(): + """ + Generator to parse each input byte by byte + + Returns: + Empty lists, while message is incomplete, else full usb-message as list [int[hex-format], int [hex-format], ..] + """ + # Initialization + piCurrMess = [] + fMesstype = None + iCurrLen = 0 + data = yield # Data of dataclass Bytes + while True: + piCurrMess.extend(data) # Starting Message = Message Type + # Automatic conversion to integers within list + fMesstype = data # Save the Byte + + data = yield [] # 2 Byte = Length of Data within message + iCurrLen = int.from_bytes(data) + piCurrMess.extend(data) + + data = yield [] # Next iCurrLen Bytes = Actual Data Bytes + for i in range(iCurrLen): + piCurrMess.extend(data) + data = yield [] + + if fMesstype != data: # Last Byte != Message Type + print( + f"Current message not complete for Starting messagetype {hex(fMesstype)} and ending type {hex(data[0])}" + ) + piCurrMess.extend(data) + iCurrLen = 0 + data = yield piCurrMess # Return fully parsed message as list + piCurrMess = [] + + +# -------------------------------------------------------------------------------------------------------------------- # +# -------------------------------------------------------------------------------------------------------------------- # +class MessageParser: + """ + Parses byte wise USB messages from an Sciospec EIT Device and sorts them according to the message type + """ + + def __init__(self, device, eitsetup=None, devicetype="FS"): + # General setup + self.bPrintMessages = False + self.iNPZSaveIndex = 1 + self.iSaveCounter = 0 # Unused + self.ppcData = [] + self.iInjIndex = 0 + + # Device setup + self.cDevice = device + self.sDevicetype = devicetype + if self.sDevicetype == "FS": + self.device_send = self.send_fs + self.device_read = self.read_fs + elif self.sDevicetype == "HS": + self.device_send = self.send_hs + self.device_read = self.read_hs + self.init_parser() + + # Setup related changes + self.CurrentFrame = None + self.iMaxChannelGroups = 1 + self.iNumExcitationSettings = 1 + self.iNumFreqSettings = 1 + self.iLenDataperFrame = 1 + self.iMessagesperFrame = 1 + self.setup = eitsetup + self.set_measurement_setup(eitsetup) + + # ---------------------------------------------------------------------------------------------------------------- # + def set_measurement_setup(self, setup: EitMeasurementSetup): + """ + Gets EIT Setup and sets up the data frame accordingly + Args: + setup: EITMeasurementSetup object + """ + self.setup = setup + if setup != None: + self.iMaxChannelGroups = setup.n_el // 16 + self.iNumExcitationSettings = setup.n_el # todo should be independently set + self.iNumFreqSettings = 1 # todo + self.iLenDataperFrame = ( + self.iMaxChannelGroups + * 16 + * self.iNumExcitationSettings + * self.iNumFreqSettings + ) + self.iMessagesperFrame = ( + self.iMaxChannelGroups + * self.iNumExcitationSettings + * self.iNumFreqSettings + ) + + # ALL needed + self.reset_new_data_frame() + + # ---------------------------------------------------------------------------------------------------------------- # + def reset_new_data_frame(self): + """ + Resets the Current EITFrame. + """ + self.iInjIndex = 0 + self.iSaveCounter = 0 + self.CurrentFrame = EITFrame( + n_el=self.setup.n_el, + excitation_stgs=np.zeros((self.iNumExcitationSettings, 2), dtype=int), + frequency_stgs=np.zeros((self.iNumFreqSettings,), dtype=int), + # todo fill in setup freq settings + timestamp1=0, + timestamp2=0, + timestamp_pc=0, + ppcData=np.zeros( + self.iMaxChannelGroups * 16 * self.iNumExcitationSettings, dtype=complex + ), + ) + + # ---------------------------------------------------------------------------------------------------------------- # + def clear_out_data(self): + """ + Deletes saved data frames + """ + self.ppcData = [] + self.reset_new_data_frame() + + # ---------------------------------------------------------------------------------------------------------------- # + def init_parser(self): + """ + Initializes the parser generator + """ + self.Parser = byte_parser() + next(self.Parser) + + # ---------------------------------------------------------------------------------------------------------------- # + def read_fs(self): + """ + Read out USB connected via FS protocol + Returns: + Byte read from USB + """ + return self.cDevice.read() + + # ---------------------------------------------------------------------------------------------------------------- # + def send_fs(self, tosend): + """ + Sends a message over the USB connected via FS protocol + Args: + tosend: list/array of integers to be sent over the USB connected via FS protocol + """ + self.cDevice.write(tosend) + + # ---------------------------------------------------------------------------------------------------------------- # + def read_hs(self): + """ + Read out USB connected via HS protocol + Returns: + Byte read from USB + """ + return self.cDevice.read_data_bytes(size=1024, attempt=150) + + # ---------------------------------------------------------------------------------------------------------------- # + def send_hs(self, tosend): + """ + Sends a message over the USB connected via HS protocol + Args: + tosend: list/array of integers to be sent over the USB connected via HS protocol + """ + self.cDevice.write_data(tosend) + + # ---------------------------------------------------------------------------------------------------------------- # + def read_usb_for_seconds( + self, + fTime: float, + bSaveData: bool = False, + bDeleteDataFrame: bool = False, + sSavePath: str = "C/", + bStartReset: bool = True, + ): + """ + Reads out the USB connection for fTime seconds, regardless of whether data is received. Data bytes are parsed, + sorted into full messages and then handled according to their Command Tag. Status or requested information is + displayed if wished and measured EIT data is stored, deleted or returned. + Args: + fTime(float): time to read out usb connection (in seconds) + bSaveData: if data should be saved + bDeleteDataFrame: if data frame is deleted after saving data + sSavePath: Path where the data should be saved + Returns: + List of received data eit frames, no Status messages are saved + """ + if bStartReset: + self.reset_new_data_frame() + iMessageCount = 0 + bMessageStarted = False + timeout_count = 0 + fEndtime = time.time() + fTime + while time.time() < fEndtime or bMessageStarted: + buffer = self.device_read() + if buffer: + message = self.Parser.send(buffer) + + if len(message) > 0: + bMessageStarted = False + self.interpret_message( + message, bSaveData, bDeleteDataFrame, sSavePath + ) + iMessageCount += 1 + else: + bMessageStarted = True + timeout_count = 0 + continue + else: + time.sleep(0.1) + timeout_count += 1 + + if timeout_count >= 100: + # Break if we haven't received any data + break + print(f"{iMessageCount} message(s) received.") + return self.ppcData + + # ---------------------------------------------------------------------------------------------------------------- # + def read_usb_till_timeout( + self, + bSaveData: bool = False, + bDeleteDataFrame: bool = False, + sSavePath: str = "C/", + bStartReset: bool = True, + ): + """ + Reads out the USB connection until the connections times out, so for messages received + timeout. Data bytes are parsed, + sorted into full messages and then handled according to their Command Tag. Status or requested information is + displayed if wished and measured EIT data is stored, deleted or returned. + Args: + bSaveData: if data should be saved + bDeleteDataFrame: if data frame is deleted after saving data + sSavePath: Path where the data should be saved + Returns: + List of received data eit frames, no Status messages are saved + """ + if bStartReset: + self.reset_new_data_frame() + iMessageCount = 0 + timeout_count = 0 + while True: + buffer = self.device_read() + if buffer: + message = self.Parser.send(buffer) + if len(message) > 0: + self.interpret_message( + message, bSaveData, bDeleteDataFrame, sSavePath + ) + iMessageCount += 1 + timeout_count = 0 + continue + timeout_count += 1 + if timeout_count >= 1: + # Break if we haven't received any data + break + + print(f"{iMessageCount} message(s) received.") + return self.ppcData + + # ---------------------------------------------------------------------------------------------------------------- # + def interpret_message( + self, message, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/" + ): + """ + Message interpreter for USB messages from the Sciospec EIT Device. Status messages or requested information is + displayed, recorded EIT data is separetely stored. + Args: + message: [Byte1:int, Byte2:int, ...], structured like [Command Tag, Message Length, Message Info, Command Tag] + bSaveData: When the message is EIT data, if it should be saved + bDeleteDataFrame: When the message is EIT data, if it should be deleted from RAM after saving + sSavePath: When the message is EIT data, save path + """ + if message[0] == 180: # DATA 0XB4 + self.interpret_data_input(message, bSaveData, bDeleteDataFrame, sSavePath) + else: + mess_hex = [hex(receive) for receive in message] + if self.bPrintMessages: + if message[0] == 24: # 0x24 Acknowledgement Message + try: + print( + "Message: " + str(mess_hex) + " -> " + msg_dict[mess_hex[2]] + ) + except: + print("Message: " + str(mess_hex) + " -> " + msg_dict["0x01"]) + else: + print("Unknown received message: " + str(mess_hex)) + + # ---------------------------------------------------------------------------------------------------------------- # + def interpret_data_input( + self, message, bSave=False, bDeleteFrame=False, sSavePath="C/" + ): + """ + Interpreter of received messages with measured data. + Args: + message: Received message + bSave: If data should be saved + bDeleteFrame: If data should be deleted from RAM after saving + sSavePath: Save path + """ + # EXCITATIONSETTING + freq_group = two_byte_to_int(message[5:7]) + if ( + message[2] <= self.iMaxChannelGroups + ): # Necessary, since all four channel groups are send + if ( + message[2] == 1 and freq_group == 1 + ): # todo or gleich 0, weil er nicht mitschreibt + self.CurrentFrame.excitation_stgs[self.iInjIndex] = [ + message[3], + message[4], + ] + self.iInjIndex += 1 + + # FREQUENCY ROW is set through eitsetup + # TODO input not the number of the frequency row, but all injected frequencies, beforehand + # self.CurrentFrame.frequency_stgs = self.iNumFreqSettings + + # TIMESTAMP + if self.iSaveCounter == 0: + self.CurrentFrame.timestamp1 = message[ + 7:11 + ] # todo byteinarray_to_flaost + self.CurrentFrame.timestamp_pc = datetime.now().timestamp() + + # Data Handling + for i in range(11, 135, 8): + data = complex( + byteintarray_to_float(message[i : i + 4]), + byteintarray_to_float(message[i + 4 : i + 8]), + ) + self.CurrentFrame.ppcData[self.iSaveCounter] = data + self.iSaveCounter += 1 + if self.iSaveCounter == self.iLenDataperFrame: + # Frame Full + self.CurrentFrame.timestamp2 = byteintarray_to_float(message[7:11]) + if bSave: + save_data_frame(sSavePath, self.CurrentFrame, self.iNPZSaveIndex) + self.iNPZSaveIndex += 1 + if bDeleteFrame: + del self.CurrentFrame + else: + self.ppcData.append(self.CurrentFrame) + self.reset_new_data_frame() + + +# -------------------------------------------------------------------------------------------------------------------- # +# -------------------------------------------------------------------------------------------------------------------- # +def make_eitframes_hex(FrameList): + # todo, not working + result = [] + for f in FrameList: + result.append(hex(f.ppcData)) + return result + + +# -------------------------------------------------------------------------------------------------------------------- # +def make_results_folder(bCreateResultsFolder: bool, bSaveData: bool, sSavePath: str): + """ + Creates a new data results folder, if data should be saved. Then stores the path in the class + Args: + bCreateResultsFolder: If folder shall be created + bSaveData(bool): If data should be saved, folder is only created if data should be saved + sSavePath(str): Path where the folder should be created + """ + if bSaveData and bCreateResultsFolder: + timestr = time.strftime("%Y%m%d-%H%M%S_eit") + path = os.path.join(sSavePath, timestr) + os.mkdir(path) + return path + "/" + else: + return sSavePath + + +# -------------------------------------------------------------------------------------------------------------------- # +def get_data_as_matrix(FrameList): + """ + List of EITFrames to be reshaped into matrix of [Number frames, num injection settings, n_el] + Args: + FrameList: List of EITFrames to be reshaped into matrix + + Returns: + np.array of eit data of shape [Number frames, num injection settings, n_el] + """ + result = [] + for f in FrameList: + L = len(f.ppcData) // len(f.excitation_stgs) + result.append(np.reshape(f.ppcData, (len(f.excitation_stgs), L))) + return np.array(result) + + +# -------------------------------------------------------------------------------------------------------------------- # +def save_data_frame(path: str, dataframe: EITFrame, iNPZSaveIndex: int): + """ + Saves a single EIT frame in a npz-file. Based on the EITframe class. Saves it at self.NPZSaveIndex + Args: + path: Where to save the EIT frame + dataframe: EITFrame to be saved + iNPZSaveIndex: Index of the EIT frame to be saved + """ + np.savez( + path + "eitsample_{0:06d}.npz".format(iNPZSaveIndex), + n_el=dataframe.n_el, + excitation_stgs=dataframe.excitation_stgs, + frequency_stgs=dataframe.frequency_stgs, + timestamp1=dataframe.timestamp1, + timestamp2=dataframe.timestamp2, + timestamp_pc=dataframe.timestamp_pc, + ppcData=dataframe.ppcData, + ) + + +# -------------------------------------------------------------------------------------------------------------------- # +def load_eit_frames(path): + """ + Loads NPZ eit frames and stores them in a list of EITFrame + Args: + path: Path of the NPZ eit frames + + Returns: + List of EITFrame + """ + loaded = [] + files = os.listdir(path) + files = sorted(files) + for f in files: + l = np.load(os.path.join(path, f), allow_pickle=True) + e = EITFrame( + n_el=l["n_el"], + excitation_stgs=l["excitation_stgs"], + frequency_stgs=l["frequency_stgs"], + timestamp1=l["timestamp1"], + timestamp2=l["timestamp2"], + timestamp_pc=l["timestamp_pc"], + ppcData=l["ppcData"], + ) + loaded.append(e) + return loaded + + +# -------------------------------------------------------------------------------------------------------------------- # +def load_eit_frames_into_nparray(path): + """ + Load NPZ eit frames, retrieves the complex data and stores it in a numpy array. + Args: + path: Path of the NPZ eit frames + + Returns: np.array(ppcData) + """ + loaded = load_eit_frames(path) + l = [] + for frame in loaded: + l.append(frame.ppcData) + return np.array(l) diff --git a/setup.py b/setup.py index 4e1c4fa..84563ab 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="sciopy", - version="0.8.2.2", + version="0.9.0", packages=find_packages(), author="Jacob P. Thönes", author_email="jacob.thoenes@uni-rostock.de",