diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..3c33578 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,58 @@ +name: Tests + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install package and test dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --editable ".[test]" + + - name: Run static checks + run: ruff check sciopy tests + + - name: Run tests + run: python -m pytest + + build: + name: Build distributions + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + cache: pip + + - name: Install build frontend + run: python -m pip install --upgrade build + + - name: Build source and wheel distributions + run: python -m build diff --git a/README.md b/README.md index d06c88c..dade776 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ This package offers the serial interface for communication with an EIT device from ScioSpec. Commands can be written serially and the system response can be read out. With the current version, it is possible to start and stop measurements with defined burst counts and to read out the measurement data. In addition, the measurement data is packed into a data class for better further processing. -**WIP** Communication with ISX-3 - ## Installation ### Windows: USB Driver Setup @@ -18,6 +16,19 @@ To communicate with the Sciospec instrument over USB on Windows, you must instal > To restore the original behavior, you will need to uninstall the driver manually. > *(This rollback process has not been fully tested.)* + +### Linux/Unix Platforms + +Clone the repository, then create and activate the Conda environment: + +```bash +conda create --file environment.yml +conda activate sciopy +pip install -e . +``` + +- FTDI Driver installation: https://www.ftdichip.com/old2020/Drivers/D2XX.htm + #### Install `libusb` using Zadig 1. Download and install Zadig: @@ -35,19 +46,6 @@ To communicate with the Sciospec instrument over USB on Windows, you must instal --- -### All Platforms - -Clone the repository, then create and activate the Conda environment: - -```bash -conda create --file environment.yml -conda activate sciopy -pip install -e . -``` - - - - ## Contact @@ -55,6 +53,10 @@ If you have any ideas or other suggestions, please don't hesitate to contact me. Email: jacob.thoenes@uni-rostock.de +> [!TIP] +> I developed and tested this package on an Ubuntu/Linux system. If you encounter errors or issues on other operating systems and find a solution, you are welcome to contribute it to this repository. +> +> Thank you! + ___ -- FTDI Driver installation: https://www.ftdichip.com/old2020/Drivers/D2XX.htm \ No newline at end of file diff --git a/docs/_autosummary/sciopy.EIT_16_32_64_128.rst b/docs/_autosummary/sciopy.EIT_16_32_64_128.rst index 4cad4f7..2beb0fd 100644 --- a/docs/_autosummary/sciopy.EIT_16_32_64_128.rst +++ b/docs/_autosummary/sciopy.EIT_16_32_64_128.rst @@ -25,16 +25,18 @@ ~EIT_16_32_64_128.SoftwareReset ~EIT_16_32_64_128.StartStopMeasurement ~EIT_16_32_64_128.SystemMessageCallback - ~EIT_16_32_64_128.SystemMessageCallback_usb_fs - ~EIT_16_32_64_128.SystemMessageCallback_usb_hs ~EIT_16_32_64_128.__init__ ~EIT_16_32_64_128.connect_device_FS ~EIT_16_32_64_128.connect_device_HS ~EIT_16_32_64_128.disconnect_device ~EIT_16_32_64_128.get_data_as_matrix ~EIT_16_32_64_128.init_channel_group + ~EIT_16_32_64_128.read_message + ~EIT_16_32_64_128.send_message ~EIT_16_32_64_128.update_BurstCount + ~EIT_16_32_64_128.update_ExcitationFrequency ~EIT_16_32_64_128.update_FrameRate + ~EIT_16_32_64_128.update_measurement_mode ~EIT_16_32_64_128.write_command_string diff --git a/docs/_autosummary/sciopy.ISX_3.rst b/docs/_autosummary/sciopy.ISX_3.rst index 6c86a51..0fbfe85 100644 --- a/docs/_autosummary/sciopy.ISX_3.rst +++ b/docs/_autosummary/sciopy.ISX_3.rst @@ -13,27 +13,63 @@ .. autosummary:: - ~ISX_3.Action + ~ISX_3.AbortDCBias + ~ISX_3.AcknowledgeCompensation + ~ISX_3.AddFrequencyList + ~ISX_3.AddFrequencyPoint + ~ISX_3.ClearFE_Settings + ~ISX_3.GetARMFirmwareID + ~ISX_3.GetDCBias ~ISX_3.GetDeviceID + ~ISX_3.GetEthernetConfiguration ~ISX_3.GetExtensionPortChannel ~ISX_3.GetExtensionPortModule ~ISX_3.GetFE_Settings + ~ISX_3.GetFPGAFirmwareID ~ISX_3.GetFPGAfirmwareID + ~ISX_3.GetFrequencyCount + ~ISX_3.GetFrequencyList + ~ISX_3.GetFrequencyPoint ~ISX_3.GetOptions ~ISX_3.GetSetup ~ISX_3.GetSyncTime + ~ISX_3.InitSetup + ~ISX_3.LoadSetup + ~ISX_3.ResetCompensationData ~ISX_3.ResetSystem + ~ISX_3.SaveSettings + ~ISX_3.SaveSetup + ~ISX_3.SetAmplitude + ~ISX_3.SetCompensationData + ~ISX_3.SetCompensationLoad + ~ISX_3.SetDCBias + ~ISX_3.SetDCBiasValue + ~ISX_3.SetEthernetConfiguration ~ISX_3.SetExtensionPortChannel ~ISX_3.SetFE_Settings ~ISX_3.SetMeasurementSetup ~ISX_3.SetOptions - ~ISX_3.SetSetup ~ISX_3.SetSyncTime + ~ISX_3.SetTCPWatchdog + ~ISX_3.SoftwareReset + ~ISX_3.StartCompensation ~ISX_3.StartMeasure + ~ISX_3.StartStopMeasurement + ~ISX_3.StopMeasure ~ISX_3.SystemMessageCallback ~ISX_3.__init__ + ~ISX_3.build_frame + ~ISX_3.connect_device_FS ~ISX_3.connect_device_USB2 + ~ISX_3.decode_frame + ~ISX_3.describe_frame + ~ISX_3.disconnect_device ~ISX_3.disconnect_device_USB2 + ~ISX_3.parse_received_bytes + ~ISX_3.read_message + ~ISX_3.send_command + ~ISX_3.send_message + ~ISX_3.setup_as_dict ~ISX_3.write_command_string diff --git a/docs/_autosummary/sciopy.com_util.rst b/docs/_autosummary/sciopy.com_util.rst index 111110a..8a4856a 100644 --- a/docs/_autosummary/sciopy.com_util.rst +++ b/docs/_autosummary/sciopy.com_util.rst @@ -9,14 +9,9 @@ .. autosummary:: available_serial_ports - bytesarray_to_byteslist - bytesarray_to_float - bytesarray_to_int clTbt_dp clTbt_sp - del_hex_in_list parse_single_frame reshape_full_message_in_bursts - single_hex_to_int split_bursts_in_frames \ No newline at end of file diff --git a/docs/_autosummary/sciopy.sciopy_dataclasses.rst b/docs/_autosummary/sciopy.sciopy_dataclasses.rst index 742ec51..4635338 100644 --- a/docs/_autosummary/sciopy.sciopy_dataclasses.rst +++ b/docs/_autosummary/sciopy.sciopy_dataclasses.rst @@ -8,6 +8,7 @@ .. autosummary:: + EITFrame EisMeasurementSetup EitMeasurementSetup PreperationConfig diff --git a/examples/EIT-16-256-Ch.ipynb b/examples/EIT-16-256-Ch.ipynb index 7902813..b506d94 100644 --- a/examples/EIT-16-256-Ch.ipynb +++ b/examples/EIT-16-256-Ch.ipynb @@ -1,11 +1,15 @@ { "cells": [ { - "cell_type": "markdown", - "id": "1e1715cc-c123-45e2-b767-e6412faeebe3", - "metadata": {}, + "cell_type": "code", + "execution_count": null, + "id": "4a81fdd6-90d6-482e-9c0b-e85aa97c7d76", + "metadata": { + "tags": [] + }, + "outputs": [], "source": [ - "# Example code for connecting a Sciospec device" + "!pip list | grep sciopy" ] }, { @@ -24,7 +28,9 @@ "cell_type": "code", "execution_count": null, "id": "f49a2bed-51db-4627-b5e6-21831a620644", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", @@ -37,7 +43,9 @@ "cell_type": "code", "execution_count": null, "id": "f8c7590a-9aea-4b22-91ad-b953b540362b", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# create a 'sciospec' class which represents the sciospec EIT device\n", @@ -49,7 +57,9 @@ "cell_type": "code", "execution_count": null, "id": "aebcee3a-f3f0-4709-88d3-525eec9b6bb8", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# connect device via USB-HS port\n", @@ -60,7 +70,9 @@ "cell_type": "code", "execution_count": null, "id": "de7aaeb5-afb9-4c0d-a3ad-1a8bcf804d6a", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# read system message buffer\n", @@ -72,7 +84,9 @@ "cell_type": "code", "execution_count": null, "id": "e8609e39-a7bd-4aaf-8f02-56d7c3c527d7", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# get device info\n", @@ -83,7 +97,9 @@ "cell_type": "code", "execution_count": null, "id": "10d15631-d005-4929-9f4e-38716f0fdfb2", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# create a measurement setup\n", @@ -103,7 +119,9 @@ "cell_type": "code", "execution_count": null, "id": "9a80f291-4722-497d-9f16-672b6b50af86", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "sciospec.SetMeasurementSetup(setup)" @@ -113,7 +131,9 @@ "cell_type": "code", "execution_count": null, "id": "4ca492ed-b55d-429c-b1a3-996691918529", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# look inside the docstring of the function and manual\n", @@ -124,7 +144,9 @@ "cell_type": "code", "execution_count": null, "id": "d88f4bf1-d618-4024-a533-ce441f141062", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "sciospec.update_BurstCount(4)" @@ -134,7 +156,9 @@ "cell_type": "code", "execution_count": null, "id": "7607a215-8bb9-429a-885d-e96707d957b5", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# get data\n", @@ -148,7 +172,9 @@ "cell_type": "code", "execution_count": null, "id": "2c4c8ac0-4a41-4d85-836d-b17b0a8b81aa", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "for pot in data:\n", @@ -160,7 +186,9 @@ "cell_type": "code", "execution_count": null, "id": "c057f22c-949c-4f86-9fb0-bf88a9028ef2", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# reset the device software (may take a while ~5s, have a look at the LEDs)\n", @@ -169,22 +197,184 @@ }, { "cell_type": "markdown", - "id": "a526b930-1c20-4f6f-a500-602451eca9d7", + "id": "34de47e7-664c-4fef-8939-c598aff35ead", "metadata": {}, "source": [ - "## EIS device\n", + "## EIT device\n", "\n", - "**USB-HS**" + "**USB-FS**\n", + "\n", + "The USB-FS interface uses a regular serial port. Adjust `fs_port` to the port shown by `available_serial_ports()`." ] }, { "cell_type": "code", "execution_count": null, - "id": "c6c247dd-a41c-4ebd-9c65-867c1b995dcf", - "metadata": {}, + "id": "5ed5a7ad-728e-4842-be92-1145b672ff28", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from sciopy import available_serial_ports\n", + "\n", + "available_serial_ports()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b39759b-b896-4a06-a9ee-24f2d275567e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Select the serial port reported above. On Windows, use e.g. 'COM3'.\n", + "fs_port = \"/dev/ttyACM0\"\n", + "\n", + "sciospec_fs = EIT_16_32_64_128(n_el)\n", + "sciospec_fs.connect_device_FS(fs_port)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "750d1786-45ef-4464-b7c3-40fdd361fbcc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# read system message buffer\n", + "sciospec_fs.SystemMessageCallback()\n", + "# should be empty" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76a9be1e-ce7a-4011-89e2-d050bd0680bc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# get device info\n", + "sciospec_fs.GetDeviceInfo()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fab51086-6894-44f6-9927-16bcb007682b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# create a measurement setup for USB-FS\n", + "setup_fs = EitMeasurementSetup(\n", + " burst_count=1,\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", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "edbd0bbd-a22e-4a83-b6ab-4141affc4989", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sciospec_fs.SetMeasurementSetup(setup_fs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a26826f4-ad77-4dee-8301-e077141460dd", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# look inside the docstring of the function and manual\n", + "sciospec_fs.GetMeasurementSetup(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65f968bc-5ec6-49a3-ad44-cb26b2acbfaa", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sciospec_fs.update_BurstCount(4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb62e9ce-98dd-4e9b-9f0a-c7eb8e2a953b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# get data via USB-FS\n", + "data_fs = sciospec_fs.StartStopMeasurement(return_as=\"pot_mat\") # or \"hex\"\n", + "# data_fs.shape = (burst_count, n_el, n_el)\n", + "assert data_fs.shape[0] == setup_fs.burst_count" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "895cd58f-d2f0-44e7-8090-10085c58c370", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "for pot in data_fs:\n", + " plt.imshow(np.abs(pot))\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11cd76f3-c387-4e4d-886f-f79b5c523e17", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# reset and disconnect the USB-FS device\n", + "sciospec_fs.SoftwareReset()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "501a6e64-fd91-4bb7-a49d-36d1e63f6cb2", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "# TBD" + "sciospec_fs.disconnect_device()" ] } ], @@ -204,7 +394,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.10.20" } }, "nbformat": 4, diff --git a/examples/EIT-16-256-MessageParser.ipynb b/examples/EIT-16-256-MessageParser.ipynb deleted file mode 100644 index ac9e9f3..0000000 --- a/examples/EIT-16-256-MessageParser.ipynb +++ /dev/null @@ -1,300 +0,0 @@ -{ - "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-3.ipynb b/examples/ISX-3.ipynb index e91d9f8..d91edc8 100644 --- a/examples/ISX-3.ipynb +++ b/examples/ISX-3.ipynb @@ -1,129 +1,444 @@ { "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "d913c06e-52a4-480b-a96f-fee1e5e0148f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "!pip list | grep sciopy" + ] + }, { "cell_type": "markdown", - "id": "2727ee29", + "id": "introduction", "metadata": {}, "source": [ - "# Example code for connecting the Sciospec ISX-3 device" + "# ISX-3 / ISX-3mini example\n", + "\n", + "This notebook configures and measures an impedance spectrum with the `ISX_3` class over the USB full-speed serial interface. Connect the instrument and close the official Sciospec software before running the notebook." ] }, { "cell_type": "code", - "execution_count": 1, - "id": "18bd09f0", - "metadata": {}, + "execution_count": null, + "id": "imports", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ + "import cmath\n", + "\n", "import matplotlib.pyplot as plt\n", - "import numpy as np\n", "\n", - "from sciopy import ISX_3, EisMeasurementSetup" + "from sciopy import ISX_3, EisMeasurementSetup, available_serial_ports" ] }, { - "cell_type": "code", - "execution_count": 2, - "id": "88778348", + "cell_type": "markdown", + "id": "connection-heading", "metadata": {}, + "source": [ + "## Connect the device\n", + "\n", + "Display the available serial ports, then set `port` to the ISX-3 port. Typical names are `\"COM3\"` on Windows and `\"/dev/ttyACM0\"` or `\"/dev/ttyUSB0\"` on Linux." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "list-ports", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "isx = ISX_3()" + "available_serial_ports()" ] }, { "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": {}, + "execution_count": null, + "id": "connect", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "# reset the system (clear previous settings)\n", - "isx.ResetSystem()" + "port = \"/dev/ttyACM0\" # Change this to the port shown above.\n", + "\n", + "isx = ISX_3()\n", + "isx.connect_device_FS(port, timeout=1)" ] }, { "cell_type": "code", - "execution_count": 4, - "id": "28c54b12", + "execution_count": null, + "id": "device-info", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Read startup messages and device identification.\n", + "isx.SystemMessageCallback()\n", + "isx.GetDeviceID()\n", + "isx.GetARMFirmwareID()\n", + "isx.GetFPGAFirmwareID()" + ] + }, + { + "cell_type": "markdown", + "id": "setup-heading", "metadata": {}, + "source": [ + "## Configure the spectrum\n", + "\n", + "The example uses a logarithmic sweep and a four-point measurement on the BNC input with automatic current ranging. Amplitude is specified in millivolts. Adjust these values for your sample and device configuration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "measurement-setup", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "setup = EisMeasurementSetup(\n", - " start=10,\n", - " stop=100000,\n", - " step=20,\n", + " start=10, # Hz\n", + " stop=100_000, # Hz\n", + " step=10, # number of frequency points\n", " stepmode=\"log\",\n", - " avg=1,\n", - " amplitude=100,\n", + " avg=1, # number of spectra\n", + " amplitude=100, # mV peak amplitude\n", " precision=1,\n", - " measurement_time=1,\n", - ")" + " measurement_time=5, # response collection timeout in seconds\n", + ")\n", + "\n", + "isx.SetMeasurementSetup(setup)" ] }, { "cell_type": "code", "execution_count": null, - "id": "e8cf6f6b", + "id": "frontend-setup", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Example 1: measure through the direct BNC input.\n", + "isx.ClearFE_Settings()\n", + "isx.SetFE_Settings(\n", + " measurement_mode=\"4-point\",\n", + " measurement_channel=\"bnc\",\n", + " current_range=\"auto\",\n", + " voltage_range=\"1v\",\n", + ")\n", + "\n", + "isx.GetFE_Settings()\n", + "isx.GetFrequencyCount()\n", + "isx.GetFrequencyList()" + ] + }, + { + "cell_type": "markdown", + "id": "measurement-heading", "metadata": {}, + "source": [ + "## Measure and plot\n", + "\n", + "Each returned `ISXMeasurement` contains a frequency-row ID and a complex impedance in ohms. The plot uses the row IDs because those are supplied directly with each measurement frame." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "measure", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "isx.SetMeasurementSetup(setup)" + "data = isx.StartStopMeasurement()\n", + "if not data:\n", + " raise RuntimeError(\n", + " \"No measurements received. Check the frontend, sample, port, and timeout.\"\n", + " )\n", + "\n", + "for point in data:\n", + " print(\n", + " f\"row={point.frequency_id:3d}, \"\n", + " f\"Z={point.impedance.real:.6g} {point.impedance.imag:+.6g}j ohm\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "plot", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "row_ids = [point.frequency_id for point in data]\n", + "impedances = [point.impedance for point in data]\n", + "\n", + "fig, (ax_magnitude, ax_phase) = plt.subplots(2, 1, sharex=True)\n", + "ax_magnitude.semilogy(row_ids, [abs(z) for z in impedances], \"o-\")\n", + "ax_magnitude.set_ylabel(\"|Z| in $\\Omega$\")\n", + "ax_magnitude.grid(True)\n", + "\n", + "ax_phase.plot(row_ids, [cmath.phase(z) for z in impedances], \"o-\")\n", + "ax_phase.set(xlabel=\"Frequency-row ID\", ylabel=\"Phase in rad\")\n", + "ax_phase.grid(True)\n", + "fig.suptitle(\"ISX-3 impedance spectrum\")\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "extension-heading", + "metadata": {}, + "source": [ + "## Example 2: direct extension-port measurement\n", + "\n", + "Use `measurement_channel=\"extension\"` for a direct measurement through the extension interface without mux routing. The frequency setup configured above is reused." ] }, { "cell_type": "code", "execution_count": null, - "id": "2a8c956e", + "id": "extension-configure", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "isx.ClearFE_Settings()\n", + "isx.SetFE_Settings(\n", + " measurement_mode=\"4-point\",\n", + " measurement_channel=\"extension\",\n", + " current_range=\"auto\",\n", + " voltage_range=\"1v\",\n", + ")\n", + "isx.GetFE_Settings()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "extension-measure", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "extension_data = isx.StartStopMeasurement()\n", + "if not extension_data:\n", + " raise RuntimeError(\"No direct extension-port measurements received.\")\n", + "\n", + "for point in extension_data:\n", + " print(\n", + " f\"row={point.frequency_id:3d}, \"\n", + " f\"Z={point.impedance.real:.6g} {point.impedance.imag:+.6g}j ohm\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "extension-plot", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "ax.semilogy(\n", + " [point.frequency_id for point in extension_data],\n", + " [abs(point.impedance) for point in extension_data],\n", + " \"o-\",\n", + ")\n", + "ax.set(\n", + " xlabel=\"Frequency-row ID\",\n", + " ylabel=\"|Z| [ohm]\",\n", + " title=\"ISX-3 direct extension-port spectrum\",\n", + ")\n", + "ax.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "mux-heading", + "metadata": {}, + "source": [ + "## Example 3: multiplexer measurement\n", + "\n", + "Use `measurement_channel=\"mux\"` when a compatible Sciospec multiplexer is connected. First query the installed module, then define the desired `(C, R, WS, W)` channel routings. Channel availability and numbering depend on the installed module, so adapt the example routings to your hardware." + ] + }, + { + "cell_type": "markdown", + "id": "module-reference", "metadata": {}, + "source": [ + "### Extension and internal module identifiers\n", + "\n", + "`GetExtensionPortModule()` returns both an extension-module identifier and an internal-module identifier. These tables follow the ISX-3 / ISX-3mini manual, revision 108 (2024-09-27), section 6.5.9, **Get ExtensionPort Module**. Where the manual provides only an identifier name and no functional explanation, that limitation is stated explicitly.\n", + "\n", + "#### Extension modules\n", + "\n", + "| Code | Module returned by the device | Description from the manual |\n", + "| --- | --- | --- |\n", + "| `0x00` | No module connected | No external extension module is detected. |\n", + "| `0x01` | `MEArack` | Sciospec MEArack identifier. Section 7.5 states that the MEArack is compatible with MUX64 and CSX-64; its first pin is marked `1`. |\n", + "| `0x02` | `MuxModule32` | External module identifier named MuxModule32. Section 6.5.9 gives no additional functional description for this specific identifier. |\n", + "| `0x03` | `ECIS Adapter` | External ECIS Adapter identifier. Section 6.5.9 gives no additional functional description. |\n", + "| `0x05` | `ExtensionPortAdapter` | External ExtensionPortAdapter identifier. Section 6.5.9 gives no additional functional description. |\n", + "| `0x06` | `SlideChipAdapter` | External SlideChipAdapter identifier. Section 6.5.9 gives no additional functional description. |\n", + "| `0x07` | `Mux32any2any` (external) | External any-to-any mux identifier. The MUX32/MUX64 section describes a 32- or 64-channel mux in which every channel can be assigned as Counter, Reference, Working Sense, or Work for 2-, 3-, or 4-point measurements; measurements are acquired sequentially. |\n", + "| `0x08` | `DaQEisMux` | External DaQEisMux identifier. Section 6.5.9 gives no additional functional description. |\n", + "| `0x09` | `Mux32any2any2202` (external) | External any-to-any mux identifier with a device-reported channel count. The response includes an additional two-byte unsigned external channel count for this code. |\n", + "\n", + "There is no extension-module code `0x04` in the revision 108 table.\n", + "\n", + "#### Internal modules\n", + "\n", + "| Code | Module returned by the device | Description from the manual |\n", + "| --- | --- | --- |\n", + "| `0x00` | No module connected | No internal module is detected. |\n", + "| `0x01` | `MuxModule16x4` | Internal module identifier named MuxModule16x4. Section 6.5.9 gives no additional functional description for this specific identifier. |\n", + "| `0x02` | `MuxModule32x2` | Internal module identifier named MuxModule32x2. Section 6.5.9 gives no additional functional description for this specific identifier. |\n", + "| `0x07` | `Mux32any2any` (internal) | Internal any-to-any mux identifier. For the general MUX32/MUX64 behavior, see the Counter/Reference/Working Sense/Work description above. |\n", + "| `0x09` | `Mux32any2any2202` (internal) | Internal any-to-any mux identifier with a device-reported channel count. The response includes an additional two-byte unsigned internal channel count for this code. |\n", + "\n", + "The response length is two bytes when neither module is `0x09`, four bytes when either module is `0x09`, and six bytes when both are `0x09`. Each optional channel count occupies two bytes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mux-detect", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "# disconnect device from serial port\n", - "isx.disconnect_device_USB2()" + "# Identify the connected external and internal mux modules.\n", + "mux_module = isx.GetExtensionPortModule()\n", + "print(mux_module)" ] }, { "cell_type": "code", "execution_count": null, - "id": "1cf1c447", + "id": "mux-configure", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Replace these example channel assignments with valid routings for your mux.\n", + "mux_routings = {\n", + " \"routing 1\": (1, 2, 3, 4), # C, R, WS, W\n", + " \"routing 2\": (5, 6, 7, 8),\n", + "}\n", + "\n", + "# Select the dedicated mux measurement channel.\n", + "isx.ClearFE_Settings()\n", + "isx.SetFE_Settings(\n", + " measurement_mode=\"4-point\",\n", + " measurement_channel=\"mux\",\n", + " current_range=\"auto\",\n", + " voltage_range=\"1v\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mux-measure", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "mux_data = {}\n", + "\n", + "for name, (counter, reference, working_sense, working) in mux_routings.items():\n", + " isx.SetExtensionPortChannel(counter, reference, working_sense, working)\n", + " isx.GetExtensionPortChannel() # Read back the active C/R/WS/W routing.\n", + " points = isx.StartStopMeasurement()\n", + " if not points:\n", + " raise RuntimeError(f\"No measurements received for {name}.\")\n", + " mux_data[name] = points\n", + " print(f\"{name}: received {len(points)} points\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mux-plot", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "for name, points in mux_data.items():\n", + " ax.semilogy(\n", + " [point.frequency_id for point in points],\n", + " [abs(point.impedance) for point in points],\n", + " \"o-\",\n", + " label=name,\n", + " )\n", + "\n", + "ax.set(\n", + " xlabel=\"Frequency-row ID\",\n", + " ylabel=\"|Z| in $\\Omega$\",\n", + " title=\"ISX-3 mux measurements\",\n", + ")\n", + "ax.grid(True)\n", + "ax.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "disconnect-heading", "metadata": {}, + "source": [ + "## Disconnect\n", + "\n", + "Close the serial connection before reconnecting with this notebook or the official software." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "disconnect", + "metadata": { + "tags": [] + }, "outputs": [], - "source": [] + "source": [ + "isx.disconnect_device()" + ] } ], "metadata": { "kernelspec": { - "display_name": "base", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -137,7 +452,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.10.20" } }, "nbformat": 4, diff --git a/examples/ISX-3v2.ipynb b/examples/ISX-3v2.ipynb deleted file mode 100644 index 35c8a10..0000000 --- a/examples/ISX-3v2.ipynb +++ /dev/null @@ -1,153 +0,0 @@ -{ - "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/examples/test_EITphantom.ipynb b/examples/test_EITphantom.ipynb deleted file mode 100644 index d6f8779..0000000 --- a/examples/test_EITphantom.ipynb +++ /dev/null @@ -1,458 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "1e1715cc-c123-45e2-b767-e6412faeebe3", - "metadata": {}, - "source": [ - "# Example code for connecting a Sciospec device" - ] - }, - { - "cell_type": "markdown", - "id": "8a5423a4-f8da-4cdb-8163-1d50d12e6e23", - "metadata": {}, - "source": [ - "## EIT device\n", - "\n", - "**USB-HS**\n", - "\n", - "If you have issues with the connection try [this](https://eblot.github.io/pyftdi/installation.html)." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "f49a2bed-51db-4627-b5e6-21831a620644", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from sciopy import EIT_16_32_64_128, EitMeasurementSetup" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "f8c7590a-9aea-4b22-91ad-b953b540362b", - "metadata": {}, - "outputs": [], - "source": [ - "# create a 'sciospec' class which represents the sciospec EIT device\n", - "n_el = 32\n", - "sciospec = EIT_16_32_64_128(n_el)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "aebcee3a-f3f0-4709-88d3-525eec9b6bb8", - "metadata": {}, - "outputs": [], - "source": [ - "# connect device via USB-HS port\n", - "sciospec.connect_device_HS()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "de7aaeb5-afb9-4c0d-a3ad-1a8bcf804d6a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "No message inside the message buffer\n", - "message buffer:\n", - " ['0x18', '0x1', '0x4', '0x18', '0x18', '0x1', '0x84', '0x18']\n", - "message length:\t 8\n" - ] - } - ], - "source": [ - "# read system message buffer\n", - "sciospec.SystemMessageCallback()\n", - "# should be empty" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "e8609e39-a7bd-4aaf-8f02-56d7c3c527d7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Command-Acknowledge: Command has been executed successfully\n", - "message buffer:\n", - " ['0xd1', '0x14', '0x1', '0x0', '0x19', '0x2', '0x65', '0x10', '0x3', '0x3', '0x0', '0x21', '0x0', '0x8d', '0x0', '0xba', '0x0', '0xe4', '0x0', '0xba', '0x0', '0xe4', '0xd1', '0x18', '0x1', '0x83', '0x18']\n", - "message length:\t 27\n" - ] - } - ], - "source": [ - "# get device info\n", - "sciospec.GetDeviceInfo()" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "10d15631-d005-4929-9f4e-38716f0fdfb2", - "metadata": {}, - "outputs": [], - "source": [ - "# create a measurement setup\n", - "setup = EitMeasurementSetup(\n", - " burst_count=1,\n", - " n_el=n_el,\n", - " exc_freq=1_000,\n", - " framerate=3,\n", - " amplitude=0.01,\n", - " inj_skip=0,\n", - " gain=1,\n", - " adc_range=5,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "id": "9a80f291-4722-497d-9f16-672b6b50af86", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Command-Acknowledge: Command has been executed successfully\n", - "message buffer:\n", - " ['0x18', '0x1', '0x83', '0x18']\n", - "message length:\t 4\n", - "Command-Acknowledge: Command has been executed successfully\n", - "message buffer:\n", - " ['0x18', '0x1', '0x83', '0x18']\n", - "message length:\t 4\n", - "Command-Acknowledge: Command has been executed successfully\n", - "message buffer:\n", - " ['0x18', '0x1', '0x83', '0x18']\n", - "message length:\t 4\n", - "Command-Acknowledge: Command has been executed successfully\n", - "message buffer:\n", - " ['0x18', '0x1', '0x83', '0x18']\n", - "message length:\t 4\n" - ] - } - ], - "source": [ - "sciospec.SetMeasurementSetup(setup)" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "4ca492ed-b55d-429c-b1a3-996691918529", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "TBD (to be checked)\n", - "Command-Acknowledge: Command has been executed successfully\n", - "message buffer:\n", - " ['0xb1', '0x3', '0x2', '0x0', '0x1', '0xb1', '0x18', '0x1', '0x83', '0x18']\n", - "message length:\t 10\n", - "TBD: Translation\n" - ] - } - ], - "source": [ - "# look inside the docstring of the function and manual\n", - "sciospec.GetMeasurementSetup(2)" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "d88f4bf1-d618-4024-a533-ce441f141062", - "metadata": {}, - "outputs": [], - "source": [ - "#sciospec.update_BurstCount(4)" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "id": "7607a215-8bb9-429a-885d-e96707d957b5", - "metadata": {}, - "outputs": [], - "source": [ - "# get data\n", - "data_0 = sciospec.StartStopMeasurement(return_as=\"pot_mat\") # or \"hex\"\n", - "# data.shape = (burst_count, n_el, n_el)\n", - "# check if data shape equals setup\n", - "assert data_0.shape[0] == setup.burst_count" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "2c4c8ac0-4a41-4d85-836d-b17b0a8b81aa", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "for pot in data_0:\n", - " plt.plot(np.real(pot.flatten()))" - ] - }, - { - "cell_type": "markdown", - "id": "79456f12-0f63-4ff8-9130-a1a111e1d4a4", - "metadata": {}, - "source": [ - "Change something on the phantom" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "id": "273c6b79-39e0-46da-b01c-475c59543a48", - "metadata": {}, - "outputs": [], - "source": [ - "# get data\n", - "data_1 = sciospec.StartStopMeasurement(return_as=\"pot_mat\") # or \"hex\"\n", - "# data.shape = (burst_count, n_el, n_el)\n", - "# check if data shape equals setup\n", - "assert data_1.shape[0] == setup.burst_count" - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "id": "0f0b9cba-e52b-4781-857f-211166252121", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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da3NS96x0El9BbumaUpfyz7UueWWW7Bx40a89dZbaX+TaVu2P9DS0oLbbrsNr732GqqqqrIex/aVIxaLYd68efjRj34EADjnnHOwZcsWPPTQQ/jSl75kHMf2leOJJ57A448/jj/96U+YMWMG1q9fj9tvvx0TJ07EjTfeaBzH9lWLivbMdLybbV7yqZDRo0ejrKwszRM7evRomudHMnPrrbfi+eefxxtvvGF5pf348eMBIGfbjh8/HgMDA+js7Mx6zHBlzZo1OHr0KObOnYvy8nKUl5dj2bJl+O///m+Ul5cb7cP2lWPChAk466yzLL8788wzceDAAQC0X6d861vfwre//W184QtfwKxZs3DDDTfgX//1X7Fw4UIAbF/VqGrP8ePH48iRI2nff+zYMdfavOQdi8rKSsydOxeLFy+2/H7x4sW48MILPTqr0kDTNNxyyy14+umn8frrr2PatGmWv0+bNg3jx4+3tO3AwACWLVtmtO3cuXNRUVFhOaa1tRWbN28e9u3/kY98BJs2bcL69euNf/PmzcP111+P9evXY/r06WxfB3zwgx9MWx69c+dOTJ06FQDt1yl9fX3w+61DRFlZmbHclO2rFlXtOX/+fASDQbz33nvGMe+++y6CwaB7be5KiWiB0Zeb/u53v9O2bt2q3X777Vptba22b98+r0+tqPna176mNTQ0aEuXLtVaW1uNf319fcYxDzzwgNbQ0KA9/fTT2qZNm7Trrrsu4/KnyZMna0uWLNHWrl2rXXbZZcN2OVk+zKtCNI3t64T33ntPKy8v1+6//35t165d2v/+7/9qNTU12uOPP24cw/aV58Ybb9QmTZpkLDd9+umntdGjR2v/9m//ZhzD9hWju7tbW7dunbZu3ToNgPazn/1MW7dunbE1gqr2vPLKK7XZs2drK1as0FasWKHNmjWLy01l+NWvfqVNnTpVq6ys1M4991xjySTJDoCM/x599FHjmFgspt19993a+PHjtUAgoF188cXapk2bLN9z4sQJ7ZZbbtGampq06upq7ROf+IR24MABl6+mNEh1LNi+znjhhRe0mTNnaoFAQDvjjDO0hx9+2PJ3tq88oVBIu+2227QpU6ZoVVVV2vTp07Xvfve7WjgcNo5h+4rxxhtvZOxzb7zxRk3T1LVne3u7dv3112t1dXVaXV2ddv3112udnZ0uXaWm8bXphBBCCFFGyddYEEIIIaR4oGNBCCGEEGXQsSCEEEKIMuhYEEIIIUQZdCwIIYQQogw6FoQQQghRBh0LQgghhCiDjgUhhBBClEHHghBCCCHKoGNBCCGEEGXQsSCEEEKIMuhYEEIIIUQZ/z88mcsNfTLDngAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "for pot in data_1:\n", - " plt.plot(np.abs(pot.flatten()))" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "id": "ea137107-de11-42f1-acd7-31a34744cc37", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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lixZomQyaT21UXHOqb6+XgsXLtTs2bMlFeZ6Xuo8pcJZz66uLl133XXyPE+rV6/Wzp07ddttt131tRx3d9G+nG//WQfpmx+Q7+7LZzU1Nan/njNnjioqKnTTTTfp1VdfVX19veHMcqvQ11WS7r///tR/z549W/PmzVNZWZnefvttrVy50nBmo7N27Vp98MEHev/99y86VkjrebnzLJT1vPXWW3XkyBF9+eWX+utf/6ra2lq1t7enjl+ttRz3V0IzZszQ5MmTLypwX1/fRaUuJNdee63mzJmj48ePW08lJy6882+iraskhcNhlZWV5eXarlu3Tm+99Zbee++9tD+5UmjrebnzvJR8Xc9p06bp5ptv1rx589TY2Ki5c+fqpZdeuuprOe4jNG3aNN15551qbW1N29/a2qrKykqjWeVeMpnUhx9+qHA4bD2VnCgvL1coFEpb18HBQbW3txf0ukrSyZMn1dPTk1dr65zT2rVr9cYbb2jfvn0qLy9PO14o63ml87yUfFzPS3HOKZlMXv21zPpbHXKgpaXFTZ061f3xj390x44dc3V1de7aa691n376qfXUsubJJ590bW1t7pNPPnEHDhxwP/3pT53f78/rc+zv73eHDx92hw8fdpJcU1OTO3z4sPvXv/7lnHNu8+bNLhAIuDfeeMN1dXW5Bx980IXDYZdIJIxnnpnhzrO/v989+eSTrqOjw3V3d7v33nvPVVRUuO9973t5dZ6PP/64CwQCrq2tzfX29qa2r776KjWmENbzSudZKOu5YcMGt3//ftfd3e0++OAD98wzz7hJkya5vXv3Oueu7lrmRYScc+43v/mNKysrc9OmTXM//OEP094yWQjuv/9+Fw6H3dSpU10kEnErV650R48etZ7WmLz33ntO0kVbbW2tc+6bt/Vu3LjRhUIh53meu+eee1xXV5ftpEdhuPP86quvXHV1tbvhhhvc1KlT3Y033uhqa2vdZ599Zj3tjFzq/CS5bdu2pcYUwnpe6TwLZT0feeSR1L+nN9xwg/vJT36SCpBzV3ct+VMOAAAz4/41IQBA4SJCAAAzRAgAYIYIAQDMECEAgBkiBAAwQ4QAAGaIEADADBECAJghQgAAM0QIAGCGCAEAzPwfcqKkDt4laM0AAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "data = data_1-data_0\n", - "for pot in data:\n", - " plt.imshow(np.abs(pot))\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "50c63c37-9e0d-4d0a-b7d4-729b78f67d62", - "metadata": {}, - "outputs": [], - "source": [ - "d0 = np.diff(data_0.flatten(), append=data_0.flatten()[-1])" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "769f249b-be6f-47c0-828c-c39bbad90b3d", - "metadata": {}, - "outputs": [], - "source": [ - "d1 = np.diff(data_1.flatten(), append=data_1.flatten()[-1])" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "c057f22c-949c-4f86-9fb0-bf88a9028ef2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0.0, 35.0)" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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0aaE8icFFtUbA+CsIhZHBRZ/WCvJCjmsnTAREZEV1KwWSIr6FFCFvhGi0qiBZePGjbjooosQDofR0dGB66+/Hh988MEZH7dlyxasXLkSPp8PCxYswEMPPTStwQaj+hUWKY9CPjetYxARETULoQeWSh8znazvK+RyUBFLVYFly5YtuPXWW/Hqq69i8+bNKJVKWLNmDdLpya947N27F9deey0uu+wybN++Hd/61rfw1a9+FZs2bap6sCF9wybAWd35iIiIrCDltUCiesdfYTGKWDwl5wQWVzV3/pd/+ZdxX//kJz9BR0cH3nzzTVx++eUTPuahhx7C3Llzce+99wIAlixZgm3btuHuu+/GDTfcUNVgFZcLSeFHWMoikxhFa+ecqh5PRETUTBTjCspJgcUT0i4A+MrOWRM6ozUs8bh2qamlpWXS+7zyyitYs2bNuNs+9alPYdu2bSgWixM+Jp/PI5FIjPswpIx2wnHndOcjIiKygquoVdXK/ti42736RojBZggsQgisX78en/zkJ7Fs2bJJ7zc0NITOzs5xt3V2dqJUKmFkZOJpnY0bNyIajVY+ent7K9/L6CudndSdj4iIyAoePbAogei42/36vkJB0QSB5bbbbsO7776Ln//852e8ryRJ474WQkx4u2HDhg2Ix+OVjwMHDlS+l9PbCRfSzunOR0REZAWvfgXFHZw17vagviY0LGVRLpXqPq7pqGoNi+ErX/kKnnnmGWzduhVz5px+HUlXVxeGhobG3TY8PAyXy4XW1tYJH+P1euH1eif8XsEVBgpAOT02naETERE1Db/et8yrN141hGNtlc9T8VFEW8fPhNhRVVdYhBC47bbb8NRTT+GFF15AX1/fGR+zevVqbN68edxtzz//PFatWgW3213daAEUPdplLTXLKSEiIqLTCep9y/zh8WtN3R4vMkK7MJCKO6PqtqrAcuutt+KJJ57Az372M4TDYQwNDWFoaAjZbLZynw0bNuCmm26qfL1u3Trs378f69evx65du/Doo4/ikUcewR133DGtAVdKs3LOaXZDRERUb0JVERIZAIA/cmpxTNIoYkk4Y4lFVYHlwQcfRDwex5VXXonu7u7Kx5NPPlm5z+DgIPr7+ytf9/X14bnnnsNLL72EFStW4K//+q9x3333VV3SbBC+GABndecjIiKqt0w6AZekAgBC0VMDS0bW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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(np.abs(d0))\n", - "plt.plot(np.abs(d1))\n", - "plt.xlim([0,35])" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "a526b930-1c20-4f6f-a500-602451eca9d7", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([1.99374064e-04, 4.57524450e-04, 4.32687240e-04, 1.24907308e-04,\n", - " 6.33593869e-05, 4.56193763e-05, 4.75973902e-05, 5.54942255e-05,\n", - " 5.74022984e-05, 5.52140546e-05, 6.66276446e-05, 6.13030226e-05,\n", - " 6.02718391e-05, 6.69965106e-05, 6.11999090e-05, 6.76837959e-05,\n", - " 6.11215231e-05, 6.26887322e-05, 6.25784182e-05, 6.09588752e-05,\n", - " 6.45932617e-05, 6.03679218e-05, 6.35512850e-05, 6.24106123e-05,\n", - " 6.16357695e-05, 7.33561331e-05, 6.06342353e-05, 7.11609493e-05,\n", - " 7.34195697e-05, 7.50249677e-05, 9.92650223e-05, 1.28357489e-04])" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "abs(data[0,1,:])" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "c6c247dd-a41c-4ebd-9c65-867c1b995dcf", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([1.02363030e-04, 1.46313849e-04, 3.11154612e-04, 2.78583974e-04,\n", - " 8.09806018e-05, 3.68793542e-05, 2.63866548e-05, 2.65659120e-05,\n", - " 3.36026736e-05, 3.41811469e-05, 2.98064237e-05, 3.58745262e-05,\n", - " 3.01800618e-05, 3.89231851e-05, 3.18832099e-05, 3.74617245e-05,\n", - " 3.47063111e-05, 4.39307486e-05, 3.74252928e-05, 3.30476321e-05,\n", - " 4.23797958e-05, 4.32881682e-05, 3.41088402e-05, 3.81523427e-05,\n", - " 3.94577691e-05, 3.87632164e-05, 3.99456672e-05, 4.02552912e-05,\n", - " 4.69936571e-05, 4.14156458e-05, 4.99788884e-05, 5.93829145e-05])" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "abs(data[0,2,:])" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "2cf6296e-107b-48da-8727-7f0569eb0a40", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([0.00095033, 0.00089374, 0.00028051, 0.00016192, 0.00013652,\n", - " 0.00013013, 0.00013085, 0.00013495, 0.0001451 , 0.00015001,\n", - " 0.00014344, 0.00014393, 0.00014325, 0.00014962, 0.0001415 ,\n", - " 0.00014056])" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "np.abs(data.flatten()[:16])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "188053fa-48bf-40ec-a65f-63860fbf955d", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "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.10.20" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index be47995..e89af4d 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -1,33 +1,15 @@ """Module for interfacing with the Sciospec EIT devices via serial communication""" -try: - import serial -except ImportError: - print("Could not import module: serial") +import serial from .com_util import ( clTbt_dp, clTbt_sp, - del_hex_in_list, - reshape_full_message_in_bursts, - split_bursts_in_frames, ) 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)", - "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", -} - from .sciopy_dataclasses import EitMeasurementSetup from .usb_message_parser import ( MessageParser, @@ -54,7 +36,6 @@ def __init__(self, n_el: int) -> None: 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 @@ -75,7 +56,7 @@ def init_channel_group(self): return [ch + 1 for ch in range(self.n_el // 16)] else: raise ValueError( - f"Unallowed value: {self.n_el}. Please set 16, 32, 48 or 64 electrode mode." + f"Unallowed value: {self.n_el}. Please set 16, 32, 48, 64 or 128 electrode mode." ) def connect_device_HS(self, url: str = "ftdi://ftdi:232h/1", baudrate: int = 9000): @@ -99,10 +80,9 @@ def connect_device_HS(self, url: str = "ftdi://ftdi:232h/1", baudrate: int = 900 """ if hasattr(self, "serial_protocol"): print( - "Serial connection 'self.serial_protocol' already defined as {self.serial_protocol}." + f"Replacing existing {self.serial_protocol} serial connection with HS." ) - else: - self.serial_protocol = "HS" + self.serial_protocol = "HS" serial = Ftdi().create_from_url(url=url) serial.purge_buffers() @@ -132,10 +112,9 @@ def connect_device_FS(self, port: str, baudrate: int = 9600, timeout: int = 1): """ if hasattr(self, "serial_protocol"): print( - "Serial connection 'self.serial_protocol' already defined as {self.serial_protocol}." + f"Replacing existing {self.serial_protocol} serial connection with FS." ) - else: - self.serial_protocol = "FS" + self.serial_protocol = "FS" self.device = serial.Serial( port=port, baudrate=baudrate, @@ -158,104 +137,46 @@ def disconnect_device(self): def send_message(self, message): """ - Reads data from a USB device, processes received messages, and returns the data in the specified format. - - The method continuously reads from the device until no more data is received, then processes the received bytes. - It converts the received data to hexadecimal format and attempts to identify a specific message index. - Depending on the value of `self.ret_hex_int`, it returns the data as hexadecimal, integer, or both. - - Prints diagnostic messages if `self.print_msg` is True. - - Returns: - list[str]: List of received data in hexadecimal format if `self.ret_hex_int == "hex"`. - list[int]: List of received data as integers if `self.ret_hex_int == "int"`. - tuple: Both integer and hexadecimal lists if `self.ret_hex_int == "both"`. - None: If `self.ret_hex_int` is None. + Sends bytes using the protocol selected by ``connect_device_HS`` or + ``connect_device_FS``. """ if self.serial_protocol == "HS": self.device.write_data(message) elif self.serial_protocol == "FS": self.device.write(message) + else: + raise ValueError(f"Unsupported serial protocol: {self.serial_protocol!r}") def read_message(self): """ - Wrapper function to read single bytes from the device. Communication method is based on the defined serial - protocol. - - Return: - A single byte read from the device. - - 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, - searches for a specific message index, and prints corresponding messages if enabled. The returned data format depends on the value of - `self.ret_hex_int` ("hex", "int", "both", or None). - - Returns: - list[str]: List of received data in hexadecimal format if `self.ret_hex_int == "hex"`. - list[int]: List of received data as integers if `self.ret_hex_int == "int"`. - tuple[list[int], list[str]]: Both integer and hexadecimal lists if `self.ret_hex_int == "both"`. - None: If `self.ret_hex_int` is None. - - Raises: - BaseException: If message index is not found in the received data. - """ - timeout_count = 0 - received = [] - received_hex = [] - data_count = 0 - - while True: - buffer = self.read_message() - 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 - - 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) - - 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 + Reads one buffer using the connected parser's HS or FS transport. + """ + if self.cMessageParser is None: + raise RuntimeError("No device connected.") + return self.cMessageParser.device_read() def SystemMessageCallback(self): """ - Handles system messages based on the selected serial protocol. + Reads and handles pending messages from the connected device. - Depending on the value of `self.serial_protocol`, this method delegates - the handling of system messages to the appropriate callback: - - If `self.serial_protocol` is "HS", calls `SystemMessageCallback_usb_hs()`. - - If `self.serial_protocol` is "FS", calls `SystemMessageCallback_usb_fs()`. + The message parser is configured for the selected serial protocol when + the device is connected, so both HS and FS connections use the same + parsing entry point here. - Raises: - AttributeError: If the required callback methods are not defined. + Returns: + list: Measurement frames collected by the message parser. For a + system-message-only buffer this list is empty. """ - if self.serial_protocol == "HS": - self.SystemMessageCallback_usb_hs() - elif self.serial_protocol == "FS": - self.SystemMessageCallback_usb_fs() + if self.cMessageParser is None: + raise RuntimeError("No device connected.") + self.cMessageParser.bPrintMessages = self.print_msg + return self.cMessageParser.read_usb_till_timeout( + bSaveData=False, + bDeleteDataFrame=True, + # A callback can run directly after connecting, before a + # measurement setup (and therefore an EIT frame) exists. + bStartReset=False, + ) def write_command_string(self, command): """ @@ -271,10 +192,17 @@ def write_command_string(self, command): Raises: AttributeError: If `self.device` does not have the required method for the selected protocol. """ + if self.cMessageParser is None: + raise RuntimeError("No device connected.") self.cMessageParser.bPrintMessages = self.print_msg + self.cMessageParser.set_pending_command(command) self.send_message(command) self.cMessageParser.read_usb_till_timeout( - bSaveData=False, bDeleteDataFrame=True + bSaveData=False, + bDeleteDataFrame=True, + # Command responses do not require an EIT measurement frame and + # commands such as GetDeviceInfo can run before a setup exists. + bStartReset=False, ) # --- sciospec device commands @@ -389,6 +317,14 @@ def SetMeasurementSetup(self, setup: EitMeasurementSetup): - Output configuration is enabled for excitation, frequency stack, and timestamp. """ + if not isinstance(setup, EitMeasurementSetup): + raise TypeError("setup must be an EitMeasurementSetup instance.") + if setup.n_el != self.n_el: + raise ValueError( + "Number of electrodes in setup configuration must match " + "EIT_16_32_64_128 initialization." + ) + self.setup = setup self.cMessageParser.set_measurement_setup(self.setup) self.print_msg = False @@ -465,9 +401,6 @@ def SetMeasurementSetup(self, setup: EitMeasurementSetup): ) # Set injection config - assert setup.n_el == self.n_el, print( - "Number of electrodes in setup configuration must match Eit_16_32_64_128() initialization." - ) el_inj = np.arange(1, setup.n_el + 1) el_gnd = np.roll(el_inj, -(setup.inj_skip + 1)) for v_el, g_el in zip(el_inj, el_gnd): @@ -509,86 +442,58 @@ 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. + Updates the measurement mode and its channel-group boundary behavior. - Parameters: - setup_of (str): A string identifier for the desired measurement setup option. - Supported options include: - - 'Burst Count' (0x02) - - 'Frame Rate' (0x03) - - 'Excitation Frequencies' (0x04) - - 'Excitation Amplitude' (0x05) - - 'Excitation Sequence' (0x06) - - 'Single-Ended or Differential Measure Mode' (0x08) - - 'Gain Settings' (0x09) - - 'Excitation Switch Type' (0x0C) - - Note: - This method sends a command to the device to retrieve or configure the specified measurement setup. - The actual translation and handling of the response is TBD (to be implemented). - Burst Count 2 -> 0x02 - Frame Rate 3 -> 0x03 - Excitation Frequencies 4 -> 0x04 - Excitation Amplitude 5 -> 0x05 - Excitation Sequence 6 -> 0x06 - Single-Ended or Differential Measure Mode 7 -> 0x08 - Gain Settings 8 -> 0x09 - Excitation Switch Type 9 -> 0x0C - """ + Args: + meamode: One of ``singleended``, ``skip0``, ``skip2`` or ``skip4``. + boundary: ``internal`` for each channel group or ``external`` for + all connected channels. + """ + mode_options = { + "singleended": 0x01, + "skip0": 0x02, + "skip2": 0x03, + "skip4": 0x04, + } + if meamode not in mode_options: + raise ValueError( + f"Unknown measurement mode {meamode!r}; expected one of " + f"{', '.join(mode_options)}." + ) + boundary_options = {"internal": 0x01, "external": 0x02} + if boundary not in boundary_options: + raise ValueError( + f"Unknown boundary {boundary!r}; expected 'internal' or 'external'." + ) - print("TBD (to be checked)") self.print_msg = True - self.write_command_string(bytearray([0xB1, 0x01, setup_of, 0xB1])) - print("TBD: Translation") + self.write_command_string( + bytearray( + [ + 0xB0, + 0x03, + 0x08, + mode_options[meamode], + boundary_options[boundary], + 0xB0, + ] + ) + ) self.print_msg = False - def StartStopMeasurement(self, return_as="pot_mat"): + def GetMeasurementSetup(self, setup_of: int): """ - 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. - Processes the received data by removing hexadecimal values, reshaping messages into bursts, - and splitting bursts into frames. Stores the processed data in `self.data`. + Requests one measurement setup value from the device. Args: - 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". - - Returns: - list or matrix: The measurement data in the format specified by `return_as`. + setup_of: Setup identifier, for example 0x02 for burst count, + 0x03 for frame rate or 0x04 for excitation frequencies. """ - if self.serial_protocol == "HS": - self.device.write_data(bytearray([0xB4, 0x01, 0x01, 0xB4])) - self.ret_hex_int = "hex" - self.print_msg = False - - data = self.SystemMessageCallback_usb_hs() - - self.device.write_data(bytearray([0xB4, 0x01, 0x00, 0xB4])) - self.ret_hex_int = None - self.SystemMessageCallback() - - elif self.serial_protocol == "FS": - self.device.write(bytearray([0xB4, 0x01, 0x01, 0xB4])) - self.ret_hex_int = "hex" - self.print_msg = False - - data = 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) - data = split_bursts_in_frames(data, self.setup.burst_count, self.channel_group) - self.data = data - - if return_as == "hex": - return self.data - elif return_as == "pot_mat": - return self.get_data_as_matrix() + if not isinstance(setup_of, int) or not 0 <= setup_of <= 0xFF: + raise ValueError("setup_of must be a byte-sized integer.") + self.print_msg = True + self.write_command_string(bytearray([0xB1, 0x01, setup_of, 0xB1])) + self.print_msg = False def StartStopMeasurement( self, @@ -624,6 +529,13 @@ def StartStopMeasurement( list or matrix: The measurement data in the format specified by `return_as`. """ + if self.cMessageParser is None: + raise RuntimeError("No device connected.") + if self.setup is None: + raise RuntimeError("SetMeasurementSetup must be called before measuring.") + if return_as not in {"hex", "pot_mat", "eitframe"}: + raise ValueError("return_as must be 'hex', 'pot_mat' or 'eitframe'.") + # Start measurement self.cMessageParser.clear_out_data() sCurrentPath = make_results_folder( @@ -669,43 +581,19 @@ def StartStopMeasurement( elif return_as == "eitframe": return data - def get_data_as_matrix(self): + def get_data_as_matrix(self, data=None): """ - Converts the raw EIT data into a 3D matrix of potentials. - - The resulting matrix has the shape (burst_count, n_el, n_el), where: - - burst_count: Number of bursts in the measurement setup. - - n_el: Number of electrodes. - - For each burst, the method iterates through its frames, grouping channel data - into electrode signals and arranging them in the matrix according to their channel group. - - After processing, self.data is replaced with the resulting matrix. + Converts parsed EIT frames into a complex-valued NumPy matrix. - Returns: - np.ndarray: A 3D complex-valued matrix containing the electrode potentials - for each burst and channel group. + Args: + data: Optional list of EIT frames. If omitted, the frames currently + held by the message parser are used. """ - pot_matrix = np.empty( - (self.setup.burst_count, self.n_el, self.n_el), dtype=complex - ) - - for b_c, burst in enumerate(self.data): - row = -1 - for frame in burst: - curr_grp = frame.channel_group - if curr_grp == 1: - row += 1 - el_signs = list() - for ch in range(16): - el_signs.append(frame.__dict__[f"ch_{ch + 1}"]) - - el_signs = np.array(el_signs) - start_idx = (curr_grp - 1) * 16 - stop_idx = curr_grp * 16 - pot_matrix[b_c, row, start_idx:stop_idx] = el_signs - self.data = pot_matrix - return pot_matrix + if data is None: + if self.cMessageParser is None: + raise RuntimeError("No device connected.") + data = self.cMessageParser.ppcData + return get_data_as_matrix(data) def SetOutputConfiguration(self): print("TBD") @@ -714,13 +602,13 @@ def GetOutputConfiguration(self): self.print_msg = True # |-- Excitation setting | [CT] 02 01 [enable/disable] [CT] print("Excitation setting: [enable/disable]") - self.write_command_string(bytearray([0xB3, 0x01, 0x01, 0xB2])) + self.write_command_string(bytearray([0xB3, 0x01, 0x01, 0xB3])) # |-- Current row in the frequency stack | [CT] 02 02 [enable/disable] [CT] print("Current row in the frequency stack: [enable/disable]") - self.write_command_string(bytearray([0xB3, 0x01, 0x02, 0xB2])) + self.write_command_string(bytearray([0xB3, 0x01, 0x02, 0xB3])) # |-- Timestamp | [CT] 02 03 [enable/disable] [CT] print("Timestamp: [enable/disable]") - self.write_command_string(bytearray([0xB3, 0x01, 0x03, 0xB2])) + self.write_command_string(bytearray([0xB3, 0x01, 0x03, 0xB3])) self.print_msg = False def GetDeviceInfo(self): diff --git a/sciopy/ISX_3.py b/sciopy/ISX_3.py index f8bfd6b..98b6b8b 100644 --- a/sciopy/ISX_3.py +++ b/sciopy/ISX_3.py @@ -1,292 +1,927 @@ -"""Module for interfacing with the Sciospec ISX-3 EIT device via serial communication""" +"""Serial interface for Sciospec ISX-3 and ISX-3mini devices.""" -try: - from pyftdi.ftdi import Ftdi - from pyftdi.usbtools import UsbTools - import serial -except ImportError: - print("Could not import module: serial") +from dataclasses import asdict, dataclass +import ipaddress +import struct +import time +from typing import Iterable, Optional -from dataclasses import dataclass +import serial from .sciopy_dataclasses import EisMeasurementSetup -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", +SYSTEM_MESSAGES = { + 0x01: "Frame-Not-Acknowledge: Incorrect syntax", + 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", + 0x90: "Overcurrent detected: Value of DC current on W-ports exceeds capability of configured current range", + 0x91: "Overvoltage detected: Value of DC voltage difference between R and WS port exceeds capability of configured voltage range", } -error_msg_dict = { - "0x01": "init setup failed", - "0x02": "add frequency block failed", - "0x03": "set parasitic parameters failed", - "0x04": "set acceleration settings failed", - "0x05": "set sync time failed", - "0x06": "set channel settings failed", - "0x07": "set calibration data failed", - "0x08": "set timestamp failed", - "0x09": "start measurement failed", - "0x22": "set amplitude failed", +COMMANDS = { + 0x18: "Acknowledge / General System Message", + 0x90: "Save Settings", + 0x97: "Set Options", + 0x98: "Get Options", + 0xA1: "Reset System", + 0xB0: "Set FE Settings", + 0xB1: "Get FE Settings", + 0xB2: "Set ExtensionPort Channel", + 0xB3: "Get ExtensionPort Channel", + 0xB5: "Get ExtensionPort Module", + 0xB6: "Set Setup", + 0xB7: "Get Setup", + 0xB8: "Start Measure", + 0xB9: "Set Sync Time", + 0xBA: "Get Sync Time", + 0xBD: "Set Ethernet Configuration", + 0xBE: "Get Ethernet Configuration", + 0xCF: "TCP Connection Watchdog", + 0xD0: "Get ARM Firmware ID", + 0xD1: "Get Device ID", + 0xD2: "Get FPGA Firmware ID", } - -acknowledge_msg_dict = { - "0x01": "Frame-Not-Acknowledge: Incorrect syntax", - "0x02": "Timeout: Communication-timeout (less data than expected)", - "0x04": "Wake-Up Message: System boot ready", - "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", +OPTION_CODES = {"timestamp_ms": 0x01, "timestamp_us": 0x02, "current_range": 0x04} +MEASUREMENT_MODES = {"2-point": 0x01, "4-point": 0x02, "3-point": 0x03} +MEASUREMENT_CHANNELS = {"bnc": 0x01, "extension": 0x02, "mux": 0x03} +CURRENT_RANGES = {"auto": 0x00, "10ma": 0x01, "100ua": 0x02, "1ua": 0x04, "10na": 0x06} +VOLTAGE_RANGES = {"auto": 0x00, "1v": 0x01, "90mv": 0x02} +EXCITATION_TYPES = {"voltage": 0x01, "current": 0x02} +SCALES = {"linear": 0x00, "logarithmic": 0x01, "lin": 0x00, "log": 0x01} + +EXTENSION_MODULES = { + 0x00: "No module", + 0x01: "MEArack", + 0x02: "MuxModule32", + 0x03: "ECIS Adapter", + 0x05: "ExtensionPortAdapter", + 0x06: "SlideChipAdapter", + 0x07: "Mux32any2any", + 0x08: "DaQEisMux", + 0x09: "Mux32any2any2202", +} +INTERNAL_MODULES = { + 0x00: "No module", + 0x01: "MuxModule16x4", + 0x02: "MuxModule32x2", + 0x07: "Mux32any2any", + 0x09: "Mux32any2any2202", } -class ISX_3: +@dataclass(frozen=True) +class ISXMeasurement: + """One decoded impedance value returned by command ``0xB8``. + + Attributes + ---------- + frequency_id : int + Zero-based identifier of the configured frequency point. + impedance : complex + Measured complex impedance in ohms. + timestamp : int, optional + Device timestamp when timestamp output is enabled. + timestamp_unit : {"ms", "us"}, optional + Unit used by ``timestamp``. + current_range : int, optional + Current-range code used for this point when range output is enabled. """ - A class for interfacing with the Sciospec ISX-3 EIT device. + + frequency_id: int + impedance: complex + timestamp: Optional[int] = None + timestamp_unit: Optional[str] = None + current_range: Optional[int] = None + + +def _float_bytes(value: float) -> bytes: + """Encode a number as a four-byte, big-endian IEEE-754 float.""" + return struct.pack(">f", float(value)) + + +def _uint_bytes(value: int, length: int) -> bytes: + """Encode a non-negative integer using exactly ``length`` bytes. + + Raises + ------ + ValueError + If ``value`` is not an integer or does not fit in the requested size. + """ + if not isinstance(value, int) or not 0 <= value < 1 << (8 * length): + raise ValueError(f"value must fit in {length} unsigned bytes") + return value.to_bytes(length, "big") + + +def _choice(value, choices, name): + """Resolve a case-insensitive symbolic protocol choice or numeric code.""" + if isinstance(value, str): + try: + return choices[value.lower()] + except KeyError as error: + raise ValueError( + f"Unknown {name} {value!r}; choose from {tuple(choices)}" + ) from error + if isinstance(value, int) and value in choices.values(): + return value + raise ValueError(f"Invalid {name}: {value!r}") + + +class ISX_3: + """Interface for the documented Sciospec ISX-3 communication protocol. + + The class owns the serial transport, buffers partial protocol frames, + decodes command responses, and stores measurements received from the + device. Constructing an instance does not open a serial connection. """ def __init__(self) -> None: + """Initialize an unconnected ISX-3 interface with default output options.""" self.print_msg = True - self.ret_hex_int = None + self.device = None + self.serial_protocol = None + self._receive_buffer = bytearray() + self._pending_command = None + self.timestamp_mode = None + self.current_range_output = False + self.responses = [] + self.measurements = [] + self.setup = None + + def connect_device_FS(self, port: str, baudrate: int = 9600, timeout: float = 1): + """Connect through the USB full-speed virtual serial interface. - def connect_device_USB2(self, port: str, baudrate: int = 9600, timeout: int = 1): - """ - Connect to USB 2.0 Type B + Parameters + ---------- + port : str + Serial device name, for example ``"COM3"`` in windows or ``"/dev/ttyUSB0"`` in Linux/Unix systems. + baudrate : int, default=9600 + Serial baud rate. + timeout : float, default=1 + Read timeout in seconds. It also terminates response collection. + + Returns + ------- + ISX_3 + This instance, allowing fluent connection setup. """ - if hasattr(self, "USB-FS"): - print( - f"Serial connection 'self.serial_protocol' already defined as {self.serial_protocol}." - ) - else: - self.serial_protocol = "USB-FS" - self.device = serial.Serial( - port=port, - baudrate=baudrate, - timeout=timeout, - parity=serial.PARITY_NONE, - stopbits=serial.STOPBITS_ONE, - bytesize=serial.EIGHTBITS, - ) - print("Connection to", self.device.name, "is established.") + self.device = serial.Serial( + port=port, + baudrate=baudrate, + timeout=timeout, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + bytesize=serial.EIGHTBITS, + ) + self.serial_protocol = "FS" + self._receive_buffer.clear() + if self.print_msg: + print(f"Connection to {self.device.name} is established.") + return self + + connect_device_USB2 = connect_device_FS - def disconnect_device_USB2(self): + def disconnect_device(self): + """Close the active serial connection; do nothing when disconnected.""" + if self.device is None: + return self.device.close() - print("Connection to", self.device.name, "is closed.") + self.device = None + self.serial_protocol = None + + disconnect_device_USB2 = disconnect_device + + def _require_connection(self): + """Raise ``RuntimeError`` unless a serial device is connected.""" + if self.device is None: + raise RuntimeError("No ISX-3 device connected.") + + @staticmethod + def build_frame(command_tag: int, data: Iterable[int] = ()) -> bytearray: + """Construct a Sciospec frame from a command tag and payload. - def SystemMessageCallback(self): + The returned layout is ``[tag, payload length, payload..., tag]``. + + Raises + ------ + ValueError + If the tag is not a byte or the payload exceeds 255 bytes. """ - Reads the message buffer of a serial connection. Also prints out the general system message. + payload = bytes(data) + if not 0 <= command_tag <= 0xFF: + raise ValueError("command_tag must be a byte") + if len(payload) > 0xFF: + raise ValueError("command payload cannot exceed 255 bytes") + return bytearray([command_tag, len(payload), *payload, command_tag]) + + def parse_received_bytes(self, data) -> list[list[int]]: + """Decode complete Sciospec frames from an arbitrary serial chunk. + + Incomplete trailing data remains buffered for the next call. Multiple + frames contained in one USB read are returned separately. + + Raises + ------ + ValueError + If a complete frame has different start and end tags. """ - 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 + if data: + self._receive_buffer.extend(data) + frames = [] + while len(self._receive_buffer) >= 2: + frame_length = self._receive_buffer[1] + 3 + if len(self._receive_buffer) < frame_length: break + frame = list(self._receive_buffer[:frame_length]) + del self._receive_buffer[:frame_length] + if frame[0] != frame[-1]: + raise ValueError( + f"Invalid ISX-3 frame: start 0x{frame[0]:02X} != end 0x{frame[-1]:02X}" + ) + frames.append(frame) + return frames + + def send_message(self, message): + """Write an already framed command to the connected serial device.""" + self._require_connection() + return self.device.write(message) + + def read_message(self, size: int = 4096): + """Read up to ``size`` bytes from the connected serial device.""" + self._require_connection() + return self.device.read(size) + + def SystemMessageCallback(self, timeout: Optional[float] = None): + """Read frames until timeout and return decoded responses. - 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) - - 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 - - def write_command_string(self, command): + Parameters + ---------- + timeout : float, optional + Maximum collection time in seconds. If omitted, collection ends + when the configured serial read returns no data. + + Returns + ------- + list + Decoded dictionaries and :class:`ISXMeasurement` objects in their + receive order. """ - Function for writing a command 'bytearray(...)' to the serial port + self._require_connection() + deadline = None if timeout is None else time.monotonic() + timeout + decoded = [] + while deadline is None or time.monotonic() < deadline: + chunk = self.read_message() + if not chunk: + break + for frame in self.parse_received_bytes(chunk): + value = self.decode_frame(frame) + decoded.append(value) + self.responses.append(value) + if isinstance(value, ISXMeasurement): + self.measurements.append(value) + if not isinstance(value, ISXMeasurement): + description = self.describe_frame(frame, value) + if self.print_msg: + print(description) + return decoded + + def write_command_string(self, command, timeout: Optional[float] = None): + """Send a complete command frame and collect its response frames.""" + self._pending_command = list(command) + self.send_message(command) + return self.SystemMessageCallback(timeout=timeout) + + def send_command(self, command_tag: int, data: Iterable[int] = (), timeout=None): + """Build, send, and read a documented Sciospec command frame. + + This is the low-level extension point for commands not yet represented + by a convenience method. """ - self.device.write(command) - self.SystemMessageCallback() + return self.write_command_string(self.build_frame(command_tag, data), timeout) + + def describe_frame(self, frame, decoded=None): + """Create a human-readable command or acknowledgement description.""" + tag = frame[0] + name = COMMANDS.get(tag, "Unknown Command") + if tag == 0x18: + status = SYSTEM_MESSAGES.get(frame[2], f"Unknown status 0x{frame[2]:02X}") + if self._pending_command is not None: + command_tag = self._pending_command[0] + command = COMMANDS.get(command_tag, f"Command 0x{command_tag:02X}") + self._pending_command = None + return f"{command} -> {status}" + return status + if decoded is not None: + return f"{name} (0x{tag:02X}): {decoded}" + return f"{name} (0x{tag:02X})" + + def decode_frame(self, frame): + """Decode one validated protocol frame into a typed or dictionary value.""" + tag, payload = frame[0], bytes(frame[2:-1]) + if tag == 0x18: + return { + "command": tag, + "status": payload[0], + "message": SYSTEM_MESSAGES.get(payload[0]), + } + if tag == 0xB8: + return self._decode_measurement(payload) + if tag == 0xB1 and len(payload) >= 4: + return { + "measurement_mode": payload[0], + "measurement_channel": payload[1], + "current_range": payload[2], + "voltage_range": payload[3], + } + if tag == 0xB3 and len(payload) == 4: + return dict( + zip(("counter", "reference", "working_sense", "working"), payload) + ) + if tag == 0xB5: + return self._decode_modules(payload) + if tag == 0x98: + return self._decode_option(payload) + if tag == 0xB7: + return self._decode_setup(payload) + if tag == 0xBA and len(payload) == 4: + return {"sync_time_us": int.from_bytes(payload, "big")} + if tag == 0xBE: + return self._decode_ethernet(payload) + if tag in {0xD0, 0xD2}: + developer_length = 2 if tag == 0xD0 else 5 + return { + "developer_information": payload[:developer_length].hex(), + "revision": int.from_bytes( + payload[developer_length : developer_length + 2], "big" + ), + "build": int.from_bytes( + payload[developer_length + 2 : developer_length + 4], "big" + ), + } + if tag == 0xD1 and len(payload) >= 7: + return { + "information_version": payload[0], + "device_identifier": int.from_bytes(payload[1:3], "big"), + "serial_number": int.from_bytes(payload[3:5], "big"), + "delivery_year": 2010 + payload[5], + "delivery_month": payload[6], + "developer_information": payload[7:].hex(), + } + return {"command": tag, "name": COMMANDS.get(tag), "data": list(payload)} + + def _decode_measurement(self, payload): + """Decode a ``0xB8`` payload using the configured output options.""" + if len(payload) < 10: + raise ValueError("Incomplete ISX-3 measurement frame") + index = 2 + timestamp = None + timestamp_unit = None + if self.timestamp_mode == "ms": + timestamp = int.from_bytes(payload[index : index + 4], "big") + timestamp_unit, index = "ms", index + 4 + elif self.timestamp_mode == "us": + timestamp = int.from_bytes(payload[index : index + 5], "big") + timestamp_unit, index = "us", index + 5 + current_range = None + if self.current_range_output: + current_range, index = payload[index], index + 1 + if len(payload) - index != 8: + raise ValueError( + "Measurement layout does not match configured output options" + ) + real, imaginary = struct.unpack(">ff", payload[index : index + 8]) + return ISXMeasurement( + frequency_id=int.from_bytes(payload[:2], "big"), + impedance=complex(real, imaginary), + timestamp=timestamp, + timestamp_unit=timestamp_unit, + current_range=current_range, + ) + + @staticmethod + def _decode_modules(payload): + """Decode external and internal multiplexer module identifiers.""" + if len(payload) < 2: + raise ValueError("Incomplete ExtensionPort module response") + result = {"extension_module": EXTENSION_MODULES.get(payload[0], payload[0])} + index = 1 + if payload[0] == 0x09: + if len(payload) < 4: + raise ValueError("Incomplete external module channel count") + result["extension_channel_count"] = int.from_bytes( + payload[index : index + 2], "big" + ) + index += 2 + if index >= len(payload): + raise ValueError("Missing internal module identifier") + result["internal_module"] = INTERNAL_MODULES.get(payload[index], payload[index]) + index += 1 + if payload[index - 1] == 0x09 and len(payload) >= index + 2: + result["internal_channel_count"] = int.from_bytes( + payload[index : index + 2], "big" + ) + return result + + @staticmethod + def _decode_option(payload): + """Decode a Get Options (``0x98``) response payload.""" + if not payload: + return {} + option, data = payload[0], payload[1:] + if option == 0x03 and len(data) == 8: + minimum, maximum = struct.unpack(">ff", data) + return { + "option": "frequency_range", + "minimum_hz": minimum, + "maximum_hz": maximum, + } + return {"option": option, "value": data[0] if len(data) == 1 else list(data)} + + @staticmethod + def _decode_setup(payload): + """Decode a Get Setup (``0xB7``) response payload.""" + if not payload: + return {} + option, data = payload[0], payload[1:] + if option == 0x01 and len(data) == 2: + return {"frequency_count": int.from_bytes(data, "big")} + if option == 0x04 and len(data) % 4 == 0: + return { + "frequencies_hz": [ + struct.unpack(">f", data[i : i + 4])[0] + for i in range(0, len(data), 4) + ] + } + if option == 0x02 and len(data) >= 12: + result = { + "frequency_hz": struct.unpack(">f", data[0:4])[0], + "precision": struct.unpack(">f", data[4:8])[0], + "amplitude": struct.unpack(">f", data[8:12])[0], + } + index = 12 + while index < len(data): + extended_option = data[index] + if index + 5 > len(data): + raise ValueError("Incomplete extended frequency option") + value = int.from_bytes(data[index + 1 : index + 5], "big") + names = { + 0x01: "point_delay_us", + 0x02: "phase_sync", + 0x03: "excitation_type", + } + result[ + names.get(extended_option, f"option_0x{extended_option:02x}") + ] = value + index += 5 + return result + if option == 0x33 and len(data) == 4: + return {"dc_bias_v": struct.unpack(">f", data)[0]} + return {"option": option, "data": list(data)} + + @staticmethod + def _decode_ethernet(payload): + """Decode IP address, MAC address, or DHCP response data.""" + if not payload: + return {} + option, data = payload[0], payload[1:] + if option == 0x01 and len(data) == 4: + return {"ip_address": str(ipaddress.IPv4Address(data))} + if option == 0x02 and len(data) == 6: + return {"mac_address": ":".join(f"{value:02X}" for value in data)} + if option == 0x03 and len(data) == 1: + return {"dhcp": bool(data[0])} + return {"option": option, "data": list(data)} + + # General and option commands ------------------------------------------------- + def SaveSettings(self): + """Persist supported settings in device flash using command ``0x90``.""" + return self.send_command(0x90) def ResetSystem(self): - self.print_msg = True - self.write_command_string(bytearray([0xA1, 0x00, 0xA1])) - self.print_msg = False + """Restart the complete device using command ``0xA1``.""" + return self.send_command(0xA1) - def SetMeasurementSetup(self, setup: EisMeasurementSetup): - """ - Configures the measurement setup for the device. + SoftwareReset = ResetSystem + + def SetOptions(self, option, enabled=True): + """Enable or disable a measurement-output option. Parameters ---------- - setup : EisMeasurementSetup - An instance of EisMeasurementSetup containing the measurement parameters. + option : {"timestamp_ms", "timestamp_us", "current_range"} or int + Symbolic option name or documented option byte. + enabled : bool, default=True + Desired option state. Enabling one timestamp mode replaces the + previously tracked timestamp mode. + + Returns + ------- + list + Decoded device responses, normally followed by an ACK. """ + option_code = _choice(option, OPTION_CODES, "option") + if option_code == 0x01 and enabled: + self.timestamp_mode = "ms" + elif option_code == 0x02 and enabled: + self.timestamp_mode = "us" + elif option_code in {0x01, 0x02} and not enabled: + self.timestamp_mode = None + elif option_code == 0x04: + self.current_range_output = bool(enabled) + return self.send_command(0x97, [option_code, int(bool(enabled))]) + + def GetOptions(self, option): + """Read a timestamp, current-range, or available-frequency option.""" + return self.send_command( + 0x98, [_choice(option, OPTION_CODES | {"frequency_range": 0x03}, "option")] + ) + + # Frontend and extension-port commands --------------------------------------- + def SetFE_Settings( + self, measurement_mode, measurement_channel, current_range, voltage_range="1v" + ): + """Append one frontend configuration to the device stack. + + Parameters accept either documented numeric codes or symbolic names + from ``MEASUREMENT_MODES``, ``MEASUREMENT_CHANNELS``, + ``CURRENT_RANGES``, and ``VOLTAGE_RANGES``. + """ + data = [ + _choice(measurement_mode, MEASUREMENT_MODES, "measurement mode"), + _choice(measurement_channel, MEASUREMENT_CHANNELS, "measurement channel"), + _choice(current_range, CURRENT_RANGES, "current range"), + _choice(voltage_range, VOLTAGE_RANGES, "voltage range"), + ] + return self.send_command(0xB0, data) + + def ClearFE_Settings(self): + """Clear the frontend configuration stack using the manual's FF frame.""" + return self.send_command(0xB0, [0xFF, 0xFF, 0xFF]) - self.print_msg = True - # self.write_command_string(command) - self.print_msg = False + def GetFE_Settings(self): + """Read the currently selected frontend configuration (command ``0xB1``).""" + return self.send_command(0xB1) - def StartMeasure(self): - self.print_msg = True - self.write_command_string(bytearray([0xB8, 0x01, 0x01, 0x01, 0xB8])) - self.print_msg = False + def SetExtensionPortChannel(self, counter, reference, working_sense, working): + """Configure C, R, WS, and W selections on the extension port.""" + return self.send_command(0xB2, [counter, reference, working_sense, working]) - def SetOptions(self): - # 0x97 - pass + def GetExtensionPortChannel(self): + """Read the current C, R, WS, and W extension-port selections.""" + return self.send_command(0xB3) - def GetOptions(self): - # 0x98 - pass + def GetExtensionPortModule(self): + """Identify connected external and internal extension modules.""" + return self.send_command(0xB5) + + # Setup commands -------------------------------------------------------------- + def InitSetup(self): + """Reset the active setup and initialize an empty frequency stack.""" + return self.send_command(0xB6, [0x01]) + + @staticmethod + def _extended_options(point_delay=None, phase_sync=None, excitation_type=None): + """Encode optional delay, phase-sync, and excitation-type fields.""" + data = bytearray() + if point_delay is not None: + data.extend([0x01, *_uint_bytes(point_delay, 4)]) + if phase_sync is not None: + data.extend([0x02, *_uint_bytes(int(bool(phase_sync)), 4)]) + if excitation_type is not None: + data.extend( + [ + 0x03, + *_uint_bytes( + _choice(excitation_type, EXCITATION_TYPES, "excitation type"), 4 + ), + ] + ) + return data + + def AddFrequencyPoint( + self, + frequency, + precision, + amplitude, + *, + point_delay=None, + phase_sync=None, + excitation_type=None, + ): + """Append one frequency point to the active setup. - def SetFE_Settings(self, PP, CH, RA): + Parameters + ---------- + frequency : float + Excitation frequency in hertz. Zero selects DC resistance. + precision : float + Device precision factor. + amplitude : float + Peak excitation amplitude in volts or amperes. + point_delay : int, optional + Delay before the next point in microseconds. + phase_sync : bool, optional + Whether to switch phase-synchronously to the next point. + excitation_type : {"voltage", "current"}, optional + Unit and excitation mode used by ``amplitude``. """ - Configures the frontend measurement settings for the device. - - PP : int - Measurement mode (see above for options). - CH : int - Measurement channel (see above for options). - RA : int - Range setting (see above for options). - - Frontend configuration: - - PP (Measurement mode): - - 0x02: 4 point configuration - - CH (Measurement channel): - - 0x01: BNC Port (ISX-3mini: Port 1) - - 0x02: ExtensionPort - - 0x03: ExtensionPort2 (ISX-3mini: Port 2, ISX-3v2: optional, InternalMux) - - RA (Range Settings): - - 0x01: 100 Ohm - - 0x02: 10 kOhm - - 0x04: 1 MOhm + data = bytearray([0x02]) + data.extend( + _float_bytes(frequency) + _float_bytes(precision) + _float_bytes(amplitude) + ) + data.extend(self._extended_options(point_delay, phase_sync, excitation_type)) + return self.send_command(0xB6, data) + + def AddFrequencyList( + self, + start, + stop, + count, + scale, + precision, + amplitude, + *, + point_delay=None, + phase_sync=None, + excitation_type=None, + ): + """Append a linearly or logarithmically spaced frequency block. + + ``start`` and ``stop`` are in hertz, ``count`` is the number of points, + and ``amplitude`` is the peak voltage or current. The optional extended + fields have the same meaning as in :meth:`AddFrequencyPoint`. """ - self.print_msg = True - self.write_command_string(bytearray([0xB0, PP, CH, RA, 0xB0])) - self.print_msg = False + data = bytearray([0x03]) + data.extend(_float_bytes(start) + _float_bytes(stop) + _float_bytes(count)) + data.append(_choice(scale, SCALES, "frequency scale")) + data.extend(_float_bytes(precision) + _float_bytes(amplitude)) + data.extend(self._extended_options(point_delay, phase_sync, excitation_type)) + return self.send_command(0xB6, data) - def GetFE_Settings(self): + def SetAmplitude(self, amplitude, excitation_type="voltage", row=None): + """Set excitation amplitude for all points or one frequency row. + + Parameters + ---------- + amplitude : float + Peak amplitude in volts or amperes. + excitation_type : {"voltage", "current"}, default="voltage" + Excitation source type. + row : int, optional + Two-byte row index. If omitted, all rows are updated. """ - Retrieves the frontend measurement settings from the device. + data = bytearray([0x05]) + if row is not None: + data.extend(_uint_bytes(row, 2)) + data.append(_choice(excitation_type, EXCITATION_TYPES, "excitation type")) + data.extend(_float_bytes(amplitude)) + return self.send_command(0xB6, data) + + def StartCompensation(self): + """Start the interactive open/short/load compensation procedure.""" + return self.send_command(0xB6, [0x10]) + + def AcknowledgeCompensation(self, accept=True): + """Continue or abort a pending compensation-interaction request.""" + return self.send_command(0xB6, [0x11 if accept else 0x12]) + + def SetCompensationLoad(self, value, load_type="resistance"): + """Set the known compensation load in ohms or farads.""" + types = {"resistance": 0x01, "capacitor": 0x02} + return self.send_command( + 0xB6, [0x16, _choice(load_type, types, "load type"), *_float_bytes(value)] + ) + + def ResetCompensationData(self): + """Delete compensation data associated with the active setup.""" + return self.send_command(0xB6, [0x17, 0x01]) + + def SetCompensationData( + self, + channel, + row, + open_value, + short_value, + load_value, + ): + """Upload one open/short/load compensation record. + + ``open_value``, ``short_value``, and ``load_value`` are complex values + whose real and imaginary parts are encoded as big-endian floats. + """ + data = bytearray([0x17, 0x02, *_uint_bytes(channel, 1), *_uint_bytes(row, 2)]) + for value in (open_value, short_value, load_value): + value = complex(value) + data.extend(_float_bytes(value.real)) + data.extend(_float_bytes(value.imag)) + return self.send_command(0xB6, data) + + def LoadSetup(self, slot): + """Load setup and compensation data from flash slot 1 through 255.""" + return self.send_command(0xB6, [0x20, *_uint_bytes(slot, 1)]) + + def SaveSetup(self, slot): + """Save the active setup and compensation data to a flash slot.""" + return self.send_command(0xB7, [0x20, *_uint_bytes(slot, 1)]) + + def SetDCBias(self, enabled): + """Activate or deactivate DC-bias regulation on the configured channel.""" + return self.send_command(0xB6, [0x30, int(bool(enabled))]) + + def AbortDCBias(self): + """Abort the current DC-bias regulation process.""" + return self.send_command(0xB6, [0x32]) + + def SetDCBiasValue(self, voltage): + """Set the requested DC bias in the inclusive range -1 V to +1 V.""" + if not -1 <= voltage <= 1: + raise ValueError("DC bias voltage must be between -1 V and +1 V") + return self.send_command(0xB6, [0x33, *_float_bytes(voltage)]) + + def GetSetup(self, option, data=()): + """Read one setup property through command ``0xB7``. - Returns: - dict: Dictionary containing PP (Measurement mode), CH (Measurement channel), RA (Range setting). + Parameters + ---------- + option : {"frequency_count", "frequency_point", "frequency_list", "dc_bias"} + Setup property to request. + data : iterable of int, optional + Additional command data, such as a two-byte frequency row. """ - self.print_msg = True - # TBD: write_command_string needs to return response - self.write_command_string(bytearray([0xB1, 0x00, 0xB1])) - response = self.read_response() - self.print_msg = False - - if ( - response - and len(response) >= 6 - and response[0] == 0xB1 - and response[-1] == 0xB1 - ): - self.PP = response[2] - self.CH = response[3] - self.RA = response[4] - print("Frontend Settings:") - print(f"Measurement Mode (PP): {self.PP}") - print(f"Measurement Channel (CH): {self.CH}") - print(f"Range Setting (RA): {self.RA}") - else: - print("Failed to get FE settings or invalid response.") - return None - - def SetExtensionPortChannel(self): - # 0xB2 - pass + return self.send_command( + 0xB7, + [ + _choice( + option, + { + "frequency_count": 0x01, + "frequency_point": 0x02, + "frequency_list": 0x04, + "dc_bias": 0x33, + }, + "setup option", + ), + *data, + ], + ) + + def GetFrequencyCount(self): + """Read the total number of configured frequency points.""" + return self.GetSetup("frequency_count") + + def GetFrequencyPoint(self, row): + """Read frequency, precision, amplitude, and options for one row.""" + return self.GetSetup("frequency_point", _uint_bytes(row, 2)) + + def GetFrequencyList(self): + """Read all configured frequencies, including split response frames.""" + return self.GetSetup("frequency_list") + + def GetDCBias(self): + """Read the configured DC-bias value in volts.""" + return self.GetSetup("dc_bias") - def GetExtensionPortChannel(self): - # 0xB3 - pass + def SetMeasurementSetup(self, setup: EisMeasurementSetup): + """Initialize and populate a setup from :class:`EisMeasurementSetup`. - def GetExtensionPortModule(self): - # 0xB5 - pass + The dataclass's start, stop, step, step mode, precision, and amplitude + fields are translated into one frequency-list command. ``avg`` and + ``measurement_time`` are consumed later by :meth:`StartStopMeasurement`. + + Raises + ------ + TypeError + If ``setup`` is not an :class:`EisMeasurementSetup` instance. + """ + if not isinstance(setup, EisMeasurementSetup): + raise TypeError("setup must be an EisMeasurementSetup instance") + self.setup = setup + responses = [] + responses.extend(self.InitSetup()) + responses.extend( + self.AddFrequencyList( + setup.start, + setup.stop, + setup.step, + setup.stepmode, + setup.precision, + setup.amplitude, + ) + ) + return responses + + # Measurement ---------------------------------------------------------------- + def StartMeasure(self, repeat=1, timeout=None): + """Start a measurement and collect impedance frames. - def GetSetup(self): - # 0xB6 - pass + Parameters + ---------- + repeat : int, default=1 + Number of spectra per channel configuration. Zero starts a + continuous measurement that must be stopped explicitly. + timeout : float, optional + Maximum collection time in seconds. + + Returns + ------- + list[ISXMeasurement] + Measurements received before the read or explicit timeout. + """ + self.measurements = [] + self.send_message(self.build_frame(0xB8, [0x01, *_uint_bytes(repeat, 2)])) + self._pending_command = list( + self.build_frame(0xB8, [0x01, *_uint_bytes(repeat, 2)]) + ) + self.SystemMessageCallback(timeout=timeout) + return list(self.measurements) - def SetSetup(self): - # 0xB7 - pass + def StopMeasure(self): + """Stop a running continuous measurement using command ``0xB8``.""" + return self.send_command(0xB8, [0x00]) - def SetSyncTime(self): - # 0xB9 - pass + def StartStopMeasurement(self, repeat=None, timeout=None, return_as="measurement"): + """Measure spectra and return records or complex impedance values. + + If ``repeat`` or ``timeout`` is omitted, values from the configured + :class:`EisMeasurementSetup` are used. A continuous run (``repeat=0``) + is stopped after collection. + + Parameters + ---------- + return_as : {"measurement", "complex"}, default="measurement" + Return full :class:`ISXMeasurement` records or only impedances. + """ + if repeat is None: + repeat = 1 if self.setup is None else int(self.setup.avg) + if timeout is None and self.setup is not None and self.setup.measurement_time: + timeout = float(self.setup.measurement_time) + data = self.StartMeasure(repeat=repeat, timeout=timeout) + if repeat == 0: + self.StopMeasure() + if return_as == "measurement": + return data + if return_as == "complex": + return [measurement.impedance for measurement in data] + raise ValueError("return_as must be 'measurement' or 'complex'") + + # Synchronization, network, and identification ------------------------------- + def SetSyncTime(self, microseconds): + """Set inter-spectrum synchronization time from 0 to 180,000,000 µs.""" + if not 0 <= microseconds <= 180_000_000: + raise ValueError("sync time must be between 0 and 180,000,000 µs") + return self.send_command(0xB9, _uint_bytes(microseconds, 4)) def GetSyncTime(self): - # 0xBA - pass + """Read the currently configured synchronization time in microseconds.""" + return self.send_command(0xBA) + + def SetEthernetConfiguration(self, *, ip_address=None, dhcp=None): + """Set either the static IPv4 address or DHCP state. + + Exactly one keyword must be provided. Saving and applying network + configuration may additionally require ``SaveSettings`` and a reboot. + """ + if (ip_address is None) == (dhcp is None): + raise ValueError("provide exactly one of ip_address or dhcp") + if ip_address is not None: + return self.send_command( + 0xBD, [0x01, *ipaddress.IPv4Address(ip_address).packed] + ) + return self.send_command(0xBD, [0x03, int(bool(dhcp))]) + + def GetEthernetConfiguration(self, option): + """Read ``ip_address``, ``mac_address``, or ``dhcp`` configuration.""" + options = {"ip_address": 0x01, "mac_address": 0x02, "dhcp": 0x03} + return self.send_command(0xBE, [_choice(option, options, "Ethernet option")]) + + def SetTCPWatchdog(self, seconds): + """Set the TCP connection watchdog interval from 1 to 600 seconds.""" + if not 1 <= seconds <= 600: + raise ValueError("TCP watchdog interval must be between 1 and 600 seconds") + return self.send_command(0xCF, [0x00, *_uint_bytes(seconds, 4)]) + + def GetARMFirmwareID(self): + """Read ARM developer, revision, and build information (``0xD0``).""" + return self.send_command(0xD0) def GetDeviceID(self): - # 0xD1 - pass + """Read device type, serial number, delivery date, and developer data.""" + return self.send_command(0xD1) - def GetFPGAfirmwareID(self): - # 0xD2 - pass + def GetFPGAFirmwareID(self): + """Read FPGA developer, revision, and build information (``0xD2``).""" + return self.send_command(0xD2) - def GetExtensionPortChannel(self): - # 0xD3 - pass + GetFPGAfirmwareID = GetFPGAFirmwareID - def Action(self): - self.print_msg = True - self.write_command_string(bytearray([0xD2, 0x00, 0xD2])) - self.print_msg = False - - -# - 0x90 - Save Settings -# - 0x97 - Set Options -# - 0x98 - Get Options -# - 0x99 - Set IOPort Configuration -# - 0x9A - Get IOPort Configuration -# - 0x9B - Set NTC Parameter 1 -# - 0x9D - Set NTC Parameter 2 -# - 0x9C - Get NTC Parameter 1 -# - 0x9E - Get NTC Parameter 2 -# - 0xA1 - Reset System -# - 0xB0 - Set FE Settings -# - 0xB1 - Get FE Settings -# - 0xB2 - Set ExtensionPort Channel -# - 0xB3 - Get ExtPort Channel -# - 0xB5 - Get ExtPort Module -# - 0xB6 - Set Setup -# - 0xB7 - Get Setup -# - 0xB8 - Start Measure -# - 0xB9 - Set Sync Time -# - 0xBA - Get Sync Time -# - 0xBD - Set Ethernet Configuration -# - 0xBE - Get Ethernet Configur -# - 0xD0 - Get ARM firmware ID + def setup_as_dict(self): + """Return the configured EIS setup as a dictionary, or ``None``.""" + return None if self.setup is None else asdict(self.setup) + + +__all__ = ["ISX_3", "ISXMeasurement", "EisMeasurementSetup"] diff --git a/sciopy/com_util.py b/sciopy/com_util.py index 89851e1..30e2c63 100644 --- a/sciopy/com_util.py +++ b/sciopy/com_util.py @@ -2,10 +2,7 @@ from typing import Union -try: - import serial -except ImportError: - print("Could not import module: serial") +import serial from .sciopy_dataclasses import EitMeasurementSetup, SingleFrame @@ -13,7 +10,11 @@ import struct import sys from glob import glob -from .datatype_conversion import * +from .datatype_conversion import ( + bytesarray_to_float, + bytesarray_to_int, + single_hex_to_int, +) def available_serial_ports() -> list: diff --git a/sciopy/datatype_conversion.py b/sciopy/datatype_conversion.py index c177847..67d21f8 100644 --- a/sciopy/datatype_conversion.py +++ b/sciopy/datatype_conversion.py @@ -30,12 +30,13 @@ def del_hex_in_list(lst: list) -> np.ndarray: np.ndarray cleared message """ - return np.array( - [ - "0" + ele.replace("0x", "") if len(ele) == 1 else ele.replace("0x", "") - for ele in lst - ] - ) + result = [] + for element in lst: + value = int(element, 16) if isinstance(element, str) else int(element) + if not 0 <= value <= 0xFF: + raise ValueError(f"Value {element!r} is not a byte.") + result.append(f"{value:02x}") + return np.asarray(result) # -------------------------------------------------------------------------------------------------------------------- # @@ -53,10 +54,6 @@ def single_hex_to_int(str_num: str) -> int: 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) @@ -75,9 +72,7 @@ def bytesarray_to_float(bytes_array: np.ndarray) -> float: 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] + return struct.unpack("!f", _hex_array_to_bytes(bytes_array, expected_length=4))[0] # -------------------------------------------------------------------------------------------------------------------- # @@ -95,7 +90,10 @@ def byteintarray_to_float(bytes_array: np.ndarray) -> float: float single precision float """ - return struct.unpack("!f", bytes(bytes_array))[0] + values = bytes(bytes_array) + if len(values) != 4: + raise ValueError("A single-precision float requires exactly 4 bytes.") + return struct.unpack("!f", values)[0] # -------------------------------------------------------------------------------------------------------------------- # @@ -113,9 +111,7 @@ def bytesarray_to_double(bytes_array: np.ndarray) -> float: 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] + return struct.unpack("!d", _hex_array_to_bytes(bytes_array, expected_length=8))[0] # -------------------------------------------------------------------------------------------------------------------- # @@ -133,8 +129,7 @@ def bytesarray_to_byteslist(bytes_array: np.ndarray) -> list: list list of bytes """ - bytes_array = [int(b, 16) for b in bytes_array] - return bytes(bytes_array) + return _hex_array_to_bytes(bytes_array) # -------------------------------------------------------------------------------------------------------------------- # @@ -170,12 +165,9 @@ def four_byte_to_int(bytelist): int integer number """ - return ( - TWOPOWER24 * bytelist[0] - + TWOPOWER16 * bytelist[1] - + TWOPOWER8 * bytelist[2] - + bytelist[3] - ) + if len(bytelist) != 4: + raise ValueError("Expected exactly 4 bytes.") + return int.from_bytes(bytes(int(value) for value in bytelist), "big") # -------------------------------------------------------------------------------------------------------------------- # @@ -192,7 +184,9 @@ def two_byte_to_int(bytelist): int integer number """ - return TWOPOWER8 * bytelist[0] + bytelist[1] + if len(bytelist) != 2: + raise ValueError("Expected exactly 2 bytes.") + return int.from_bytes(bytes(int(value) for value in bytelist), "big") # -------------------------------------------------------------------------------------------------------------------- # @@ -208,7 +202,13 @@ def bytelist_to_int(bytelist): int integer number """ - r = bytelist[-1] - for j in range(2, len(bytelist)): - r += bytelist[-j] * 2 ** ((j - 1) * 8) - return r + return int.from_bytes(bytes(int(value) for value in bytelist), "big") + + +def _hex_array_to_bytes(values, expected_length=None): + result = bytes( + int(value, 16) if isinstance(value, str) else int(value) for value in values + ) + if expected_length is not None and len(result) != expected_length: + raise ValueError(f"Expected exactly {expected_length} bytes.") + return result diff --git a/sciopy/device_interface.py b/sciopy/device_interface.py index a753f0e..78ca745 100644 --- a/sciopy/device_interface.py +++ b/sciopy/device_interface.py @@ -6,10 +6,7 @@ Date :26.11.2025 14:04 """ -try: - import serial -except ImportError: - print("Could not import module: serial") +import serial # -------------------------------------------------------------------------------------------------------------------- # # -------------------------------------------------------------------------------------------------------------------- # @@ -20,17 +17,17 @@ def __init__(self): self.sProtocol = "None" def send_data(self, data): - pass + raise NotImplementedError def read_data(self): - return None - + raise NotImplementedError -import serial + def close(self): + raise NotImplementedError class USB_FS_Device(DeviceInterface): - def __init__(self, port: str, baudrate: int = 9600, timeout: int = 9000): + def __init__(self, port: str, baudrate: int = 9600, timeout: float = 1): super().__init__() self.sProtocol = "FS" self.device = serial.Serial( @@ -49,7 +46,13 @@ def send_data(self, data): def read_data(self): return self.device.read() + def close(self): + self.device.close() + class USB_HS_Device(DeviceInterface): def __init__(self, port: str, baudrate: int = 9600, timeout: int = 9000): super().__init__() + raise NotImplementedError( + "USB_HS_Device is not implemented; use EIT_16_32_64_128.connect_device_HS()." + ) diff --git a/sciopy/doteit.py b/sciopy/doteit.py index 1de5478..8f57b18 100644 --- a/sciopy/doteit.py +++ b/sciopy/doteit.py @@ -75,12 +75,9 @@ def list_eit_files(path: str) -> list: list list of files with .eit ending """ - global src_list - try: - src_list = [_ for _ in os.listdir(path) if _.endswith(".eit")] - return src_list - except BaseException: - print(f"Can not find a .eit file in:\n {path}") + if not os.path.isdir(path): + raise FileNotFoundError(f"Directory does not exist: {path}") + return sorted(name for name in os.listdir(path) if name.lower().endswith(".eit")) def list_all_files(path: str) -> None: @@ -119,7 +116,7 @@ def single_eit_in_pickle(fname: str, s_path: str) -> None: frame = doteit_in_SingleEitFrame(read_content) - with open(f"{s_path}/{frame.setup_name}.pickle", "wb") as f: + with open(os.path.join(s_path, f"{frame.setup_name}.pickle"), "wb") as f: pickle.dump(frame, f) @@ -137,8 +134,13 @@ def load_pickle_to_dict(path: str) -> dict: dict dicionary of the loaded pickle file """ - tmp = np.load(path, allow_pickle=True).__dict__ - return tmp + with open(path, "rb") as file: + value = pickle.load(file) + if isinstance(value, dict): + return value + if hasattr(value, "__dict__"): + return vars(value) + raise TypeError("Pickle must contain a dictionary or an object with attributes.") def convert_fulldir_doteit_to_pickle(lpath: str, spath: str) -> None: @@ -158,7 +160,7 @@ def convert_fulldir_doteit_to_pickle(lpath: str, spath: str) -> None: """ objects = list_eit_files(lpath) for obj in objects: - fname = lpath + obj + fname = os.path.join(lpath, obj) single_eit_in_pickle(fname, spath) print("converted:", obj) print("\t Saved in", spath) @@ -181,9 +183,9 @@ def convert_fulldir_doteit_to_npz(lpath: str, spath: str) -> None: """ objects = list_eit_files(lpath) for obj in objects: - fname = lpath + obj + fname = os.path.join(lpath, obj) with open(fname, "r") as file: read_content = file.read().split("\n") frame = doteit_in_SingleEitFrame(read_content) - np.savez(f"{spath}{frame.setup_name}.npz", **(frame.__dict__)) + np.savez(os.path.join(spath, f"{frame.setup_name}.npz"), **vars(frame)) diff --git a/sciopy/meshing.py b/sciopy/meshing.py index fcebaf4..f67501b 100644 --- a/sciopy/meshing.py +++ b/sciopy/meshing.py @@ -37,7 +37,7 @@ def create_empty_2d_mesh( """ mesh_obj = mesh.create(n_el=n_el, h0=h0) mesh_obj.node[:, 2] = z_level - mesh_obj.el_pos = np.roll(np.arange(16)[::-1], -3) + mesh_obj.el_pos = np.roll(np.arange(n_el)[::-1], -3) if default_perm != 1.0: mesh_obj.perm_array = mesh_obj.perm_array * default_perm mesh_obj.perm = default_perm diff --git a/sciopy/usb_message_parser.py b/sciopy/usb_message_parser.py index ad34fb6..d4d336d 100644 --- a/sciopy/usb_message_parser.py +++ b/sciopy/usb_message_parser.py @@ -11,12 +11,12 @@ import os from .sciopy_dataclasses import EitMeasurementSetup, EITFrame -from .com_util import byteintarray_to_float, two_byte_to_int +from .datatype_conversion import byteintarray_to_float, two_byte_to_int from datetime import datetime # -------------------------------------------------------------------------------------------------------------------- # msg_dict = { - "0x01": "No message inside the message buffer", + "0x01": "Frame-Not-Acknowledge: Incorrect syntax", "0x02": "Timeout: Communication-timeout (less data than expected)", "0x04": "Wake-Up Message: System boot ready", "0x11": "TCP-Socket: Valid TCP client-socket connection", @@ -24,9 +24,81 @@ "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", + "0x91": "Data holdup: Measurement data could not be sent via the master interface", } +COMMAND_INFO = { + 0x18: "Acknowledge / General System Message", + 0x90: "Save Settings", + 0xA1: "Software Reset", + 0xB0: "Set Measurement Setup", + 0xB1: "Get Measurement Setup", + 0xB2: "Set Output Configuration", + 0xB3: "Get Output Configuration", + 0xB4: "Start/Stop Measurement", + 0xB5: "Get Temperature", + 0xC6: "Set Battery Control", + 0xC7: "Get Battery Control", + 0xC8: "Set LED Control", + 0xC9: "Get LED Control", + 0xCB: "FrontIOs", + 0xCC: "Power Plug Detect", + 0xCF: "TCP Connection Watchdog", + 0xD1: "Get Device Info", + 0xD2: "Get Firmware IDs", +} + +MEASUREMENT_SETUP_OPTIONS = { + 0x01: "Reset Setup", + 0x02: "Burst Count", + 0x03: "Frame Rate", + 0x04: "Excitation Frequencies", + 0x05: "Excitation Amplitude", + 0x06: "Excitation Sequence", + 0x08: "Single-Ended or Differential Measurement Mode", + 0x09: "Gain Settings", + 0x0C: "Excitation Switch Type", + 0x0D: "ADC Range", +} + +OUTPUT_CONFIGURATION_OPTIONS = { + 0x01: "Excitation Setting", + 0x02: "Current Row in Frequency Stack", + 0x03: "Timestamp", +} + + +def describe_message(message): + """Return the command information defined by the Sciospec EIT manual.""" + if len(message) < 3: + return "Incomplete Sciospec message" + + command_tag = message[0] + command_name = COMMAND_INFO.get(command_tag, "Unknown Command") + description = f"{command_name} (0x{command_tag:02X})" + + if command_tag == 0x18: + status = msg_dict.get( + hex(message[2]), f"Unknown system status 0x{message[2]:02X}" + ) + return f"{description}: {status}" + + if command_tag in {0xB0, 0xB1}: + option = MEASUREMENT_SETUP_OPTIONS.get( + message[2], f"Unknown option 0x{message[2]:02X}" + ) + description += f" – {option}" + elif command_tag in {0xB2, 0xB3}: + option = OUTPUT_CONFIGURATION_OPTIONS.get( + message[2], f"Unknown option 0x{message[2]:02X}" + ) + description += f" – {option}" + + payload = " ".join(f"{value:02X}" for value in message[2:-1]) + if payload: + description += f": {payload}" + return description + # -------------------------------------------------------------------------------------------------------------------- # def byte_parser(): @@ -78,6 +150,7 @@ def __init__(self, device, eitsetup=None, devicetype="FS"): self.iSaveCounter = 0 # Unused self.ppcData = [] self.iInjIndex = 0 + self.pending_command = None # Device setup self.cDevice = device @@ -100,6 +173,10 @@ def __init__(self, device, eitsetup=None, devicetype="FS"): self.setup = eitsetup self.set_measurement_setup(eitsetup) + def set_pending_command(self, command): + """Remember a command so its following ACK can be labelled.""" + self.pending_command = list(command) + # ---------------------------------------------------------------------------------------------------------------- # def set_measurement_setup(self, setup: EitMeasurementSetup): """ @@ -108,7 +185,7 @@ def set_measurement_setup(self, setup: EitMeasurementSetup): setup: EITMeasurementSetup object """ self.setup = setup - if setup != None: + if setup is not None: self.iMaxChannelGroups = setup.n_el // 16 self.iNumExcitationSettings = setup.n_el # todo should be independently set self.iNumFreqSettings = 1 # todo @@ -132,6 +209,10 @@ def reset_new_data_frame(self): """ Resets the Current EITFrame. """ + if self.setup is None: + raise RuntimeError( + "A measurement setup is required to create an EIT frame." + ) self.iInjIndex = 0 self.iSaveCounter = 0 self.CurrentFrame = EITFrame( @@ -153,15 +234,39 @@ def clear_out_data(self): Deletes saved data frames """ self.ppcData = [] - self.reset_new_data_frame() + if self.setup is None: + self.CurrentFrame = None + else: + self.reset_new_data_frame() # ---------------------------------------------------------------------------------------------------------------- # def init_parser(self): """ - Initializes the parser generator + Initializes the receive buffer used by the USB stream decoder. """ - self.Parser = byte_parser() - next(self.Parser) + self._receive_buffer = bytearray() + + def parse_received_bytes(self, data): + """Add a transport chunk and return all complete protocol messages.""" + if data: + self._receive_buffer.extend(data) + + messages = [] + while len(self._receive_buffer) >= 2: + payload_length = self._receive_buffer[1] + message_length = payload_length + 3 + if len(self._receive_buffer) < message_length: + break + + message = list(self._receive_buffer[:message_length]) + del self._receive_buffer[:message_length] + if message[-1] != message[0]: + raise ValueError( + f"Invalid message framing: starts with {hex(message[0])} " + f"and ends with {hex(message[-1])}." + ) + messages.append(message) + return messages # ---------------------------------------------------------------------------------------------------------------- # def read_fs(self): @@ -220,24 +325,21 @@ def read_usb_for_seconds( Returns: List of received data eit frames, no Status messages are saved """ - if bStartReset: + if bStartReset and self.setup is not None: 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: + messages = self.parse_received_bytes(buffer) + for message in messages: bMessageStarted = False self.interpret_message( message, bSaveData, bDeleteDataFrame, sSavePath ) - iMessageCount += 1 - else: + if not messages and self._receive_buffer: bMessageStarted = True timeout_count = 0 continue @@ -248,7 +350,6 @@ def read_usb_for_seconds( if timeout_count >= 100: # Break if we haven't received any data break - print(f"{iMessageCount} message(s) received.") return self.ppcData # ---------------------------------------------------------------------------------------------------------------- # @@ -270,19 +371,16 @@ def read_usb_till_timeout( Returns: List of received data eit frames, no Status messages are saved """ - if bStartReset: + if bStartReset and self.setup is not None: 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: + for message in self.parse_received_bytes(buffer): self.interpret_message( message, bSaveData, bDeleteDataFrame, sSavePath ) - iMessageCount += 1 timeout_count = 0 continue timeout_count += 1 @@ -290,7 +388,6 @@ def read_usb_till_timeout( # Break if we haven't received any data break - print(f"{iMessageCount} message(s) received.") return self.ppcData # ---------------------------------------------------------------------------------------------------------------- # @@ -309,17 +406,14 @@ def interpret_message( if message[0] == 180: # DATA 0XB4 self.interpret_data_input(message, bSaveData, bDeleteDataFrame, sSavePath) else: - mess_hex = [hex(receive) for receive in message] + description = describe_message(message) + if message[0] == 0x18 and self.pending_command is not None: + description = ( + f"{describe_message(self.pending_command)} -> {description}" + ) + self.pending_command = None 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)) + print(description) # ---------------------------------------------------------------------------------------------------------------- # def interpret_data_input( @@ -456,20 +550,19 @@ def load_eit_frames(path): 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) + files = sorted(file for file in os.listdir(path) if file.endswith(".npz")) + for filename in files: + with np.load(os.path.join(path, filename), allow_pickle=True) as data: + frame = EITFrame( + n_el=data["n_el"], + excitation_stgs=data["excitation_stgs"], + frequency_stgs=data["frequency_stgs"], + timestamp1=data["timestamp1"], + timestamp2=data["timestamp2"], + timestamp_pc=data["timestamp_pc"], + ppcData=data["ppcData"], + ) + loaded.append(frame) return loaded @@ -483,7 +576,7 @@ def load_eit_frames_into_nparray(path): Returns: np.array(ppcData) """ loaded = load_eit_frames(path) - l = [] + data = [] for frame in loaded: - l.append(frame.ppcData) - return np.array(l) + data.append(frame.ppcData) + return np.array(data) diff --git a/sciopy/visualization.py b/sciopy/visualization.py index c6e831e..b2cadc1 100644 --- a/sciopy/visualization.py +++ b/sciopy/visualization.py @@ -18,7 +18,13 @@ def norm_data(data: np.ndarray) -> np.ndarray: np.ndarray normalized data """ - return (data - np.min(data)) / (np.max(data) - np.min(data)) + data = np.asarray(data) + if data.size == 0: + raise ValueError("Cannot normalize an empty array.") + data_range = np.max(data) - np.min(data) + if data_range == 0: + return np.zeros_like(data, dtype=float) + return (data - np.min(data)) / data_range def plot_potential_matrix(sample: np.lib.npyio.NpzFile) -> None: diff --git a/setup.py b/setup.py index c9f3076..1d4056f 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,30 @@ -from setuptools import setup, find_packages +from pathlib import Path + +from setuptools import find_packages, setup + +ROOT = Path(__file__).parent setup( name="sciopy", - version="0.9.1", + version="1.0", packages=find_packages(), author="Jacob P. Thönes", author_email="jacob.thoenes@uni-rostock.de", description="Python based interface module for communication with Sciospec devices.", - long_description=open("README.md").read(), + long_description=(ROOT / "README.md").read_text(encoding="utf-8"), long_description_content_type="text/markdown", keywords="Sciospec EIT EIS".split(), platforms="any", + python_requires=">=3.9", + install_requires=[ + "numpy>=1.23", + "pyftdi>=0.54", + "pyserial>=3.5", + ], + extras_require={ + "plot": ["matplotlib>=3.6", "pyeit>=1.2"], + "test": ["matplotlib>=3.6", "pytest>=7", "ruff>=0.11"], + }, classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", diff --git a/tests/test_datatype_conversion.py b/tests/test_datatype_conversion.py new file mode 100644 index 0000000..8e5f2d1 --- /dev/null +++ b/tests/test_datatype_conversion.py @@ -0,0 +1,38 @@ +import struct + +import numpy as np + +from sciopy.datatype_conversion import ( + bytelist_to_int, + byteintarray_to_float, + bytesarray_to_double, + bytesarray_to_float, + del_hex_in_list, + four_byte_to_int, + two_byte_to_int, +) + + +def test_hex_list_is_normalized_to_two_digits(): + assert del_hex_in_list(["0x0", "0xa", "0xff", 1]).tolist() == [ + "00", + "0a", + "ff", + "01", + ] + + +def test_float_conversions_use_network_byte_order(): + float_bytes = struct.pack("!f", 12.5) + double_bytes = struct.pack("!d", -0.125) + + assert byteintarray_to_float(float_bytes) == 12.5 + assert bytesarray_to_float([f"{value:02x}" for value in float_bytes]) == 12.5 + assert bytesarray_to_double([f"{value:02x}" for value in double_bytes]) == -0.125 + + +def test_integer_conversions_include_every_byte(): + assert two_byte_to_int([0x12, 0x34]) == 0x1234 + assert four_byte_to_int([0x12, 0x34, 0x56, 0x78]) == 0x12345678 + assert bytelist_to_int([0x12, 0x34]) == 0x1234 + assert bytelist_to_int(np.array([0x12, 0x34, 0x56])) == 0x123456 diff --git a/tests/test_eit_system_message_callback.py b/tests/test_eit_system_message_callback.py new file mode 100644 index 0000000..ed0adde --- /dev/null +++ b/tests/test_eit_system_message_callback.py @@ -0,0 +1,79 @@ +from unittest.mock import Mock + +from sciopy.EIT_16_32_64_128 import EIT_16_32_64_128 + + +def test_system_message_callback_uses_configured_message_parser(): + device = EIT_16_32_64_128(16) + device.print_msg = True + device.cMessageParser = Mock() + device.cMessageParser.read_usb_till_timeout.return_value = [] + + result = device.SystemMessageCallback() + + assert result == [] + assert device.cMessageParser.bPrintMessages is True + device.cMessageParser.read_usb_till_timeout.assert_called_once_with( + bSaveData=False, + bDeleteDataFrame=True, + bStartReset=False, + ) + + +def test_get_device_info_does_not_require_measurement_setup(): + device = EIT_16_32_64_128(16) + device.serial_protocol = "HS" + device.device = Mock() + device.cMessageParser = Mock() + + device.GetDeviceInfo() + + device.device.write_data.assert_called_once_with(bytearray([0xD1, 0x00, 0xD1])) + device.cMessageParser.read_usb_till_timeout.assert_called_once_with( + bSaveData=False, + bDeleteDataFrame=True, + bStartReset=False, + ) + + +def test_read_message_uses_parser_transport_without_recursion(): + device = EIT_16_32_64_128(16) + device.cMessageParser = Mock() + device.cMessageParser.device_read.return_value = bytearray([0x18, 0x01, 0x83, 0x18]) + + assert device.read_message() == bytearray([0x18, 0x01, 0x83, 0x18]) + device.cMessageParser.device_read.assert_called_once_with() + + +def test_update_measurement_mode_builds_valid_command(): + device = EIT_16_32_64_128(16) + device.write_command_string = Mock() + + device.update_measurement_mode("skip4", "external") + + device.write_command_string.assert_called_once_with( + bytearray([0xB0, 0x03, 0x08, 0x04, 0x02, 0xB0]) + ) + + +def test_get_measurement_setup_builds_valid_command(): + device = EIT_16_32_64_128(16) + device.write_command_string = Mock() + + device.GetMeasurementSetup(0x02) + + device.write_command_string.assert_called_once_with( + bytearray([0xB1, 0x01, 0x02, 0xB1]) + ) + + +def test_measurement_requires_setup_before_parser_reset(): + device = EIT_16_32_64_128(16) + device.cMessageParser = Mock() + + try: + device.StartStopMeasurement() + except RuntimeError as error: + assert str(error) == "SetMeasurementSetup must be called before measuring." + else: + raise AssertionError("StartStopMeasurement accepted a missing setup") diff --git a/tests/test_file_and_visualization_helpers.py b/tests/test_file_and_visualization_helpers.py new file mode 100644 index 0000000..e4ebce8 --- /dev/null +++ b/tests/test_file_and_visualization_helpers.py @@ -0,0 +1,31 @@ +import pickle +from types import SimpleNamespace + +import numpy as np + +from sciopy.doteit import list_eit_files, load_pickle_to_dict +from sciopy.visualization import norm_data + + +def test_list_eit_files_is_filtered_case_insensitively_and_sorted(tmp_path): + (tmp_path / "b.eit").touch() + (tmp_path / "A.EIT").touch() + (tmp_path / "ignored.txt").touch() + + assert list_eit_files(tmp_path) == ["A.EIT", "b.eit"] + + +def test_load_pickle_to_dict_reads_pickled_objects(tmp_path): + path = tmp_path / "sample.pickle" + with path.open("wb") as file: + pickle.dump(SimpleNamespace(value=42), file) + + assert load_pickle_to_dict(path) == {"value": 42} + + +def test_norm_data_handles_constant_arrays(): + assert np.array_equal(norm_data(np.array([5.0, 5.0])), np.array([0.0, 0.0])) + + +def test_norm_data_scales_to_unit_interval(): + assert np.allclose(norm_data(np.array([2.0, 4.0, 6.0])), [0.0, 0.5, 1.0]) diff --git a/tests/test_isx_3.py b/tests/test_isx_3.py new file mode 100644 index 0000000..0d843ec --- /dev/null +++ b/tests/test_isx_3.py @@ -0,0 +1,245 @@ +from unittest.mock import Mock +import struct + +import numpy as np + +from sciopy.ISX_3 import ISXMeasurement, ISX_3 +from sciopy.sciopy_dataclasses import EisMeasurementSetup + + +def command_device(): + device = ISX_3() + device.send_command = Mock(return_value=[]) + return device + + +def test_frame_builder_uses_sciospec_framing(): + assert ISX_3.build_frame(0xB0, [0x02, 0x01, 0x00, 0x01]) == bytearray( + [0xB0, 0x04, 0x02, 0x01, 0x00, 0x01, 0xB0] + ) + + +def test_stream_parser_handles_partial_and_multiple_frames(): + device = ISX_3() + first = bytes([0x18, 0x01, 0x83, 0x18]) + second = bytes([0xBA, 0x04, 0x00, 0x00, 0x03, 0xE8, 0xBA]) + + assert device.parse_received_bytes(first[:2]) == [] + assert device.parse_received_bytes(first[2:] + second) == [ + list(first), + list(second), + ] + + +def test_set_and_get_frontend_settings_follow_manual(): + device = command_device() + + device.SetFE_Settings("4-point", "bnc", "auto", "1v") + device.GetFE_Settings() + + assert device.send_command.call_args_list == [ + ((0xB0, [0x02, 0x01, 0x00, 0x01]),), + ((0xB1,),), + ] + assert device.decode_frame([0xB1, 0x04, 0x02, 0x01, 0x04, 0x02, 0xB1]) == { + "measurement_mode": 0x02, + "measurement_channel": 0x01, + "current_range": 0x04, + "voltage_range": 0x02, + } + + +def test_options_update_measurement_layout_state(): + device = command_device() + + device.SetOptions("timestamp_us", True) + device.SetOptions("current_range", True) + + assert device.timestamp_mode == "us" + assert device.current_range_output is True + assert device.send_command.call_args_list == [ + ((0x97, [0x02, 0x01]),), + ((0x97, [0x04, 0x01]),), + ] + + +def test_add_frequency_point_encodes_float_and_extended_options(): + device = command_device() + + device.AddFrequencyPoint( + 32_000, + 1.0, + 0.25, + point_delay=1000, + phase_sync=True, + excitation_type="voltage", + ) + + payload = device.send_command.call_args.args[1] + assert device.send_command.call_args.args[0] == 0xB6 + assert payload[0] == 0x02 + assert struct.unpack(">fff", payload[1:13]) == (32_000.0, 1.0, 0.25) + assert payload[13:] == bytearray( + [0x01, 0x00, 0x00, 0x03, 0xE8, 0x02, 0, 0, 0, 1, 0x03, 0, 0, 0, 1] + ) + + +def test_add_frequency_list_encodes_documented_setup(): + device = command_device() + + device.AddFrequencyList(1_000, 10_000, 10, "log", 1.0, 0.25) + + tag, payload = device.send_command.call_args.args + assert tag == 0xB6 + assert payload[0] == 0x03 + assert struct.unpack(">fff", payload[1:13]) == (1_000.0, 10_000.0, 10.0) + assert payload[13] == 0x01 + assert struct.unpack(">ff", payload[14:22]) == (1.0, 0.25) + + +def test_eis_dataclass_configures_setup(): + device = command_device() + setup = EisMeasurementSetup( + start=100, + stop=100_000, + step=20, + stepmode="log", + avg=1, + amplitude=0.1, + precision=1, + measurement_time=0, + ) + + device.SetMeasurementSetup(setup) + + assert device.setup is setup + assert device.send_command.call_args_list[0] == ((0xB6, [0x01]),) + assert device.send_command.call_args_list[1].args[0] == 0xB6 + + +def test_measurement_without_optional_fields_is_decoded(): + device = ISX_3() + payload = [0x00, 0x02, *struct.pack(">ff", 123.5, -4.25)] + + result = device.decode_frame([0xB8, len(payload), *payload, 0xB8]) + + assert result == ISXMeasurement(frequency_id=2, impedance=123.5 - 4.25j) + + +def test_measurement_with_microsecond_timestamp_and_range_is_decoded(): + device = ISX_3() + device.timestamp_mode = "us" + device.current_range_output = True + payload = [0x00, 0x03, 0, 0, 0, 1, 2, 0x04, *struct.pack(">ff", 10, 2)] + + result = device.decode_frame([0xB8, len(payload), *payload, 0xB8]) + + assert result == ISXMeasurement( + frequency_id=3, + impedance=10 + 2j, + timestamp=258, + timestamp_unit="us", + current_range=0x04, + ) + + +def test_setup_response_decoders(): + device = ISX_3() + frequencies = struct.pack(">ff", 100.0, 1_000.0) + + assert device.decode_frame([0xB7, 0x03, 0x01, 0x00, 0x10, 0xB7]) == { + "frequency_count": 16 + } + result = device.decode_frame([0xB7, 0x09, 0x04, *frequencies, 0xB7]) + assert np.allclose(result["frequencies_hz"], [100, 1_000]) + + +def test_frequency_point_response_decodes_extended_options(): + device = ISX_3() + data = [ + 0x02, + *struct.pack(">fff", 32_000, 1.0, 0.25), + 0x01, + 0, + 0, + 3, + 0xE8, + 0x03, + 0, + 0, + 0, + 1, + ] + + result = device.decode_frame([0xB7, len(data), *data, 0xB7]) + + assert result == { + "frequency_hz": 32_000.0, + "precision": 1.0, + "amplitude": 0.25, + "point_delay_us": 1_000, + "excitation_type": 1, + } + + +def test_compensation_record_encodes_three_complex_values(): + device = command_device() + + device.SetCompensationData(2, 3, 1 + 2j, 3 + 4j, 5 + 6j) + + tag, payload = device.send_command.call_args.args + assert tag == 0xB6 + assert payload[:5] == bytearray([0x17, 0x02, 0x02, 0x00, 0x03]) + assert struct.unpack(">ffffff", payload[5:]) == (1, 2, 3, 4, 5, 6) + + +def test_sync_ethernet_and_watchdog_commands(): + device = command_device() + + device.SetSyncTime(1_000) + device.SetEthernetConfiguration(ip_address="192.168.1.25") + device.SetEthernetConfiguration(dhcp=True) + device.GetEthernetConfiguration("mac_address") + device.SetTCPWatchdog(60) + + assert device.send_command.call_args_list == [ + ((0xB9, bytes([0, 0, 3, 0xE8])),), + ((0xBD, [0x01, 192, 168, 1, 25]),), + ((0xBD, [0x03, 1]),), + ((0xBE, [0x02]),), + ((0xCF, [0x00, 0, 0, 0, 60]),), + ] + + +def test_firmware_and_device_information_are_decoded(): + device = ISX_3() + + arm = device.decode_frame([0xD0, 0x06, 1, 2, 0, 3, 0, 4, 0xD0]) + identity = device.decode_frame([0xD1, 0x07, 1, 0, 7, 0x12, 0x34, 14, 9, 0xD1]) + + assert arm == {"developer_information": "0102", "revision": 3, "build": 4} + assert identity == { + "information_version": 1, + "device_identifier": 7, + "serial_number": 0x1234, + "delivery_year": 2024, + "delivery_month": 9, + "developer_information": "", + } + + +def test_invalid_protocol_values_are_rejected(): + device = command_device() + + for action in ( + lambda: device.SetFE_Settings("invalid", "bnc", "auto"), + lambda: device.SetSyncTime(180_000_001), + lambda: device.SetTCPWatchdog(0), + lambda: device.SetDCBiasValue(1.1), + ): + try: + action() + except ValueError: + pass + else: + raise AssertionError("invalid protocol value was accepted") diff --git a/tests/test_usb_message_parser.py b/tests/test_usb_message_parser.py new file mode 100644 index 0000000..afb9578 --- /dev/null +++ b/tests/test_usb_message_parser.py @@ -0,0 +1,112 @@ +from unittest.mock import Mock + +import numpy as np + +from sciopy.sciopy_dataclasses import EITFrame +from sciopy.usb_message_parser import ( + MessageParser, + describe_message, + load_eit_frames, + save_data_frame, +) + + +def test_parser_accepts_a_message_split_across_reads(): + parser = MessageParser(Mock(), devicetype="FS") + + assert parser.parse_received_bytes(bytes([0x18, 0x01])) == [] + assert parser.parse_received_bytes(bytes([0x83, 0x18])) == [ + [0x18, 0x01, 0x83, 0x18] + ] + + +def test_parser_extracts_multiple_messages_from_one_hs_block(): + parser = MessageParser(Mock(), devicetype="HS") + block = bytes([0x18, 0x01, 0x83, 0x18, 0x18, 0x01, 0x84, 0x18]) + + assert parser.parse_received_bytes(block) == [ + [0x18, 0x01, 0x83, 0x18], + [0x18, 0x01, 0x84, 0x18], + ] + + +def test_parser_rejects_mismatched_message_tags(): + parser = MessageParser(Mock(), devicetype="FS") + + try: + parser.parse_received_bytes(bytes([0x18, 0x01, 0x83, 0x19])) + except ValueError as error: + assert "Invalid message framing" in str(error) + else: + raise AssertionError("Parser accepted mismatched message tags") + + +def test_status_read_does_not_require_measurement_setup(): + device = Mock() + device.read.side_effect = [ + bytes([0x18]), + bytes([0x01]), + bytes([0x83]), + bytes([0x18]), + b"", + ] + parser = MessageParser(device, devicetype="FS") + + assert parser.read_usb_till_timeout() == [] + + +def test_manual_command_information_describes_acknowledgement(): + assert describe_message([0x18, 0x01, 0x83, 0x18]) == ( + "Acknowledge / General System Message (0x18): " + "Command-Acknowledge: Command has been executed successfully" + ) + + +def test_manual_command_information_describes_setup_option(): + assert describe_message([0xB1, 0x03, 0x02, 0x00, 0x04, 0xB1]) == ( + "Get Measurement Setup (0xB1) – Burst Count: 02 00 04" + ) + + +def test_parser_prints_command_information_instead_of_message_count(capsys): + parser = MessageParser(Mock(), devicetype="FS") + parser.bPrintMessages = True + + parser.interpret_message([0xD1, 0x02, 0x01, 0x19, 0xD1]) + + output = capsys.readouterr().out + assert output == "Get Device Info (0xD1): 01 19\n" + assert "message(s) received" not in output + + +def test_acknowledgement_is_labelled_with_its_pending_command(capsys): + parser = MessageParser(Mock(), devicetype="FS") + parser.bPrintMessages = True + parser.set_pending_command([0xB0, 0x03, 0x02, 0x00, 0x04, 0xB0]) + + parser.interpret_message([0x18, 0x01, 0x83, 0x18]) + + output = capsys.readouterr().out + assert "Set Measurement Setup (0xB0) – Burst Count" in output + assert "Command-Acknowledge: Command has been executed successfully" in output + assert parser.pending_command is None + + +def test_saved_eit_frame_round_trips_and_ignores_other_files(tmp_path): + frame = EITFrame( + n_el=16, + excitation_stgs=np.array([[1, 2]]), + frequency_stgs=np.array([125_000]), + timestamp1=1.0, + timestamp2=2.0, + timestamp_pc=3.0, + ppcData=np.array([1 + 2j]), + ) + save_data_frame(f"{tmp_path}/", frame, 1) + (tmp_path / "notes.txt").write_text("ignored", encoding="utf-8") + + loaded = load_eit_frames(tmp_path) + + assert len(loaded) == 1 + assert loaded[0].n_el == 16 + assert np.array_equal(loaded[0].ppcData, frame.ppcData)