From cad317f084efc67e5005e49abfbd5317ac5d1759 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 9 Jan 2025 12:23:20 +0100 Subject: [PATCH 01/36] updated setup for dev install --- .gitignore | 1 + setup.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 9142a12..e534a22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ __pycache__/ manuals +error_device_logs build/ dist/ .update_package.sh.swp diff --git a/setup.py b/setup.py index 8185efe..63851ce 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,9 @@ -import setuptools +from setuptools import setup, find_packages -setuptools.setup( +setup( name="sciopy", - version="0.8.0", + version="0.8.1", + packages=find_packages(), author="Jacob Peter Thönes", author_email="jacob.thoenes@uni-rostock.de", description="Python based interface module for communication with the Sciospec Electrical Impedance Tomography (EIT) device.", From ba90c7a34f9d338283d809bc45c8f8fb6c9a00a3 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Wed, 22 Jan 2025 13:57:20 +0100 Subject: [PATCH 02/36] return class data --- sciopy/EIT_16_32_64_128.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index 213050b..42122a9 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -393,7 +393,7 @@ def StartStopMeasurement(self, return_as="pot_mat"): self.data = data if return_as == "hex": - return data + return self.data elif return_as == "pot_mat": return self.get_data_as_matrix() From b6fc7870f7520a811f73029edb2d0db0c0ed8153 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Mon, 7 Jul 2025 09:00:29 +0200 Subject: [PATCH 03/36] close HS port --- sciopy/EIT_16_32_64_128.py | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index 42122a9..7329359 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -97,6 +97,12 @@ def connect_device_FS(self, port: str, baudrate: int = 9600, timeout: int = 1): print("Connection to", self.device.name, "is established.") + def disconnect_device(self): + """ + Disconnect serial device + """ + self.device.close() + def SystemMessageCallback_usb_fs(self): """ !Only used if a full-speed connection is established! diff --git a/setup.py b/setup.py index 63851ce..f250a9b 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="sciopy", - version="0.8.1", + version="0.8.2", packages=find_packages(), author="Jacob Peter Thönes", author_email="jacob.thoenes@uni-rostock.de", From 359a983b729df3405234d2059e913fbe5eec5d9a Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Wed, 15 Oct 2025 15:17:57 +0200 Subject: [PATCH 04/36] renamed for EIT --- examples/{example_notebook.ipynb => EIT-16-256-Ch.ipynb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{example_notebook.ipynb => EIT-16-256-Ch.ipynb} (100%) diff --git a/examples/example_notebook.ipynb b/examples/EIT-16-256-Ch.ipynb similarity index 100% rename from examples/example_notebook.ipynb rename to examples/EIT-16-256-Ch.ipynb From c9dc1faaa43590aa2801d720773b2d62a5d06329 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Wed, 15 Oct 2025 15:18:11 +0200 Subject: [PATCH 05/36] Started EIS package implementation --- sciopy/ISX_3.py | 130 ++++++++++++++++++++++++++++++----- sciopy/__init__.py | 9 ++- sciopy/sciopy_dataclasses.py | 43 +++++++++++- 3 files changed, 162 insertions(+), 20 deletions(-) diff --git a/sciopy/ISX_3.py b/sciopy/ISX_3.py index 2bc7a34..0c9fc8e 100644 --- a/sciopy/ISX_3.py +++ b/sciopy/ISX_3.py @@ -5,10 +5,30 @@ from dataclasses import dataclass - -@dataclass -class EisMeasurementSetup: - pass +from sciopy.sciopy_dataclasses import EisMeasurementSetup + +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", +} + +frame_status_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", +} class ISX_3: @@ -16,9 +36,9 @@ def __init__(self, n_el) -> None: # number of electrodes used self.n_el = n_el - def connect_device_FS(self, port: str, baudrate: int = 9600, timeout: int = 1): + def connect_device_USB2(self, port: str, baudrate: int = 9600, timeout: int = 1): """ - Connect to full speed + Connect to USB 2.0 Type B """ if hasattr(self, "serial_protocol"): print( @@ -45,16 +65,66 @@ def GetOptions(self): pass def ResetSystem(self): - # 0xA1 - pass + self.print_msg = True + self.write_command_string(bytearray([0xA1, 0x00, 0xA1])) + self.print_msg = False - def SetFE_Settings(self): - # 0xB0 - pass + def SetFE_Settings(self, PP, CH, RA): + """ + 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 + """ + self.print_msg = True + self.write_command_string(bytearray([0xB0, PP, CH, RA, 0xB0])) + self.print_msg = False def GetFE_Settings(self): - # 0xB1 - pass + """ + Retrieves the frontend measurement settings from the device. + + Returns: + dict: Dictionary containing PP (Measurement mode), CH (Measurement channel), RA (Range setting). + """ + 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 @@ -100,8 +170,32 @@ def GetExtensionPortChannel(self): # 0xD3 pass - -# 0xBD - Set Ethernet Configuration -# 0xBE - Get Ethernet Configuration -# 0xCF - TCP connection watchdog -# 0xD0 - Get ARM firmware ID + 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 diff --git a/sciopy/__init__.py b/sciopy/__init__.py index 4699715..2ef1da6 100644 --- a/sciopy/__init__.py +++ b/sciopy/__init__.py @@ -3,6 +3,13 @@ ) from .EIT_16_32_64_128 import EIT_16_32_64_128, EitMeasurementSetup +from .ISX_3 import ISX_3, EisMeasurementSetup -__all__ = ["available_serial_ports", "EIT_16_32_64_128", "EitMeasurementSetup"] +__all__ = [ + "available_serial_ports", + "EIT_16_32_64_128", + "EitMeasurementSetup", + "ISX_3", + "EisMeasurementSetup", +] diff --git a/sciopy/sciopy_dataclasses.py b/sciopy/sciopy_dataclasses.py index c28e14b..e42ece4 100644 --- a/sciopy/sciopy_dataclasses.py +++ b/sciopy/sciopy_dataclasses.py @@ -4,6 +4,20 @@ @dataclass class EitMeasurementSetup: + """ + Represents the setup parameters for Electrical Impedance Tomography (EIT) measurements. + + Attributes: + burst_count (int): Number of bursts per measurement cycle. + n_el (int): Number of electrodes used in the measurement. + exc_freq (int or float): Excitation frequency in Hz. + framerate (int or float): Frame rate of the measurement in Hz. + amplitude (int or float): Amplitude of the excitation signal. + inj_skip (int or list): Electrode(s) to skip during current injection. + gain (int): Amplifier gain setting. + adc_range (int): Analog-to-digital converter range setting. + """ + burst_count: int n_el: int exc_freq: Union[int, float] @@ -17,7 +31,34 @@ class EitMeasurementSetup: @dataclass class EisMeasurementSetup: - pass + """ + Represents the setup parameters for an Electrochemical Impedance Spectroscopy (EIS) measurement. + + Attributes: + start (int | float): Start frequency in Hz (e.g., 500000 for 500kHz). + stop (int | float): Stop frequency in Hz. + step (int | float): Number of frequency steps. + stepmode (str): Type of frequency distribution over the interval ('lin' for linear, 'log' for logarithmic). + AVG (int | float): Number of averages taken per measurement. + Amplitude (int | float): Amplitude of the excitation signal in mV. + Precision (int): Desired precision configuration (≥0): + - 1: Standard configuration (max relative deviation < 0.1%) + - <1: Faster measurement, less precise + - >1: More precise, slower measurement + MeasurementTime (int | float): Duration of the measurement in seconds. + + Note: + Additional parameters may include measurement channel settings and other hardware-specific configurations. + """ + + start: Union[int, float] + stop: Union[int, float] + step: Union[int, float] + stepmode: str # 'lin', 'log' + AVG: Union[int, float] + Amplitude: Union[int, float] + Precision: int + MeasurementTime: Union[int, float] @dataclass From 7ad1f9021d7144d33361c3246ffa9d31818809f7 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Wed, 15 Oct 2025 15:20:10 +0200 Subject: [PATCH 06/36] v and readme --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 28e7b50..c2ab352 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Bildbeschreibung +Bildbeschreibung 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. diff --git a/setup.py b/setup.py index f250a9b..bbd1d34 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="sciopy", - version="0.8.2", + version="0.8.9", packages=find_packages(), author="Jacob Peter Thönes", author_email="jacob.thoenes@uni-rostock.de", From dccba4577fa1af846716daa7610d689c39133aab Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 09:10:00 +0200 Subject: [PATCH 07/36] docstrings --- sciopy/EIT_16_32_64_128.py | 251 ++++++++++++++++++++++++++++++++++--- 1 file changed, 237 insertions(+), 14 deletions(-) diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index 7329359..85800d2 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -48,7 +48,19 @@ def __init__(self, n_el: int) -> None: self.ret_hex_int = None def init_channel_group(self): - if self.n_el in [16, 32, 48, 64]: + """ + Initializes and returns a list representing the channel group based on the number of electrodes. + Returns: + list: A list of channel group indices, where each index corresponds to a channel. + The number of channels is determined by dividing `self.n_el` by 16. + Raises: + ValueError: If `self.n_el` is not one of the allowed values (16, 32, 48, 64, or 128). + Notes: + - Allowed values for `self.n_el` are 16, 32, 48, 64, and 128. + - The returned list contains consecutive integers starting from 1 up to `self.n_el // 16`. + """ + + if self.n_el in [16, 32, 48, 64, 128]: return [ch + 1 for ch in range(self.n_el // 16)] else: raise ValueError( @@ -57,7 +69,22 @@ def init_channel_group(self): def connect_device_HS(self, url: str = "ftdi://ftdi:232h/1", baudrate: int = 9000): """ - Connect to high speed + Establishes a high-speed serial connection to an FTDI device. + + This method initializes the FTDI device using the specified URL and baudrate, + configures the device for synchronous FIFO mode, and sets up the serial protocol. + If a serial connection is already defined, it notifies the user. + + Args: + url (str): The FTDI device URL. Defaults to "ftdi://ftdi:232h/1". + baudrate (int): The baud rate for the serial connection. Defaults to 9000. + + Side Effects: + Sets the `self.serial_protocol` attribute to "HS" if not already defined. + Initializes and configures the FTDI device, assigning it to `self.device`. + + Raises: + Any exceptions raised by the FTDI library during device initialization or configuration. """ if hasattr(self, "serial_protocol"): print( @@ -78,7 +105,18 @@ def connect_device_HS(self, url: str = "ftdi://ftdi:232h/1", baudrate: int = 900 def connect_device_FS(self, port: str, baudrate: int = 9600, timeout: int = 1): """ - Connect to full speed + Establishes a serial connection to a device using the FS protocol. + + Parameters: + port (str): The serial port to connect to (e.g., 'COM3' or '/dev/ttyUSB0'). + baudrate (int, optional): The baud rate for the serial connection. Defaults to 9600. + timeout (int, optional): The timeout value for the serial connection in seconds. Defaults to 1. + + Notes: + - If a serial connection is already defined in 'self.serial_protocol', a message is printed. + - Sets 'self.serial_protocol' to "FS" if not already defined. + - Initializes 'self.device' as a serial.Serial object with specified parameters. + - Prints a confirmation message upon successful connection. """ if hasattr(self, "serial_protocol"): print( @@ -99,15 +137,27 @@ def connect_device_FS(self, port: str, baudrate: int = 9600, timeout: int = 1): def disconnect_device(self): """ - Disconnect serial device + Disconnects the currently connected device by closing its connection. + + This method should be called to safely terminate communication with the device. """ self.device.close() def SystemMessageCallback_usb_fs(self): """ - !Only used if a full-speed connection is established! + Reads data from a USB device, processes received messages, and returns the data in the specified format. - Reads the message buffer of a serial connection. Also prints out the general system message. + 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. """ timeout_count = 0 received = [] @@ -151,11 +201,21 @@ def SystemMessageCallback_usb_fs(self): def SystemMessageCallback_usb_hs(self): """ - !Only used if a high-speed connection is established! + Reads data from a USB high-speed device, processes received messages, and returns the data in various formats. - Reads the message buffer of a serial connection. Also prints out the general system message. - """ + 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 = [] @@ -198,7 +258,15 @@ def SystemMessageCallback_usb_hs(self): def SystemMessageCallback(self): """ - SystemMessageCallback + Handles system messages based on the selected serial protocol. + + 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()`. + + Raises: + AttributeError: If the required callback methods are not defined. """ if self.serial_protocol == "HS": self.SystemMessageCallback_usb_hs() @@ -207,7 +275,17 @@ def SystemMessageCallback(self): def write_command_string(self, command): """ - Function for writing a command 'bytearray(...)' to the serial port + Sends a command string to the device using the appropriate serial protocol. + + Depending on the value of `self.serial_protocol`, the command is sent using either + high-speed ("HS") or full-speed ("FS") protocol. After sending the command, the + system message callback is triggered. + + Args: + command (str): The command string to be sent to the device. + + Raises: + AttributeError: If `self.device` does not have the required method for the selected protocol. """ if self.serial_protocol == "HS": self.device.write_data(command) @@ -218,12 +296,29 @@ def write_command_string(self, command): # --- sciospec device commands def SoftwareReset(self): + """ + Performs a software reset of the device. + + This method sends a specific command sequence to the device to initiate a software reset. + """ self.print_msg = True self.write_command_string(bytearray([0xA1, 0x00, 0xA1])) self.print_msg = False def update_BurstCount(self, burst_count): + """ + Updates the burst count setting and sends the corresponding command to the device. + + Args: + burst_count (int): The new burst count value to set. + + Side Effects: + - Sets `self.setup.burst_count` to the provided value. + - Sends a command to the device using `write_command_string`. + - Temporarily sets `self.print_msg` to True during the operation. self.print_msg = True + + """ self.setup.burst_count = burst_count self.write_command_string( bytearray([0xB0, 0x03, 0x02, 0x00, self.setup.burst_count, 0xB0]) @@ -231,6 +326,20 @@ def update_BurstCount(self, burst_count): self.print_msg = False def update_FrameRate(self, framerate): + """ + Updates the frame rate setting for the device and sends the corresponding command. + + Args: + framerate (int): The desired frame rate to set. + + Side Effects: + - Sets `self.setup.framerate` to the provided value. + - Sends a command to the device to update the frame rate. + - Temporarily sets `self.print_msg` to True during the operation. + + Note: + The command sent is constructed using the `clTbt_sp` function and numpy's `concatenate`. + """ self.print_msg = True self.setup.framerate = framerate self.write_command_string( @@ -244,7 +353,29 @@ def update_FrameRate(self, framerate): def SetMeasurementSetup(self, setup: EitMeasurementSetup): """ - set_measurement_config sets the ScioSpec device configuration depending on the EitMeasurementSetup configuration dataclass. + Configures the ScioSpec device measurement setup according to the provided EitMeasurementSetup dataclass. + + This method sets various device parameters including burst count, excitation amplitude, ADC range, gain, + single-ended mode, excitation switch type, framerate, excitation frequencies, and electrode injection configuration. + It also enables output configuration options such as excitation setting, frequency stack row, and timestamp. + + Parameters + ---------- + setup : EitMeasurementSetup + The measurement setup configuration containing parameters such as burst count, amplitude, ADC range, gain, + framerate, excitation frequency, number of electrodes, and injection skip. + + Raises + ------ + AssertionError + If the number of electrodes in the setup does not match the device initialization. + + Notes + ----- + - Amplitude is limited to a maximum of 10mA. + - ADC range and gain are set according to predefined device commands. + - Electrode injection configuration is set for all electrodes based on the provided setup. + - Output configuration is enabled for excitation, frequency stack, and timestamp. """ self.setup = setup @@ -346,14 +477,40 @@ def SaveSettings(self): self.print_msg = False def ResetMeasurementSetup(self): + """ + Resets the measurement setup by sending a specific command to the device. + + This method sets the `print_msg` flag to True, sends a reset command via + `write_command_string`, and then sets `print_msg` back to False. + + The command sent is a bytearray: [0xB0, 0x01, 0x01, 0xB0]. + + Returns: + None + """ self.print_msg = True self.write_command_string(bytearray([0xB0, 0x01, 0x01, 0xB0])) self.print_msg = False def GetMeasurementSetup(self, setup_of: str): """ - GetMeasurementSetup - + Retrieves and configures the measurement setup for the device based on the specified setup option. + + 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 @@ -371,6 +528,21 @@ def GetMeasurementSetup(self, setup_of: str): self.print_msg = False def StartStopMeasurement(self, return_as="pot_mat"): + """ + 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`. + + 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`. + """ if self.serial_protocol == "HS": self.device.write_data(bytearray([0xB4, 0x01, 0x01, 0xB4])) self.ret_hex_int = "hex" @@ -404,6 +576,22 @@ def StartStopMeasurement(self, return_as="pot_mat"): return self.get_data_as_matrix() def get_data_as_matrix(self): + """ + 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. + + Returns: + np.ndarray: A 3D complex-valued matrix containing the electrode potentials + for each burst and channel group. + """ pot_matrix = np.empty( (self.setup.burst_count, self.n_el, self.n_el), dtype=complex ) @@ -442,16 +630,51 @@ def GetOutputConfiguration(self): self.print_msg = False def GetDeviceInfo(self): + """ + Retrieves device information by sending a specific command to the device. + + This method sets the print_msg flag to True, sends the device info command + using `write_command_string`, and then resets the print_msg flag to False. + + Returns: + None + """ self.print_msg = True self.write_command_string(bytearray([0xD1, 0x00, 0xD1])) self.print_msg = False def GetFirmwareIDs(self): + """ + Sends a command to retrieve firmware IDs from the device. + + This method sets the print_msg flag to True, sends a specific command + to the device to request firmware identification, and then resets the + print_msg flag to False. + + Returns: + None + """ self.print_msg = True self.write_command_string(bytearray([0xD2, 0x00, 0xD2])) self.print_msg = False def PowerPlugDetect(self): + """ + Detects the presence of a power plug by sending a specific command to the device. + + This method sets the print_msg flag to True, sends a command to check for power plug detection, + and then resets the print_msg flag to False. + + Command sent: + - [0xCC, 0x01, 0x81, 0xCC]: Power plug detection command. + + Note: + The method does not return any value. It is assumed that the result of the detection + is handled elsewhere in the class or by a callback. + + Side Effects: + Modifies the self.print_msg attribute. + """ self.print_msg = True self.write_command_string(bytearray([0xCC, 0x01, 0x81, 0xCC])) self.print_msg = False From f16c568246be037928afcecc0bc8e2d656d09e07 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 09:10:12 +0200 Subject: [PATCH 08/36] ISX-3 start --- examples/ISX-3.ipynb | 145 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 examples/ISX-3.ipynb diff --git a/examples/ISX-3.ipynb b/examples/ISX-3.ipynb new file mode 100644 index 0000000..e91d9f8 --- /dev/null +++ b/examples/ISX-3.ipynb @@ -0,0 +1,145 @@ +{ + "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": "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 +} From cdf1e983343cf3eb66a7d12a116eca8966c23626 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 09:10:28 +0200 Subject: [PATCH 09/36] ISX-3 impl start --- sciopy/ISX_3.py | 136 ++++++++++++++++++++++++++++------- sciopy/sciopy_dataclasses.py | 12 ++-- 2 files changed, 116 insertions(+), 32 deletions(-) diff --git a/sciopy/ISX_3.py b/sciopy/ISX_3.py index 0c9fc8e..973d06b 100644 --- a/sciopy/ISX_3.py +++ b/sciopy/ISX_3.py @@ -5,7 +5,20 @@ from dataclasses import dataclass -from sciopy.sciopy_dataclasses import EisMeasurementSetup +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", +} error_msg_dict = { "0x01": "init setup failed", @@ -20,7 +33,8 @@ "0x22": "set amplitude failed", } -frame_status_dict = { + +acknowledge_msg_dict = { "0x01": "Frame-Not-Acknowledge: Incorrect syntax", "0x02": "Timeout: Communication-timeout (less data than expected)", "0x04": "Wake-Up Message: System boot ready", @@ -32,29 +46,108 @@ class ISX_3: - def __init__(self, n_el) -> None: - # number of electrodes used - self.n_el = n_el + def __init__(self) -> None: + self.print_msg = True + self.ret_hex_int = None def connect_device_USB2(self, port: str, baudrate: int = 9600, timeout: int = 1): """ Connect to USB 2.0 Type B """ - if hasattr(self, "serial_protocol"): + if hasattr(self, "USB-FS"): print( - "Serial connection 'self.serial_protocol' already defined as {self.serial_protocol}." + f"Serial connection 'self.serial_protocol' already defined as {self.serial_protocol}." ) else: - self.serial_protocol = "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.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.") + + def disconnect_device_USB2(self): + self.device.close() + print("Connection to", self.device.name, "is closed.") + + def SystemMessageCallback(self): + """ + Reads the message buffer of a serial connection. Also prints out the general system message. + """ + timeout_count = 0 + received = [] + received_hex = [] + data_count = 0 + + while True: + buffer = self.device.read() + if buffer: + received.extend(buffer) + data_count += len(buffer) + timeout_count = 0 + continue + timeout_count += 1 + if timeout_count >= 1: + # Break if we haven't received any data + break + + 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): + """ + Function for writing a command 'bytearray(...)' to the serial port + """ + self.device.write(command) + self.SystemMessageCallback() + + def ResetSystem(self): + self.print_msg = True + self.write_command_string(bytearray([0xA1, 0x00, 0xA1])) + self.print_msg = False + + def SetMeasurementSetup(self, setup: EisMeasurementSetup): + """ + Configures the measurement setup for the device. + + Parameters + ---------- + setup : EisMeasurementSetup + An instance of EisMeasurementSetup containing the measurement parameters. + """ + + self.print_msg = True + # self.write_command_string(command) + self.print_msg = False + + def StartMeasure(self): + self.print_msg = True + self.write_command_string(bytearray([0xB8, 0x01, 0x01, 0x01, 0xB8])) + self.print_msg = False def SetOptions(self): # 0x97 @@ -64,11 +157,6 @@ def GetOptions(self): # 0x98 pass - def ResetSystem(self): - self.print_msg = True - self.write_command_string(bytearray([0xA1, 0x00, 0xA1])) - self.print_msg = False - def SetFE_Settings(self, PP, CH, RA): """ Configures the frontend measurement settings for the device. @@ -146,10 +234,6 @@ def SetSetup(self): # 0xB7 pass - def StartMeasure(self): - # 0xB8 - pass - def SetSyncTime(self): # 0xB9 pass diff --git a/sciopy/sciopy_dataclasses.py b/sciopy/sciopy_dataclasses.py index e42ece4..a8ed977 100644 --- a/sciopy/sciopy_dataclasses.py +++ b/sciopy/sciopy_dataclasses.py @@ -51,14 +51,14 @@ class EisMeasurementSetup: Additional parameters may include measurement channel settings and other hardware-specific configurations. """ - start: Union[int, float] - stop: Union[int, float] + start: Union[int, float] # min 100mHz + stop: Union[int, float] # max 10MHz step: Union[int, float] stepmode: str # 'lin', 'log' - AVG: Union[int, float] - Amplitude: Union[int, float] - Precision: int - MeasurementTime: Union[int, float] + avg: Union[int, float] + amplitude: Union[int, float] + precision: int + measurement_time: Union[int, float] @dataclass From 6c918645727791c44cbd52a8883183bf79ae1250 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 09:10:40 +0200 Subject: [PATCH 10/36] ign developements --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e534a22..d7d7d65 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ examples/measuremet_16/* examples/measuremet_32/* sciopy/eth_* Driver +ISX3-dev.ipynb From f24159f5a069c7517d553cb5dc1d930ba2737df6 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 09:45:52 +0200 Subject: [PATCH 11/36] moved image --- README.md | 2 +- doc/images/logo_sciopy.jpg | Bin 56378 -> 0 bytes setup.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 doc/images/logo_sciopy.jpg diff --git a/README.md b/README.md index c2ab352..64a9ee2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Bildbeschreibung +Sciopy-logo 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. diff --git a/doc/images/logo_sciopy.jpg b/doc/images/logo_sciopy.jpg deleted file mode 100644 index 5209c0864a1d07794271028b6bb37bfc232d5409..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56378 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%0Dq)OXBeD~_4 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 DTA+fk diff --git a/setup.py b/setup.py index bbd1d34..2e94b6d 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ name="sciopy", version="0.8.9", packages=find_packages(), - author="Jacob Peter Thönes", + author="Jacob P. Thönes", author_email="jacob.thoenes@uni-rostock.de", description="Python based interface module for communication with the Sciospec Electrical Impedance Tomography (EIT) device.", long_description=open("README.md").read(), From f3381b47b35f308737156401729ccff4a0b70773 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 10:06:00 +0200 Subject: [PATCH 12/36] docu and changes --- sciopy/EIT_16_32_64_128.py | 6 ++++++ sciopy/ISX_3.py | 6 ++++++ sciopy/__init__.py | 2 ++ sciopy/com_util.py | 2 ++ sciopy/doteit.py | 2 +- sciopy/meshing.py | 2 ++ sciopy/sciopy_dataclasses.py | 2 ++ sciopy/visualization.py | 24 +++++++++++++++++++----- 8 files changed, 40 insertions(+), 6 deletions(-) diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index 85800d2..fc4d0f7 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -1,3 +1,5 @@ +"""Module for interfacing with the Sciospec EIT devices via serial communication""" + try: import serial except ImportError: @@ -32,6 +34,10 @@ class EIT_16_32_64_128: + """ + A class for interfacing with the Sciospec EIT 16/32/64/128 devices. + """ + def __init__(self, n_el: int) -> None: """ __init__ diff --git a/sciopy/ISX_3.py b/sciopy/ISX_3.py index 973d06b..cc4f3f9 100644 --- a/sciopy/ISX_3.py +++ b/sciopy/ISX_3.py @@ -1,3 +1,5 @@ +"""Module for interfacing with the Sciospec ISX-3 EIT device via serial communication""" + try: import serial except ImportError: @@ -46,6 +48,10 @@ class ISX_3: + """ + A class for interfacing with the Sciospec ISX-3 EIT device. + """ + def __init__(self) -> None: self.print_msg = True self.ret_hex_int = None diff --git a/sciopy/__init__.py b/sciopy/__init__.py index 2ef1da6..1bda5a2 100644 --- a/sciopy/__init__.py +++ b/sciopy/__init__.py @@ -1,3 +1,5 @@ +"""A Python package for Sciospec device communication""" + from .com_util import ( available_serial_ports, ) diff --git a/sciopy/com_util.py b/sciopy/com_util.py index 0664ceb..0477c19 100644 --- a/sciopy/com_util.py +++ b/sciopy/com_util.py @@ -1,3 +1,5 @@ +"""Serial data handling""" + from typing import Union try: diff --git a/sciopy/doteit.py b/sciopy/doteit.py index d74464a..021c4c3 100644 --- a/sciopy/doteit.py +++ b/sciopy/doteit.py @@ -1,4 +1,4 @@ -""" Convert a .eit file to python sctructured data""" +"""Convert a .eit file to python sctructured data""" import os import numpy as np diff --git a/sciopy/meshing.py b/sciopy/meshing.py index bb31fc1..fcebaf4 100644 --- a/sciopy/meshing.py +++ b/sciopy/meshing.py @@ -1,3 +1,5 @@ +"""*WIP* module for mesh generation and plotting""" + from typing import Union import matplotlib.pyplot as plt import numpy as np diff --git a/sciopy/sciopy_dataclasses.py b/sciopy/sciopy_dataclasses.py index a8ed977..ee2c079 100644 --- a/sciopy/sciopy_dataclasses.py +++ b/sciopy/sciopy_dataclasses.py @@ -1,3 +1,5 @@ +"""Dataclasses for sciopy package.""" + from dataclasses import dataclass from typing import List, Tuple, Union diff --git a/sciopy/visualization.py b/sciopy/visualization.py index 11c4ed1..c6e831e 100644 --- a/sciopy/visualization.py +++ b/sciopy/visualization.py @@ -1,10 +1,24 @@ +"""Visualize recorded samples""" + import matplotlib.pyplot as plt import numpy as np -import math -import os -from typing import Tuple -from .prepare_data import comp_tank_relative_r_phi -from sciopy.prepare_data import norm_data + + +def norm_data(data: np.ndarray) -> np.ndarray: + """ + Normalize data between 0 and 1. + + Parameters + ---------- + data : np.ndarray + data to normalize + + Returns + ------- + np.ndarray + normalized data + """ + return (data - np.min(data)) / (np.max(data) - np.min(data)) def plot_potential_matrix(sample: np.lib.npyio.NpzFile) -> None: From 5c24452949adb49af0299815ade847e483fd3d03 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 10:06:13 +0200 Subject: [PATCH 13/36] documentation --- .github/workflows/docs.yml | 40 ++++++++++++ .gitignore | 39 +++++++++++- .readthedocs.yml | 12 ++++ docs/Makefile | 18 ++++++ docs/_autosummary/sciopy.EIT_16_32_64_128.rst | 44 +++++++++++++ docs/_autosummary/sciopy.ISX_3.rst | 43 +++++++++++++ docs/_autosummary/sciopy.com_util.rst | 22 +++++++ docs/_autosummary/sciopy.doteit.rst | 18 ++++++ docs/_autosummary/sciopy.meshing.rst | 15 +++++ docs/_autosummary/sciopy.rst | 6 ++ .../sciopy.sciopy_dataclasses.rst | 17 +++++ docs/_autosummary/sciopy.visualization.rst | 14 +++++ docs/_static/logo_sciopy.jpg | Bin 0 -> 94810 bytes docs/api.rst | 14 +++++ docs/conf.py | 59 ++++++++++++++++++ docs/index.rst | 20 ++++++ docs/make.bat | 10 +++ docs/requirements.txt | 6 ++ 18 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 .readthedocs.yml create mode 100644 docs/Makefile create mode 100644 docs/_autosummary/sciopy.EIT_16_32_64_128.rst create mode 100644 docs/_autosummary/sciopy.ISX_3.rst create mode 100644 docs/_autosummary/sciopy.com_util.rst create mode 100644 docs/_autosummary/sciopy.doteit.rst create mode 100644 docs/_autosummary/sciopy.meshing.rst create mode 100644 docs/_autosummary/sciopy.rst create mode 100644 docs/_autosummary/sciopy.sciopy_dataclasses.rst create mode 100644 docs/_autosummary/sciopy.visualization.rst create mode 100644 docs/_static/logo_sciopy.jpg create mode 100644 docs/api.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/requirements.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..2497422 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,40 @@ +name: Build and deploy docs + +on: + push: + branches: [ main, master, develop ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r docs/requirements.txt + pip install -e . + + - name: Build Sphinx docs + working-directory: docs + run: make html + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + steps: + - uses: actions/checkout@v4 + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/_build/html diff --git a/.gitignore b/.gitignore index d7d7d65..7d89378 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,5 @@ -__pycache__/ manuals error_device_logs -build/ -dist/ .update_package.sh.swp .eggs/ sciopy.egg-info/ @@ -23,3 +20,39 @@ examples/measuremet_32/* sciopy/eth_* Driver ISX3-dev.ipynb +update_docu.md + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ + +# virtualenvs +.env/ +.venv/ +.docs-venv/ +docs-venv/ +venv/ +venv*/ + +# Sphinx build outputs & doctrees +docs/_build/ +docs/_doctrees/ +_build/ +.doctrees/ + +# Sphinx cache / temp +docs/.doctrees/ +*.doctree + +# OS / editor files +.DS_Store +Thumbs.db +.idea/ +.vscode/ + +# pip / wheel cache (optional) +pip-wheel-metadata/ +*.whl \ No newline at end of file diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..1ce954d --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,12 @@ +version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.12" +python: + install: + - method: pip + path: . + - requirements: docs/requirements.txt +requests: + - type: github diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..a8e594a --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,18 @@ +# Minimal Makefile for Sphinx +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +.PHONY: help html clean + +help: + @echo "Please use 'make ' where is one of" + @echo " html to build the documentation" + @echo " clean to remove build artifacts" + +html: + $(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" -a -E + +clean: + @rm -rf $(BUILDDIR)/* + @echo "Cleaned build artifacts" \ 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 new file mode 100644 index 0000000..4cad4f7 --- /dev/null +++ b/docs/_autosummary/sciopy.EIT_16_32_64_128.rst @@ -0,0 +1,44 @@ +sciopy.EIT\_16\_32\_64\_128 +=========================== + +.. currentmodule:: sciopy + +.. autoclass:: EIT_16_32_64_128 + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~EIT_16_32_64_128.GetDeviceInfo + ~EIT_16_32_64_128.GetFirmwareIDs + ~EIT_16_32_64_128.GetMeasurementSetup + ~EIT_16_32_64_128.GetOutputConfiguration + ~EIT_16_32_64_128.PowerPlugDetect + ~EIT_16_32_64_128.ResetMeasurementSetup + ~EIT_16_32_64_128.SaveSettings + ~EIT_16_32_64_128.SetMeasurementSetup + ~EIT_16_32_64_128.SetOutputConfiguration + ~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.update_BurstCount + ~EIT_16_32_64_128.update_FrameRate + ~EIT_16_32_64_128.write_command_string + + + + + + \ No newline at end of file diff --git a/docs/_autosummary/sciopy.ISX_3.rst b/docs/_autosummary/sciopy.ISX_3.rst new file mode 100644 index 0000000..6c86a51 --- /dev/null +++ b/docs/_autosummary/sciopy.ISX_3.rst @@ -0,0 +1,43 @@ +sciopy.ISX\_3 +============= + +.. currentmodule:: sciopy + +.. autoclass:: ISX_3 + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~ISX_3.Action + ~ISX_3.GetDeviceID + ~ISX_3.GetExtensionPortChannel + ~ISX_3.GetExtensionPortModule + ~ISX_3.GetFE_Settings + ~ISX_3.GetFPGAfirmwareID + ~ISX_3.GetOptions + ~ISX_3.GetSetup + ~ISX_3.GetSyncTime + ~ISX_3.ResetSystem + ~ISX_3.SetExtensionPortChannel + ~ISX_3.SetFE_Settings + ~ISX_3.SetMeasurementSetup + ~ISX_3.SetOptions + ~ISX_3.SetSetup + ~ISX_3.SetSyncTime + ~ISX_3.StartMeasure + ~ISX_3.SystemMessageCallback + ~ISX_3.__init__ + ~ISX_3.connect_device_USB2 + ~ISX_3.disconnect_device_USB2 + ~ISX_3.write_command_string + + + + + + \ No newline at end of file diff --git a/docs/_autosummary/sciopy.com_util.rst b/docs/_autosummary/sciopy.com_util.rst new file mode 100644 index 0000000..111110a --- /dev/null +++ b/docs/_autosummary/sciopy.com_util.rst @@ -0,0 +1,22 @@ +sciopy.com\_util +================ + +.. automodule:: sciopy.com_util + + + .. rubric:: Functions + + .. 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.doteit.rst b/docs/_autosummary/sciopy.doteit.rst new file mode 100644 index 0000000..1c56144 --- /dev/null +++ b/docs/_autosummary/sciopy.doteit.rst @@ -0,0 +1,18 @@ +sciopy.doteit +============= + +.. automodule:: sciopy.doteit + + + .. rubric:: Functions + + .. autosummary:: + + convert_fulldir_doteit_to_npz + convert_fulldir_doteit_to_pickle + doteit_in_SingleEitFrame + list_all_files + list_eit_files + load_pickle_to_dict + single_eit_in_pickle + \ No newline at end of file diff --git a/docs/_autosummary/sciopy.meshing.rst b/docs/_autosummary/sciopy.meshing.rst new file mode 100644 index 0000000..2143bcb --- /dev/null +++ b/docs/_autosummary/sciopy.meshing.rst @@ -0,0 +1,15 @@ +sciopy.meshing +============== + +.. automodule:: sciopy.meshing + + + .. rubric:: Functions + + .. autosummary:: + + add_circle_anomaly + create_empty_2d_mesh + mesh_sample + plot_mesh + \ No newline at end of file diff --git a/docs/_autosummary/sciopy.rst b/docs/_autosummary/sciopy.rst new file mode 100644 index 0000000..851bc18 --- /dev/null +++ b/docs/_autosummary/sciopy.rst @@ -0,0 +1,6 @@ +sciopy +====== + +.. automodule:: sciopy + + \ No newline at end of file diff --git a/docs/_autosummary/sciopy.sciopy_dataclasses.rst b/docs/_autosummary/sciopy.sciopy_dataclasses.rst new file mode 100644 index 0000000..742ec51 --- /dev/null +++ b/docs/_autosummary/sciopy.sciopy_dataclasses.rst @@ -0,0 +1,17 @@ +sciopy.sciopy\_dataclasses +========================== + +.. automodule:: sciopy.sciopy_dataclasses + + + .. rubric:: Classes + + .. autosummary:: + + EisMeasurementSetup + EitMeasurementSetup + PreperationConfig + ScioSpecMeasurementConfig + SingleEitFrame + SingleFrame + \ No newline at end of file diff --git a/docs/_autosummary/sciopy.visualization.rst b/docs/_autosummary/sciopy.visualization.rst new file mode 100644 index 0000000..3cf4965 --- /dev/null +++ b/docs/_autosummary/sciopy.visualization.rst @@ -0,0 +1,14 @@ +sciopy.visualization +==================== + +.. automodule:: sciopy.visualization + + + .. rubric:: Functions + + .. autosummary:: + + norm_data + plot_el_sign + plot_potential_matrix + \ No newline at end of file diff --git a/docs/_static/logo_sciopy.jpg b/docs/_static/logo_sciopy.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5dbe4abfd2edfa6f277c907dc761ad8f11df5af3 GIT binary patch literal 94810 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 literal 0 HcmV?d00001 diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..d6c7171 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,14 @@ +API reference +============= + +.. autosummary:: + :toctree: _autosummary + + sciopy + sciopy.com_util + sciopy.doteit + sciopy.EIT_16_32_64_128 + sciopy.ISX_3 + sciopy.meshing + sciopy.sciopy_dataclasses + sciopy.visualization diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..5004367 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,59 @@ +import os +import sys +from datetime import datetime + +# Allow importing the package from repo root +sys.path.insert(0, os.path.abspath("..")) + +project = "sciopy" +author = "Jacob P. Thönes" +release = "0.8.9" + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.autosummary", + "sphinx.ext.viewcode", + "sphinx_autodoc_typehints", + "sphinx.ext.intersphinx", + "nbsphinx", +] + +autosummary_generate = True +autodoc_member_order = "bysource" +autodoc_typehints = "description" + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] + +# Project logo shown in the theme header +html_logo = "_static/logo_sciopy.jpg" + +# Intersphinx mapping to useful external docs +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "pandas": ("https://pandas.pydata.org/docs/", None), +} + +# Linkcheck settings (tolerate some known external issues) +linkcheck_ignore = [r"https://localhost:\d+/"] +linkcheck_timeout = 10 + +# Small helper for copyright line +copyright = f"{datetime.now().year}, {author}" + +# Mock imports if building on CI without installing heavy deps (adjust as needed) +autodoc_mock_imports = [ + "numpy", + "pandas", + "matplotlib", + "serial", + "pyeit", + "pyftdi", + "pyserial", + "tqdm", +] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..6ba2772 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,20 @@ +sciopy documentation master file +================================= + +Welcome to sciopy's documentation! + +Contents: + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + api + + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..648943d --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,10 @@ +@echo off +set SPHINXBUILD=sphinx-build +set SOURCEDIR=. +set BUILDDIR=_build +if "%1"=="clean" ( + if exist %BUILDDIR% rd /s /q %BUILDDIR% + echo Cleaned build artifacts + goto :eof +) +%SPHINXBUILD% -M html %SOURCEDIR% %BUILDDIR% -a -E diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..ee6e820 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,6 @@ +sphinx +sphinx-rtd-theme +sphinx-autodoc-typehints +sphinxcontrib-napoleon +sphinx-linkcheck +nbsphinx From 4ef438d3fcafef5b28da86607688b24083ca7c07 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 10:07:52 +0200 Subject: [PATCH 14/36] shinx lint check --- docs/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index ee6e820..54bff3a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,5 +2,4 @@ sphinx sphinx-rtd-theme sphinx-autodoc-typehints sphinxcontrib-napoleon -sphinx-linkcheck nbsphinx From c47c444bfbd0e4dea4c9a73b32d72220b4969220 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 10:28:55 +0200 Subject: [PATCH 15/36] shinx/img update --- README.md | 2 +- docs/_static/logo_sciopy.jpg | Bin 94810 -> 56378 bytes requirements.txt | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 64a9ee2..fb3970e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Sciopy-logo +Sciopy-logo 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. diff --git a/docs/_static/logo_sciopy.jpg b/docs/_static/logo_sciopy.jpg index 5dbe4abfd2edfa6f277c907dc761ad8f11df5af3..5209c0864a1d07794271028b6bb37bfc232d5409 100644 GIT binary patch delta 47942 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

XudDml3i@vA1br9 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 literal 94810 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 diff --git a/requirements.txt b/requirements.txt index 8f52869..3af6f5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ pandas==2.0.3 pyeit==1.2.4 pyftdi==0.55.0 pyserial==3.5 -setuptools==67.7.2 -tqdm==4.65.0 \ No newline at end of file +setuptools==78.1.1 +tqdm==4.66.3 From 6e93e6bcd089e3185a48c42b7d90bab0c709e70e Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 10:31:24 +0200 Subject: [PATCH 16/36] image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb3970e..5dd5f93 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Sciopy-logo +Sciopy-logo 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. From 4f34836a75cd1ac53d34c089807faafe83ccb439 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 10:44:20 +0200 Subject: [PATCH 17/36] pytest 0.8.2.2 --- README.md | 2 +- docs/conf.py | 2 +- setup.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5dd5f93..ae2d8ee 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Sciopy-logo +Sciopy-logo 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. diff --git a/docs/conf.py b/docs/conf.py index 5004367..c4611c7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,7 +7,7 @@ project = "sciopy" author = "Jacob P. Thönes" -release = "0.8.9" +release = "0.8.2.2" extensions = [ "sphinx.ext.autodoc", diff --git a/setup.py b/setup.py index 2e94b6d..4e1c4fa 100644 --- a/setup.py +++ b/setup.py @@ -2,11 +2,11 @@ setup( name="sciopy", - version="0.8.9", + version="0.8.2.2", packages=find_packages(), author="Jacob P. Thönes", author_email="jacob.thoenes@uni-rostock.de", - description="Python based interface module for communication with the Sciospec Electrical Impedance Tomography (EIT) device.", + description="Python based interface module for communication with Sciospec devices.", long_description=open("README.md").read(), long_description_content_type="text/markdown", keywords="Sciospec EIT EIS".split(), @@ -16,5 +16,5 @@ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], - url="https://github.com/spatialaudio/sciopy.git", + url="https://github.com/EITLabworks/sciopy", ) From cf72f843b07a445a91d69a0effb7da85951260da Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 10:46:35 +0200 Subject: [PATCH 18/36] uncomment deployment --- .github/workflows/docs.yml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2497422..2846ec1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,17 +24,4 @@ jobs: - name: Build Sphinx docs working-directory: docs - run: make html - - deploy: - needs: build - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' - steps: - - uses: actions/checkout@v4 - - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs/_build/html + run: make html \ No newline at end of file From a3cf5e6e48df01a77b01713a2a07b0a69128f507 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 10:52:21 +0200 Subject: [PATCH 19/36] upd readme --- .github/workflows/docs.yml | 2 +- README.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2846ec1..a72040f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,4 @@ -name: Build and deploy docs +name: Build docs on: push: diff --git a/README.md b/README.md index ae2d8ee..cb71594 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ 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 ## Contact -If you have ideas or other advice don't hesitate to contact me! +If you have any ideas or other suggestions, please don't hesitate to contact me. Email: jacob.thoenes@uni-rostock.de From 5a15b2fbbeb24e1ab57458d9a0b6aaf9780876c9 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Thu, 16 Oct 2025 16:16:28 +0200 Subject: [PATCH 20/36] FTDI connection wip --- README.md | 4 ++ sciopy/ISX_3.py | 105 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 90 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index cb71594..26b81c2 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,7 @@ This package offers the serial interface for communication with an EIT device fr If you have any ideas or other suggestions, please don't hesitate to contact me. Email: jacob.thoenes@uni-rostock.de + +___ + +- FTDI Driver installation: https://www.ftdichip.com/old2020/Drivers/D2XX.htm \ No newline at end of file diff --git a/sciopy/ISX_3.py b/sciopy/ISX_3.py index cc4f3f9..5ae5d49 100644 --- a/sciopy/ISX_3.py +++ b/sciopy/ISX_3.py @@ -1,6 +1,8 @@ """Module for interfacing with the Sciospec ISX-3 EIT device via serial communication""" try: + from pyftdi.ftdi import Ftdi + from pyftdi.usbtools import UsbTools import serial except ImportError: print("Could not import module: serial") @@ -52,33 +54,98 @@ class ISX_3: A class for interfacing with the Sciospec ISX-3 EIT device. """ - def __init__(self) -> None: + def __init__(self, VID: hex = 0x0403, PID: hex = 0x89D0) -> None: self.print_msg = True self.ret_hex_int = None + self.VID = VID + self.PID = PID + + def info(): + print("ISX-3v2 EIT device specifications") + print( + """ + Frequency + range: 100mHz to 10MHz + resolution: <10mHz @ f<10kHz + <25mHz @ 10kHz ≤ f < 100kHz + <150mHz @ 100kHz ≤ f ≤ 10MHz + precision absolute: ±100ppm (at 25°C) + temperature drift: ±10ppm over operating temperature range + long time stability: ±5ppm first year + + Excitation Signal + range: 1mV to 1000mV peak-amplitude + resolution: 0.1 mV + maximum continuous output current: 50 mA + + Output Impedance + output impedance: 1Ω (nominal) + + DC Bias (optional) + voltage range: -8 V to 8 V + voltage resolution: 10 mV + current range: -20 mA to 20 mA + current resolution: 25µA + + Precision Settings + precision range: + 0 to 1: high speed, lower accuracy + 1: standard configuration Δ|Z|/|Z|<0.1% + 1 to 10: high accuracy, low speed + averaging: 1 to 1024 + + Sweep Setting + available sweep parameters: frequency, amplitude, DC bias voltage, DC bias current, kinetic, point delay + sweep type: linear, logarithmic, list + points: 1 to 2048 + sweep delay[1]: 0s to 3min in 1µs steps + point delay[2]: 0s to 3min in 1µs steps + + [1] Sweep-Delay: Timing delay between two consecutive measurements of complete impedance spectra + [2] Point-Delay: Timing delay between two consecutive measurements of single frequencies + """ + ) + + def list_usb_devices(self): + """ + Lists all connected USB devices and checks for the ISX-3 device. - def connect_device_USB2(self, port: str, baudrate: int = 9600, timeout: int = 1): + Returns + ------- + list + A list of connected USB devices. """ - Connect to USB 2.0 Type B + devices = UsbTools.find_all([(self.VID, self.PID)]) + for device in devices: + device_info = { + "vendor": hex(device[0].vid), + "product": hex(device[0].pid), + "description": device[0].description, + } + if self.print_msg: + print(f"Found device: {device_info}") + + def connect_device_FTDI(self): """ - if hasattr(self, "USB-FS"): + Connect to the ISX-3 device via FTDI interface. + """ + if hasattr(self, "USB-FD"): 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.serial_protocol = "USB-FD" + self.device = Ftdi() + self.device.open(vendor=self.VID, product=self.PID) + print("Connection to", self.device, "is established.") - def disconnect_device_USB2(self): - self.device.close() - print("Connection to", self.device.name, "is closed.") + def disconnect_device_FTDI(self): + """ + Disconnects the FTDI device. + """ + if hasattr(self, "device"): + self.device.close() + print("Connection to ISX-3 is closed.") def SystemMessageCallback(self): """ @@ -90,7 +157,7 @@ def SystemMessageCallback(self): data_count = 0 while True: - buffer = self.device.read() + buffer = self.device.read_data(size=4) if buffer: received.extend(buffer) data_count += len(buffer) @@ -128,7 +195,7 @@ def write_command_string(self, command): """ Function for writing a command 'bytearray(...)' to the serial port """ - self.device.write(command) + self.device.write_data(command) self.SystemMessageCallback() def ResetSystem(self): From a8c5e415e0108ae7ca588c6165f3561ed87bbf56 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Tue, 21 Oct 2025 08:11:35 +0200 Subject: [PATCH 21/36] upd EIT example --- examples/EIT-16-256-Ch.ipynb | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/examples/EIT-16-256-Ch.ipynb b/examples/EIT-16-256-Ch.ipynb index 7902813..c591e34 100644 --- a/examples/EIT-16-256-Ch.ipynb +++ b/examples/EIT-16-256-Ch.ipynb @@ -5,7 +5,7 @@ "id": "1e1715cc-c123-45e2-b767-e6412faeebe3", "metadata": {}, "source": [ - "# Example code for connecting a Sciospec device" + "# Example code for connecting a Sciospec EIT device" ] }, { @@ -13,10 +13,10 @@ "id": "8a5423a4-f8da-4cdb-8163-1d50d12e6e23", "metadata": {}, "source": [ - "## EIT device\n", - "\n", "**USB-HS**\n", "\n", + "Connect the EIT device to the USB-HS port.\n", + "\n", "If you have issues with the connection try [this](https://eblot.github.io/pyftdi/installation.html)." ] }, @@ -169,22 +169,10 @@ }, { "cell_type": "markdown", - "id": "a526b930-1c20-4f6f-a500-602451eca9d7", + "id": "b1005ea6-0f4e-44f9-a721-6e2f20edc11d", "metadata": {}, "source": [ - "## EIS device\n", - "\n", - "**USB-HS**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c6c247dd-a41c-4ebd-9c65-867c1b995dcf", - "metadata": {}, - "outputs": [], - "source": [ - "# TBD" + "___" ] } ], From 3dbc91f6a30c42e302af7a224527336a41091731 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Tue, 21 Oct 2025 08:26:47 +0200 Subject: [PATCH 22/36] upd EIT example --- examples/EIT-16-256-Ch.ipynb | 71 ++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/examples/EIT-16-256-Ch.ipynb b/examples/EIT-16-256-Ch.ipynb index c591e34..d4d7395 100644 --- a/examples/EIT-16-256-Ch.ipynb +++ b/examples/EIT-16-256-Ch.ipynb @@ -8,18 +8,6 @@ "# Example code for connecting a Sciospec EIT device" ] }, - { - "cell_type": "markdown", - "id": "8a5423a4-f8da-4cdb-8163-1d50d12e6e23", - "metadata": {}, - "source": [ - "**USB-HS**\n", - "\n", - "Connect the EIT device to the USB-HS port.\n", - "\n", - "If you have issues with the connection try [this](https://eblot.github.io/pyftdi/installation.html)." - ] - }, { "cell_type": "code", "execution_count": null, @@ -45,6 +33,23 @@ "sciospec = EIT_16_32_64_128(n_el)" ] }, + { + "cell_type": "markdown", + "id": "c6d1efce-1e7f-46a0-a092-e2fe38017fc6", + "metadata": {}, + "source": [ + "___\n", + "For some Sciospec devices two USB ports are available.\n", + "It it preferred to use the USB-HS port, because it is faster than USB-FS.\n", + "You cannot connect both USB ports with the same script.\n", + "\n", + "**USB-HS**\n", + "\n", + "Connect the EIT device to the USB-HS port.\n", + "\n", + "If you have issues with the connection try [this](https://eblot.github.io/pyftdi/installation.html)." + ] + }, { "cell_type": "code", "execution_count": null, @@ -56,6 +61,48 @@ "sciospec.connect_device_HS()" ] }, + { + "cell_type": "markdown", + "id": "8dadceb4-5f5a-4fe7-8490-c16423bf9f24", + "metadata": {}, + "source": [ + "**USB-FS**\n", + "\n", + "Connect the EIT device to the USB-FS port." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3f4ca6b-fff9-41e2-97c6-6d458702b7b1", + "metadata": {}, + "outputs": [], + "source": [ + "from sciopy import available_serial_ports\n", + "\n", + "# this function can be used to get the available ports (works only on windows, I guess)\n", + "available_serial_ports()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "924cbf79-ca2a-4d32-a1f1-6450746078c0", + "metadata": {}, + "outputs": [], + "source": [ + "# connect device via USB-FS port\n", + "sciospec.connect_device_FS(port=\"COM1\") # Insert available serial port here." + ] + }, + { + "cell_type": "markdown", + "id": "5fd0f70f-f0c8-429c-b0db-43d23b75f365", + "metadata": {}, + "source": [ + "___" + ] + }, { "cell_type": "code", "execution_count": null, From 33214e85ea898f5e10f684de529ff8a904bca96b Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Tue, 21 Oct 2025 15:58:42 +0200 Subject: [PATCH 23/36] Pypi-test-v0.8.2.3 --- docs/_autosummary/sciopy.EIT_16_32_64_128.rst | 1 + docs/_autosummary/sciopy.ISX_3.rst | 6 +++-- docs/conf.py | 2 +- examples/{ISX-3.ipynb => ISX-3v2.ipynb} | 8 ++++++ sciopy/EIT_16_32_64_128.py | 26 +++++++++++++++++++ sciopy/ISX_3.py | 22 +++++++++------- setup.py | 2 +- 7 files changed, 54 insertions(+), 13 deletions(-) rename examples/{ISX-3.ipynb => ISX-3v2.ipynb} (98%) diff --git a/docs/_autosummary/sciopy.EIT_16_32_64_128.rst b/docs/_autosummary/sciopy.EIT_16_32_64_128.rst index 4cad4f7..5bb6771 100644 --- a/docs/_autosummary/sciopy.EIT_16_32_64_128.rst +++ b/docs/_autosummary/sciopy.EIT_16_32_64_128.rst @@ -34,6 +34,7 @@ ~EIT_16_32_64_128.get_data_as_matrix ~EIT_16_32_64_128.init_channel_group ~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.write_command_string diff --git a/docs/_autosummary/sciopy.ISX_3.rst b/docs/_autosummary/sciopy.ISX_3.rst index 6c86a51..5024d57 100644 --- a/docs/_autosummary/sciopy.ISX_3.rst +++ b/docs/_autosummary/sciopy.ISX_3.rst @@ -32,8 +32,10 @@ ~ISX_3.StartMeasure ~ISX_3.SystemMessageCallback ~ISX_3.__init__ - ~ISX_3.connect_device_USB2 - ~ISX_3.disconnect_device_USB2 + ~ISX_3.connect_device_FTDI + ~ISX_3.disconnect_device_FTDI + ~ISX_3.info + ~ISX_3.list_usb_devices ~ISX_3.write_command_string diff --git a/docs/conf.py b/docs/conf.py index c4611c7..c9abb4c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,7 +7,7 @@ project = "sciopy" author = "Jacob P. Thönes" -release = "0.8.2.2" +release = "0.8.2.3" extensions = [ "sphinx.ext.autodoc", diff --git a/examples/ISX-3.ipynb b/examples/ISX-3v2.ipynb similarity index 98% rename from examples/ISX-3.ipynb rename to examples/ISX-3v2.ipynb index e91d9f8..35c8a10 100644 --- a/examples/ISX-3.ipynb +++ b/examples/ISX-3v2.ipynb @@ -31,6 +31,14 @@ "isx = ISX_3()" ] }, + { + "cell_type": "markdown", + "id": "ec641683", + "metadata": {}, + "source": [ + "### WIP" + ] + }, { "cell_type": "code", "execution_count": 3, diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index fc4d0f7..0f7ca6e 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -357,6 +357,32 @@ def update_FrameRate(self, framerate): ) self.print_msg = False + def update_ExcitationFrequency(self, exc_freq): + """ + update_ExcitationFrequencies _summary_ + + Parameters + ---------- + exc_freq int + frequency to be set from 100 Hz to 1 MHz + """ + # Set frequencies: + # [CT] 0C 04 [fmin] [fmax] [fcount] [ftype] [CT] + self.print_msg = True + f_min = clTbt_sp(exc_freq) + f_max = clTbt_sp(exc_freq) + f_count = [0, 1] + f_type = [0] # linear/log + # bytearray + self.write_command_string( + bytearray( + list( + np.concatenate([[176, 12, 4], f_min, f_max, f_count, f_type, [176]]) + ) + ) + ) + self.print_msg = False + def SetMeasurementSetup(self, setup: EitMeasurementSetup): """ Configures the ScioSpec device measurement setup according to the provided EitMeasurementSetup dataclass. diff --git a/sciopy/ISX_3.py b/sciopy/ISX_3.py index 5ae5d49..8d1e497 100644 --- a/sciopy/ISX_3.py +++ b/sciopy/ISX_3.py @@ -115,15 +115,19 @@ def list_usb_devices(self): list A list of connected USB devices. """ - devices = UsbTools.find_all([(self.VID, self.PID)]) - for device in devices: - device_info = { - "vendor": hex(device[0].vid), - "product": hex(device[0].pid), - "description": device[0].description, - } + try: + devices = UsbTools.find_all([(self.VID, self.PID)]) + for device in devices: + device_info = { + "vendor": hex(device[0].vid), + "product": hex(device[0].pid), + "description": device[0].description, + } + if self.print_msg: + print(f"Found device: {device_info}") + except Exception as e: if self.print_msg: - print(f"Found device: {device_info}") + print(f"Error listing USB devices: {e}") def connect_device_FTDI(self): """ @@ -157,7 +161,7 @@ def SystemMessageCallback(self): data_count = 0 while True: - buffer = self.device.read_data(size=4) + buffer = self.device.read_data() if buffer: received.extend(buffer) data_count += len(buffer) diff --git a/setup.py b/setup.py index 4e1c4fa..322e572 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="sciopy", - version="0.8.2.2", + version="0.8.2.3", packages=find_packages(), author="Jacob P. Thönes", author_email="jacob.thoenes@uni-rostock.de", From e6d4258193a3213cf3f289d9b3753000d6ab544a Mon Sep 17 00:00:00 2001 From: Patricia Fuchs <140624836+patfuchs@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:25:13 +0100 Subject: [PATCH 24/36] Added function for setting measurement mode --- sciopy/EIT_16_32_64_128.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index 0f7ca6e..62a911c 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -48,7 +48,6 @@ def __init__(self, n_el: int) -> None: number of electrodes used for measurement. """ self.n_el = n_el - self.channel_group = self.init_channel_group() self.print_msg = True self.ret_hex_int = None @@ -524,6 +523,21 @@ def ResetMeasurementSetup(self): self.write_command_string(bytearray([0xB0, 0x01, 0x01, 0xB0])) self.print_msg = False + def update_measurement_mode(self, meamode="skip4", boundary="external"): + meamodeoptions = {"singleended": 0x01, "skip0": 0x02, "skip2": 0x03, "skip4": 0x04} + try: + cmd = meamodeoptions[meamode] + except: + print("Option for measurement mode is unknown. Measurement mode ist set to single-ended.") + cmd = 0x01 + if boundary == "external": + bnd = "0x02" + else: + bnd = "0x01" + self.write_command_string(bytearray([0xB0, 0x03, 0x08,cmd, bnd, 0xB0])) + #todo read out ACK messages + + def GetMeasurementSetup(self, setup_of: str): """ Retrieves and configures the measurement setup for the device based on the specified setup option. From 8460792d95ace5b5a9182584783205eb1e03b4e8 Mon Sep 17 00:00:00 2001 From: Patricia Fuchs <140624836+patfuchs@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:15:21 +0100 Subject: [PATCH 25/36] USB Message Parser --- sciopy/usb_message_parser.py | 356 +++++++++++++++++++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 sciopy/usb_message_parser.py diff --git a/sciopy/usb_message_parser.py b/sciopy/usb_message_parser.py new file mode 100644 index 0000000..2d8fbda --- /dev/null +++ b/sciopy/usb_message_parser.py @@ -0,0 +1,356 @@ +""" +Project :quali_effects_eit_measurements +Directory: src +File : usb_message_parser.py +Author :Patricia Fuchs +Date :17.11.2025 09:04 +""" +import numpy as np +import time + +from dataclasses import dataclass +from typing import List, Tuple, Union +import numpy.typing as npt +import os +from pandas.core.interchange import dataframe +import struct +from .sciopy_dataclasses import EitMeasurementSetup + +TWOPOWER24 = 16777216 +TWOPOWER16 = 65536 +TWOPOWER8 = 256 +# -------------------------------------------------------------------------------------------------------------------- # +# -------------------------------------------------------------------------------------------------------------------- # +msg_dict = { + "0x01": "No message inside the message buffer", + "0x02": "Timeout: Communication-timeout (less data than expected)", + "0x04": "Wake-Up Message: System boot ready", + "0x11": "TCP-Socket: Valid TCP client-socket connection", + "0x81": "Not-Acknowledge: Command has not been executed", + "0x82": "Not-Acknowledge: Command could not be recognized", + "0x83": "Command-Acknowledge: Command has been executed successfully", + "0x84": "System-Ready Message: System is operational and ready to receive data", + "0x92": "Data holdup: Measurement data could not be sent via the master interface", +} + + +def byte_parser(): + # Return empty lists, while message is incomplete, else full usb-message as list [int[hex-format], int [hex-format], ..] + # Initialization + piCurrMess = [] + fMesstype = None + iCurrLen = 0 + data = yield # Data of dataclass Bytes + while True: + piCurrMess.extend(data) # Starting Message = Message Type + # Automatic conversion to integers within list + fMesstype = data # Save the Byte + + data = yield [] # 2 Byte = Length of Data within message + iCurrLen = int.from_bytes(data) + print(iCurrLen) + piCurrMess.extend(data) + + data = yield [] # Next iCurrLen Bytes = Actual Data Bytes + for i in range(iCurrLen): + piCurrMess.extend(data) + data = yield [] + + if fMesstype != data: # Last Byte != Message Type + print(f"Current message not complete for Starting messagetype {hex(fMesstype)} and ending type {hex(data[0])}") + piCurrMess.extend(data) + iCurrLen = 0 + data = yield piCurrMess # Return fully parsed message as list + piCurrMess = [] + + +#todo doku +class MessageParser: + def __init__(self, device, eitsetup=None, devicetype="FS"): + # General setup + + self.bCreateResultsFolder = True + self.bPrintMessages = False + self.iNPZSaveIndex = 1 + self.iSaveCounter = 0 + self.ppiMessages = [] # Unused + self.ppcData = [] + self.iInjIndex = 0 + + # Device setup + self.cDevice = device + self.sDevicetype = devicetype + if self.sDevicetype == "FS": + self.device_send = self.send_fs + self.device_read = self.read_fs + elif self.sDevicetype == "HS": + self.device_send = self.send_hs + self.device_read = self.read_hs + self.init_parser() + + + # Setup related changes + self.CurrentFrame = None + self.iMaxChannelGroups = 1 + self.iNumExcitationSettings = 1 + self.iNumFreqSettings = 1 + self.iLenDataperFrame = 1 + self.iMessagesperFrame = 1 + self.setup= eitsetup + self.set_measurement_setup(eitsetup) + + + def set_measurement_setup(self, setup: EitMeasurementSetup): + self.setup = setup + if setup != None: + self.iMaxChannelGroups = setup.n_el//16 + self.iNumExcitationSettings = setup.n_el # todo should be independently set + self.iNumFreqSettings = 1 # todo + self.iLenDataperFrame = self.iMaxChannelGroups * 16 * self.iNumExcitationSettings * self.iNumFreqSettings + self.iMessagesperFrame = self.iMaxChannelGroups * self.iNumExcitationSettings * self.iNumFreqSettings + + # ALL needed + self.reset_new_data_frame() + + + def init_parser(self): + self.Parser= byte_parser() + next(self.Parser) + + def read_fs(self): + return self.cDevice.read() + + def send_fs(self, tosend): + self.cDevice.write(tosend) + + def read_hs(self): + return self.cDevice.read_data_bytes(size=1024, attempt=150) + + def send_hs(self, tosend): + self.cDevice.write_data(tosend) + + + def read_usb_for_seconds(self, fTime, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/"): + if self.bCreateResultsFolder and bSaveData: + timestr = time.strftime("%Y%m%d-%H%M%S_eit") + path = os.path.join(sSavePath, timestr) + os.mkdir(path) + sSavePath = path + "/" + messages = [] + iMessageCount= 0 + bMessageStarted = False + timeout_count= 0 + fEndtime = time.time()+fTime + while time.time() < fEndtime or bMessageStarted: + buffer = self.device_read() + if buffer: + message = self.Parser.send(buffer) + + if len(message) > 0: + bMessageStarted = False + self.interpret_message(message, bSaveData,bDeleteDataFrame,sSavePath) + iMessageCount += 1 + else: + bMessageStarted = True + timeout_count = 0 + continue + else: + time.sleep(0.1) + timeout_count += 1 + + if timeout_count >= 100: + # Break if we haven't received any data + break + print(f"{iMessageCount} messages received.") + if self.bPrintMessages: + for message in self.ppcData: + print(message) + return self.ppcData + + def read_usb_till_timeout(self, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/"): + messages= [] + iMessageCount = 0 + timeout_count = 0 + while True: + buffer = self.device_read() + if buffer: + message= self.Parser.send(buffer) + if len(message)>0: + self.interpret_message(message, bSaveData,bDeleteDataFrame,sSavePath) + iMessageCount += 1 + timeout_count = 0 + continue + timeout_count += 1 + if timeout_count >= 1: + # Break if we haven't received any data + break + + print(f"{iMessageCount} messages received.") + if self.bPrintMessages: + for message in self.ppcData: + print(message) + return self.ppcData + + + def save_data_frame(self, path, dataframe): + np.savez(path + "eitsample_{0:06d}.npz".format(self.iNPZSaveIndex), + # aorta=aorta_segs[j], + excitation_stgs =dataframe.excitation_stgs, + frequency_row=dataframe.frequency_row, + timestamp1=dataframe.timestamp1, + timestamp2=dataframe.timestamp2, + ppcData=dataframe.ppcData) + self.iNPZSaveIndex+=1 + + + + # Message Interpreter Sciospec EIT + def interpret_message(self, message, bSaveData=False, bDeleteDataFrame=False, sSavePath= "C/"): + if message[0] == 180: # DATA 0XB4 + self.interpret_data_input(message, bSaveData, bDeleteDataFrame, sSavePath) + else: + mess_hex = [hex(receive) for receive in message] + if self.bPrintMessages: + if message[0] == 24: # 0x24 Acknowledgement Message + try: + print("Message: " +str(mess_hex) +" -> "+ msg_dict[mess_hex[2]]) + except: + print("Message: " +str(mess_hex) +" -> "+msg_dict["0x01"]) + else: + print("Unknown received message: "+str(mess_hex)) + + def reset_new_data_frame(self): + self.iInjIndex= 0 + self.iSaveCounter= 0 + self.CurrentFrame = EITFrame(channel_group=self.iMaxChannelGroups, + excitation_stgs=np.zeros((self.iNumExcitationSettings, 2), dtype=int), + frequency_stgs=np.zeros((self.iNumFreqSettings,), dtype=int), # todo fill in setup freq settings + timestamp1=0, + timestamp2= 0, + ppcData= np.zeros(self.iMaxChannelGroups * 16 * self.iNumExcitationSettings, dtype= complex)) + + + + def interpret_data_input(self, message, bSave=False, bDeleteFrame=False, sSavePath="C/"): + # Look after channel group and if data already started, append + # ChannelGROUP + + # EXCITATIONSETTING + freq_group = two_byte_to_int(message[5:7]) + if message[2] == 1 and freq_group == 1: + self.CurrentFrame.excitation_stgs[self.iInjIndex] = [message[3], message[4]] + self.iInjIndex += 1 + + # FREQUENCY ROW is set through eitsetup + # self.CurrentFrame.frequency_stgs = self.iNumFreqSettings + + #TIMESTAMP + if self.iSaveCounter == 0: + self.CurrentFrame.timestamp1 = four_byte_to_int(message[7:11]) + + # Data Handling + for i in range(11, 135, 8): + data = complex( + four_byte_to_int(message[i: i + 4]), + four_byte_to_int(message[i + 4: i + 8]), + ) + self.CurrentFrame.ppcData[self.iSaveCounter] = data + self.iSaveCounter += 1 + + if self.iSaveCounter == self.iLenDataperFrame: + # Frame Full + self.CurrentFrame.timestamp2 = four_byte_to_int(message[7:11]) + if bSave: + self.save_data_frame(sSavePath, self.CurrentFrame) + if bDeleteFrame: + del self.CurrentFrame + else: + self.ppcData.append(self.CurrentFrame) + + self.reset_new_data_frame() + # extract + +@dataclass +class EITFrame: + """ + This class is for parsing the whole EIT excitation stages. + + Parameters + ---------- + start_tag : str + has to be 'b4' + channel_group : int + channel group: CG=1 -> Channels 1-16, CG=2 -> Channels 17-32 up to CG=4 + excitation_stgs : List[str] + excitation setting: [ESout, ESin] + frequency_row : List[str] + frequency row + timestamp : int + milli seconds + + end_tag : str + has to be 'b4' + """ + + channel_group: int # todo weglassen + excitation_stgs: npt.NDArray[int] # Num Excitation Settings X 2 + frequency_stgs: npt.NDArray[int] # List of Frequency-Sweep Settings, + timestamp1: int # [ms] + timestamp2: int + ppcData: npt.NDArray[complex] # Channels 1-(64) all channel groups combined + + def __hex__(self): + return hex(self.ppcData) + + +def make_eitframes_hex(FrameList): + result= [] + for f in FrameList: + result.append(hex(f)) + return result + + +def get_data_as_matrix(FrameList): + result= [] + for f in FrameList: + L = len(f.ppcData)//(f.excitation_stgs) + result.append(np.reshape(f.ppcData,(f.excitation_stgs, L))) + return np.array(result) + + +def load_eit_frames(path,names): + # Load NPZ data into list of (EITFrame) + pass + +def load_eit_frames_into_nparray(path): + # Loads NPZ Data into nparray of [NumFrames X Sizeof Frequency and Excitation Settings] + pass + + + +def four_byte_to_int(bytelist): + return TWOPOWER24*bytelist[0] + TWOPOWER16* bytelist[1] +TWOPOWER8* bytelist[2] + bytelist[3] + +def two_byte_to_int(bytelist): + return TWOPOWER8* bytelist[0] + bytelist[1] + +def bytelist_to_int(bytelist): + r= bytelist[-1] + for j in range(2,len(bytelist)): + r+= bytelist[-j]* 2**((j-1)*8) + return r + + +def fourbytetst(bytes_array): + bytes_array = [int(b, 16) for b in bytes_array] + bytes_array = bytes(bytes_array) + s= struct.unpack("!f", bytes(bytes_array)) + return struct.unpack("!f", bytes(bytes_array))[0] + +def byteintlist_to_int(bytelist): + #faster than four bytes to int at the moment + bytes_array = bytes(bytelist) + return int.from_bytes(bytes_array, byteorder='big') + + + From 2cdf44d8dbcbb3549807db2282cb0592b1adc49d Mon Sep 17 00:00:00 2001 From: Patricia Fuchs <140624836+patfuchs@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:16:07 +0100 Subject: [PATCH 26/36] Integration of USB Message Parser to EITclass --- sciopy/EIT_16_32_64_128.py | 228 ++++++++++++++++++----------------- sciopy/sciopy_dataclasses.py | 2 + 2 files changed, 122 insertions(+), 108 deletions(-) diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index 62a911c..02133ed 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -5,7 +5,6 @@ except ImportError: print("Could not import module: serial") - from .com_util import ( clTbt_dp, clTbt_sp, @@ -17,7 +16,6 @@ import numpy as np from pyftdi.ftdi import Ftdi - msg_dict = { "0x01": "No message inside the message buffer", "0x02": "Timeout: Communication-timeout (less data than expected)", @@ -31,6 +29,7 @@ } from .sciopy_dataclasses import EitMeasurementSetup +from sciopydev.sciopy.usb_message_parser import MessageParser, make_eitframes_hex, get_data_as_matrix class EIT_16_32_64_128: @@ -51,6 +50,8 @@ def __init__(self, n_el: int) -> None: self.channel_group = self.init_channel_group() self.print_msg = True self.ret_hex_int = None + self.cMessageParser = None + self.setup = None def init_channel_group(self): """ @@ -107,6 +108,7 @@ def connect_device_HS(self, url: str = "ftdi://ftdi:232h/1", baudrate: int = 900 serial.STOP_BIT_1 serial.set_baudrate(baudrate) self.device = serial + self.cMessageParser = MessageParser(self.device, devicetype="HS") def connect_device_FS(self, port: str, baudrate: int = 9600, timeout: int = 1): """ @@ -139,6 +141,7 @@ def connect_device_FS(self, port: str, baudrate: int = 9600, timeout: int = 1): ) print("Connection to", self.device.name, "is established.") + self.cMessageParser = MessageParser(self.device, devicetype="FS") def disconnect_device(self): """ @@ -148,78 +151,50 @@ def disconnect_device(self): """ self.device.close() - def SystemMessageCallback_usb_fs(self): - """ - 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. + def send_message(self, message): + """ + Wrapper function to send a byte array to the device. Communication method is based on the defined serial + protocol. - 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. + Args: + message (bytearray): A byte array to be sent to the device. """ - timeout_count = 0 - received = [] - received_hex = [] - data_count = 0 + if self.serial_protocol == "HS": + self.device.write_data(message) + elif self.serial_protocol == "FS": + self.device.write(message) - while True: - buffer = self.device.read() - if buffer: - received.extend(buffer) - data_count += len(buffer) - timeout_count = 0 - continue - timeout_count += 1 - if timeout_count >= 1: - # Break if we haven't received any data - break - received = "".join(str(received)) # If you need all the data - received_hex = [hex(receive) for receive in received] - try: - msg_idx = received_hex.index("0x18") - if self.print_msg: - print(msg_dict[received_hex[msg_idx + 2]]) - except BaseException: - if self.print_msg: - print(msg_dict["0x01"]) - # self.print_msg = False - if self.print_msg: - print("message buffer:\n", received_hex) - print("message length:\t", data_count) + def read_message(self): + """ + Wrapper function to read single bytes from the device. Communication method is based on the defined serial + protocol. - if self.ret_hex_int is None: - return - elif self.ret_hex_int == "hex": - return received_hex - elif self.ret_hex_int == "int": - return received - elif self.ret_hex_int == "both": - return received, received_hex + Return: + A single byte read from the device. + """ + if self.serial_protocol == "HS": + return self.device.read_data_bytes(size=1024, attempt=150) + elif self.serial_protocol == "FS": + return self.device.read() - def SystemMessageCallback_usb_hs(self): + def SystemMessageCallback(self): """ - Reads data from a USB high-speed device, processes received messages, and returns the data in various formats. + Handles system messages based on the selected serial protocol. + Reads data from a USB device, processes received messages, and returns the data in the specified format. - 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). + 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[list[int], list[str]]: Both integer and hexadecimal lists if `self.ret_hex_int == "both"`. + tuple: 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 = [] @@ -227,7 +202,7 @@ def SystemMessageCallback_usb_hs(self): data_count = 0 while True: - buffer = self.device.read_data_bytes(size=1024, attempt=150) + buffer = self.read_message() if buffer: received.extend(buffer) data_count += len(buffer) @@ -261,23 +236,6 @@ def SystemMessageCallback_usb_hs(self): elif self.ret_hex_int == "both": return received, received_hex - def SystemMessageCallback(self): - """ - Handles system messages based on the selected serial protocol. - - 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()`. - - Raises: - AttributeError: If the required callback methods are not defined. - """ - if self.serial_protocol == "HS": - self.SystemMessageCallback_usb_hs() - elif self.serial_protocol == "FS": - self.SystemMessageCallback_usb_fs() - def write_command_string(self, command): """ Sends a command string to the device using the appropriate serial protocol. @@ -292,11 +250,9 @@ def write_command_string(self, command): Raises: AttributeError: If `self.device` does not have the required method for the selected protocol. """ - if self.serial_protocol == "HS": - self.device.write_data(command) - elif self.serial_protocol == "FS": - self.device.write(command) - self.SystemMessageCallback() + self.cMessageParser.bPrintMessages = self.print_msg + self.send_message(command) + self.cMessageParser.read_usb_till_timeout(bSaveData=False, bDeleteDataFrame=True) # --- sciospec device commands @@ -325,6 +281,7 @@ def update_BurstCount(self, burst_count): """ self.setup.burst_count = burst_count + self.print_msg = True self.write_command_string( bytearray([0xB0, 0x03, 0x02, 0x00, self.setup.burst_count, 0xB0]) ) @@ -410,6 +367,7 @@ def SetMeasurementSetup(self, setup: EitMeasurementSetup): """ self.setup = setup + self.cMessageParser.set_measurement_setup(self.setup) self.print_msg = False self.ResetMeasurementSetup() @@ -523,8 +481,18 @@ def ResetMeasurementSetup(self): self.write_command_string(bytearray([0xB0, 0x01, 0x01, 0xB0])) self.print_msg = False - def update_measurement_mode(self, meamode="skip4", boundary="external"): + + def update_measurement_mode(self, meamode: str = "skip4", boundary: str = "external"): + """ + Updates the measurement modes out of the options "singleended", "skip0", "skip2" or "skip4" + + Args: + meamode (str): Measurement mode "singleended", "skip0", "skip2" or "skip4" + boundary (str): Return if the boundary for skip patterns is internal of a channel group or external for all + optional channels + """ meamodeoptions = {"singleended": 0x01, "skip0": 0x02, "skip2": 0x03, "skip4": 0x04} + self.print_msg = True try: cmd = meamodeoptions[meamode] except: @@ -534,10 +502,10 @@ def update_measurement_mode(self, meamode="skip4", boundary="external"): bnd = "0x02" else: bnd = "0x01" - self.write_command_string(bytearray([0xB0, 0x03, 0x08,cmd, bnd, 0xB0])) + self.write_command_string(bytearray([0xB0, 0x03, 0x08, cmd, bnd, 0xB0])) + self.print_msg = False #todo read out ACK messages - def GetMeasurementSetup(self, setup_of: str): """ Retrieves and configures the measurement setup for the device based on the specified setup option. @@ -573,7 +541,8 @@ def GetMeasurementSetup(self, setup_of: str): print("TBD: Translation") self.print_msg = False - def StartStopMeasurement(self, return_as="pot_mat"): + #todo + def StartStopMeasurementFast(self, return_as="pot_mat"): """ 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. @@ -589,27 +558,18 @@ def StartStopMeasurement(self, return_as="pot_mat"): Returns: list or matrix: The measurement data in the format specified by `return_as`. """ - 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 + if self.setup.burst_count == 0: + print("Burst count for this setup needs to be >=1") + return + self.send_message(bytearray([0xB4, 0x01, 0x01, 0xB4])) + self.ret_hex_int = "hex" + self.print_msg = False - data = self.SystemMessageCallback_usb_fs() + data = self.SystemMessageCallback() - self.device.write(bytearray([0xB4, 0x01, 0x00, 0xB4])) - self.ret_hex_int = None - self.SystemMessageCallback() + self.send_message(bytearray([0xB4, 0x01, 0x00, 0xB4])) + self.ret_hex_int = None + self.SystemMessageCallback() data = del_hex_in_list(data) data = reshape_full_message_in_bursts(data, self.setup) @@ -619,7 +579,59 @@ def StartStopMeasurement(self, return_as="pot_mat"): if return_as == "hex": return self.data elif return_as == "pot_mat": - return self.get_data_as_matrix() + return self.get_data_as_matrix(self.data) + + #todo check + def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData:bool=False, bDeleteData:bool=False, + sSavepath:str="C/"): + """ + Starts and stops a measurement process using the configured serial protocol (HS or FS). + Sends appropriate commands to the device to initiate and terminate measurement. + If a timeout is specified, data is measrued for timeout seconds (burst_count==0). Else, a burst count needs to + be specified, and all measured data is received. + Processes the received data by removing hexadecimal values, reshaping messages into bursts, + and splitting bursts into frames. Stores the processed data in NPZ format at sSavepath + + Args: + timeout (int): Specifies the timeout in seconds. + return_as (str, optional): Specifies the format of the returned data. + - "hex": Returns the processed data as a list of hexadecimal values. + - "pot_mat": Returns the processed data as a matrix using `get_data_as_matrix()`. + Default is "pot_mat". + - else: data is only stored + bSaveData (bool): Specifies if a the measured data is saved in NPZ format + bDeleteData (bool): Specifies if a the measured data is deleted out of memory after each EITframe, with + bSaveData=True, measured data is saved and then removed from RAM + sSavepath (str): Specifies the sPath where the measured data is saved. + + Returns: + list or matrix: The measurement data in the format specified by `return_as`. + """ + + # Start measurement + self.send_message(bytearray([0xB4, 0x01, 0x01, 0xB4])) + self.cMessageParser.bPrintMessages = False + if timeout != 0: + data = self.cMessageParser.read_usb_for_seconds(timeout, bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, + sSavepath=sSavepath) + else: + if self.setup.burst_count ==0: + print("Burst count for this setup needs to be >=1") + return + data = self.cMessageParser.read_usb_till_timeout(bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, + sSavepath=sSavepath) + + # Stop measurement + self.send_message(bytearray([0xB4, 0x01, 0x00, 0xB4])) + data2 = self.cMessageParser.read_usb_till_timeout(bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, + sSavepath=sSavepath) + data = data + data2 + if bDeleteData: + return + if return_as == "hex": + return make_eitframes_hex(data) + elif return_as == "pot_mat": + return get_data_as_matrix(data) def get_data_as_matrix(self): """ @@ -650,7 +662,7 @@ def get_data_as_matrix(self): row += 1 el_signs = list() for ch in range(16): - el_signs.append(frame.__dict__[f"ch_{ch+1}"]) + el_signs.append(frame.__dict__[f"ch_{ch + 1}"]) el_signs = np.array(el_signs) start_idx = (curr_grp - 1) * 16 diff --git a/sciopy/sciopy_dataclasses.py b/sciopy/sciopy_dataclasses.py index ee2c079..9bd1a01 100644 --- a/sciopy/sciopy_dataclasses.py +++ b/sciopy/sciopy_dataclasses.py @@ -28,6 +28,8 @@ class EitMeasurementSetup: inj_skip: Union[int, list] gain: int adc_range: int + mea_mode: str = "singleended" + mea_mode_boundary: str = "external" # TBD: lin/log/sweep From 760f5910f882c149db8254809888187d83bf2825 Mon Sep 17 00:00:00 2001 From: JacobTh98 Date: Tue, 2 Dec 2025 11:29:03 +0100 Subject: [PATCH 27/36] Debugging 1 --- sciopy/EIT_16_32_64_128.py | 23 +++++---- sciopy/com_util.py | 5 ++ sciopy/usb_message_parser.py | 92 +++++++++++++++++------------------- 3 files changed, 62 insertions(+), 58 deletions(-) diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index 02133ed..9305ad0 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -499,9 +499,9 @@ def update_measurement_mode(self, meamode: str = "skip4", boundary: str = "exter print("Option for measurement mode is unknown. Measurement mode ist set to single-ended.") cmd = 0x01 if boundary == "external": - bnd = "0x02" + bnd = 0x02 else: - bnd = "0x01" + bnd = 0x01 self.write_command_string(bytearray([0xB0, 0x03, 0x08, cmd, bnd, 0xB0])) self.print_msg = False #todo read out ACK messages @@ -579,7 +579,7 @@ def StartStopMeasurementFast(self, return_as="pot_mat"): if return_as == "hex": return self.data elif return_as == "pot_mat": - return self.get_data_as_matrix(self.data) + return self.get_data_as_matrix() #todo check def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData:bool=False, bDeleteData:bool=False, @@ -609,29 +609,32 @@ def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData: """ # Start measurement + self.cMessageParser.ppcData=[] + self.send_message(bytearray([0xB4, 0x01, 0x01, 0xB4])) self.cMessageParser.bPrintMessages = False if timeout != 0: - data = self.cMessageParser.read_usb_for_seconds(timeout, bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavepath=sSavepath) + self.cMessageParser.read_usb_for_seconds(timeout, bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, + sSavePath=sSavepath) else: if self.setup.burst_count ==0: print("Burst count for this setup needs to be >=1") return - data = self.cMessageParser.read_usb_till_timeout(bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavepath=sSavepath) + self.cMessageParser.read_usb_till_timeout(bSaveData=bSaveData, bDeleteDataFrame=bDeleteData,sSavePath=sSavepath) # Stop measurement self.send_message(bytearray([0xB4, 0x01, 0x00, 0xB4])) - data2 = self.cMessageParser.read_usb_till_timeout(bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavepath=sSavepath) - data = data + data2 + # self.cMessageParser.ppcData=[] + data = self.cMessageParser.read_usb_till_timeout(bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, + sSavePath=sSavepath) if bDeleteData: return if return_as == "hex": return make_eitframes_hex(data) elif return_as == "pot_mat": return get_data_as_matrix(data) + elif return_as == "eitframe": + return data def get_data_as_matrix(self): """ diff --git a/sciopy/com_util.py b/sciopy/com_util.py index 0477c19..e9d9614 100644 --- a/sciopy/com_util.py +++ b/sciopy/com_util.py @@ -256,6 +256,7 @@ def split_bursts_in_frames( channel depending burst frames """ msg_len = 140 # Constant + iC= 0 frame = [] # Channel group depending frame burst_frame = [] # single burst count frame with channel depending frame subframe_length = split_list.shape[1] // msg_len @@ -266,6 +267,10 @@ def split_bursts_in_frames( # Select the right channel group data if parsed_sgl_frame.channel_group in channel_group: frame.append(parsed_sgl_frame) + + else: + iC+=1 burst_frame.append(frame) frame = [] # Reset channel depending single burst frame + print("UNUSED CG "+ str(iC)) return np.array(burst_frame) diff --git a/sciopy/usb_message_parser.py b/sciopy/usb_message_parser.py index 2d8fbda..298cd3a 100644 --- a/sciopy/usb_message_parser.py +++ b/sciopy/usb_message_parser.py @@ -40,7 +40,7 @@ def byte_parser(): piCurrMess = [] fMesstype = None iCurrLen = 0 - data = yield # Data of dataclass Bytes + data = yield # Data of dataclass Bytes while True: piCurrMess.extend(data) # Starting Message = Message Type # Automatic conversion to integers within list @@ -48,7 +48,7 @@ def byte_parser(): data = yield [] # 2 Byte = Length of Data within message iCurrLen = int.from_bytes(data) - print(iCurrLen) + # print(iCurrLen) #todo raus piCurrMess.extend(data) data = yield [] # Next iCurrLen Bytes = Actual Data Bytes @@ -62,6 +62,7 @@ def byte_parser(): iCurrLen = 0 data = yield piCurrMess # Return fully parsed message as list piCurrMess = [] + #todo doku @@ -161,10 +162,7 @@ def read_usb_for_seconds(self, fTime, bSaveData=False, bDeleteDataFrame=False, s if timeout_count >= 100: # Break if we haven't received any data break - print(f"{iMessageCount} messages received.") - if self.bPrintMessages: - for message in self.ppcData: - print(message) + print(f"{iMessageCount} message(s) received.") return self.ppcData def read_usb_till_timeout(self, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/"): @@ -185,10 +183,7 @@ def read_usb_till_timeout(self, bSaveData=False, bDeleteDataFrame=False, sSaveP # Break if we haven't received any data break - print(f"{iMessageCount} messages received.") - if self.bPrintMessages: - for message in self.ppcData: - print(message) + print(f"{iMessageCount} message(s) received.") return self.ppcData @@ -196,7 +191,7 @@ def save_data_frame(self, path, dataframe): np.savez(path + "eitsample_{0:06d}.npz".format(self.iNPZSaveIndex), # aorta=aorta_segs[j], excitation_stgs =dataframe.excitation_stgs, - frequency_row=dataframe.frequency_row, + frequency_stgs=dataframe.frequency_stgs, timestamp1=dataframe.timestamp1, timestamp2=dataframe.timestamp2, ppcData=dataframe.ppcData) @@ -206,6 +201,7 @@ def save_data_frame(self, path, dataframe): # Message Interpreter Sciospec EIT def interpret_message(self, message, bSaveData=False, bDeleteDataFrame=False, sSavePath= "C/"): + mess_hex = [hex(receive) for receive in message] if message[0] == 180: # DATA 0XB4 self.interpret_data_input(message, bSaveData, bDeleteDataFrame, sSavePath) else: @@ -216,8 +212,8 @@ def interpret_message(self, message, bSaveData=False, bDeleteDataFrame=False, sS 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)) + else: + print("Unknown received message: "+str(mess_hex)) def reset_new_data_frame(self): self.iInjIndex= 0 @@ -237,38 +233,38 @@ def interpret_data_input(self, message, bSave=False, bDeleteFrame=False, sSavePa # EXCITATIONSETTING freq_group = two_byte_to_int(message[5:7]) - if message[2] == 1 and freq_group == 1: - self.CurrentFrame.excitation_stgs[self.iInjIndex] = [message[3], message[4]] - self.iInjIndex += 1 - - # FREQUENCY ROW is set through eitsetup - # self.CurrentFrame.frequency_stgs = self.iNumFreqSettings - - #TIMESTAMP - if self.iSaveCounter == 0: - self.CurrentFrame.timestamp1 = four_byte_to_int(message[7:11]) - - # Data Handling - for i in range(11, 135, 8): - data = complex( - four_byte_to_int(message[i: i + 4]), - four_byte_to_int(message[i + 4: i + 8]), - ) - self.CurrentFrame.ppcData[self.iSaveCounter] = data - self.iSaveCounter += 1 - - if self.iSaveCounter == self.iLenDataperFrame: - # Frame Full - self.CurrentFrame.timestamp2 = four_byte_to_int(message[7:11]) - if bSave: - self.save_data_frame(sSavePath, self.CurrentFrame) - if bDeleteFrame: - del self.CurrentFrame - else: - self.ppcData.append(self.CurrentFrame) - - self.reset_new_data_frame() - # extract + if message[2] <= self.iMaxChannelGroups:# Necessary, since sometimes all four channel groups are send + if message[2] == 1 and freq_group == 1: + self.CurrentFrame.excitation_stgs[self.iInjIndex] = [message[3], message[4]] + self.iInjIndex += 1 + + # FREQUENCY ROW is set through eitsetup + # self.CurrentFrame.frequency_stgs = self.iNumFreqSettings + + #TIMESTAMP + if self.iSaveCounter == 0: + self.CurrentFrame.timestamp1 = four_byte_to_int(message[7:11]) + + # Data Handling + for i in range(11, 135, 8): + data = complex( + four_byte_to_int(message[i: i + 4]), + four_byte_to_int(message[i + 4: i + 8]), + ) + self.CurrentFrame.ppcData[self.iSaveCounter] = data + self.iSaveCounter += 1 + if self.iSaveCounter == self.iLenDataperFrame: + # Frame Full + self.CurrentFrame.timestamp2 = four_byte_to_int(message[7:11]) + if bSave: + self.save_data_frame(sSavePath, self.CurrentFrame) + if bDeleteFrame: + del self.CurrentFrame + else: + self.ppcData.append(self.CurrentFrame) + + self.reset_new_data_frame() + # extract @dataclass class EITFrame: @@ -306,15 +302,15 @@ def __hex__(self): def make_eitframes_hex(FrameList): result= [] for f in FrameList: - result.append(hex(f)) + result.append(hex(f.ppcData)) return result def get_data_as_matrix(FrameList): result= [] for f in FrameList: - L = len(f.ppcData)//(f.excitation_stgs) - result.append(np.reshape(f.ppcData,(f.excitation_stgs, L))) + L = len(f.ppcData)//len(f.excitation_stgs) + result.append(np.reshape(f.ppcData,(len(f.excitation_stgs), L))) return np.array(result) From 2bbd047ff1ac916348afead80f948bad30f6ea01 Mon Sep 17 00:00:00 2001 From: Patricia Fuchs <140624836+patfuchs@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:03:52 +0100 Subject: [PATCH 28/36] Debugging --- sciopy/EIT_16_32_64_128.py | 15 +++++--- sciopy/datatype_conversion.py | 17 +++++++++ sciopy/usb_message_parser.py | 70 +++++++++++++++-------------------- 3 files changed, 56 insertions(+), 46 deletions(-) create mode 100644 sciopy/datatype_conversion.py diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index 9305ad0..53ebc46 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -583,7 +583,7 @@ def StartStopMeasurementFast(self, return_as="pot_mat"): #todo check def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData:bool=False, bDeleteData:bool=False, - sSavepath:str="C/"): + sSavepath:str="C/", bResultsFolder=True): """ 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. @@ -609,24 +609,27 @@ def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData: """ # Start measurement - self.cMessageParser.ppcData=[] + self.cMessageParser.clear_out_data() + if bResultsFolder: + self.cMessageParser.make_results_folder(bResultsFolder, bSaveData, sSavepath) self.send_message(bytearray([0xB4, 0x01, 0x01, 0xB4])) self.cMessageParser.bPrintMessages = False if timeout != 0: self.cMessageParser.read_usb_for_seconds(timeout, bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavePath=sSavepath) + sSavePath=sSavepath,bResultsFolder=False ) else: if self.setup.burst_count ==0: print("Burst count for this setup needs to be >=1") return - self.cMessageParser.read_usb_till_timeout(bSaveData=bSaveData, bDeleteDataFrame=bDeleteData,sSavePath=sSavepath) + self.cMessageParser.read_usb_till_timeout(bSaveData=bSaveData, bDeleteDataFrame=bDeleteData,sSavePath=sSavepath, bResultsFolder=False) # Stop measurement self.send_message(bytearray([0xB4, 0x01, 0x00, 0xB4])) - # self.cMessageParser.ppcData=[] + # All data is returned if wanted data = self.cMessageParser.read_usb_till_timeout(bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavePath=sSavepath) + sSavePath=sSavepath, bResultsFolder=False) + if bDeleteData: return if return_as == "hex": diff --git a/sciopy/datatype_conversion.py b/sciopy/datatype_conversion.py new file mode 100644 index 0000000..9356ca6 --- /dev/null +++ b/sciopy/datatype_conversion.py @@ -0,0 +1,17 @@ +""" +Project :sciopy +Directory: sciopy/sciopy +File : datatype_conversion.py +Author :Patricia Fuchs +Date :03.12.2025 14:02 +""" + + + + +TWOPOWER24 = 16777216 +TWOPOWER16 = 65536 +TWOPOWER8 = 256 + +# -------------------------------------------------------------------------------------------------------------------- # +# -------------------------------------------------------------------------------------------------------------------- # \ No newline at end of file diff --git a/sciopy/usb_message_parser.py b/sciopy/usb_message_parser.py index 298cd3a..52eff20 100644 --- a/sciopy/usb_message_parser.py +++ b/sciopy/usb_message_parser.py @@ -15,6 +15,7 @@ from pandas.core.interchange import dataframe import struct from .sciopy_dataclasses import EitMeasurementSetup +from .com_util import bytesarray_to_float, byteintarray_to_float, two_byte_to_int TWOPOWER24 = 16777216 TWOPOWER16 = 65536 @@ -70,13 +71,14 @@ class MessageParser: def __init__(self, device, eitsetup=None, devicetype="FS"): # General setup - self.bCreateResultsFolder = True + self.bPrintMessages = False self.iNPZSaveIndex = 1 self.iSaveCounter = 0 self.ppiMessages = [] # Unused self.ppcData = [] self.iInjIndex = 0 + self.sCurrentPath="" # Device setup self.cDevice = device @@ -113,6 +115,9 @@ def set_measurement_setup(self, setup: EitMeasurementSetup): # ALL needed self.reset_new_data_frame() + def clear_out_data(self): + self.ppcData = [] + def init_parser(self): self.Parser= byte_parser() @@ -130,14 +135,17 @@ def read_hs(self): def send_hs(self, tosend): self.cDevice.write_data(tosend) - - def read_usb_for_seconds(self, fTime, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/"): - if self.bCreateResultsFolder and bSaveData: + def make_new_folder(self, bCreateResultsFolder,bSaveData, sSavePath): + if bSaveData and bCreateResultsFolder: timestr = time.strftime("%Y%m%d-%H%M%S_eit") path = os.path.join(sSavePath, timestr) os.mkdir(path) - sSavePath = path + "/" - messages = [] + self.sCurrentPath = path + "/" + else: + self.sCurrentPath = sSavePath + + def read_usb_for_seconds(self, fTime, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/", bResultsFolder=False): + self.make_new_folder(bResultsFolder, bSaveData, sSavePath) iMessageCount= 0 bMessageStarted = False timeout_count= 0 @@ -149,7 +157,7 @@ def read_usb_for_seconds(self, fTime, bSaveData=False, bDeleteDataFrame=False, s if len(message) > 0: bMessageStarted = False - self.interpret_message(message, bSaveData,bDeleteDataFrame,sSavePath) + self.interpret_message(message, bSaveData,bDeleteDataFrame,self.sCurrentPath) iMessageCount += 1 else: bMessageStarted = True @@ -165,8 +173,8 @@ def read_usb_for_seconds(self, fTime, bSaveData=False, bDeleteDataFrame=False, s print(f"{iMessageCount} message(s) received.") return self.ppcData - def read_usb_till_timeout(self, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/"): - messages= [] + def read_usb_till_timeout(self, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/", bResultsFolder=False): + self.make_new_folder(bResultsFolder, bSaveData, sSavePath) iMessageCount = 0 timeout_count = 0 while True: @@ -174,7 +182,7 @@ def read_usb_till_timeout(self, bSaveData=False, bDeleteDataFrame=False, sSaveP if buffer: message= self.Parser.send(buffer) if len(message)>0: - self.interpret_message(message, bSaveData,bDeleteDataFrame,sSavePath) + self.interpret_message(message, bSaveData,bDeleteDataFrame,self.sCurrentPath) iMessageCount += 1 timeout_count = 0 continue @@ -243,19 +251,19 @@ def interpret_data_input(self, message, bSave=False, bDeleteFrame=False, sSavePa #TIMESTAMP if self.iSaveCounter == 0: - self.CurrentFrame.timestamp1 = four_byte_to_int(message[7:11]) + self.CurrentFrame.timestamp1 = (message[7:11]) # Data Handling for i in range(11, 135, 8): data = complex( - four_byte_to_int(message[i: i + 4]), - four_byte_to_int(message[i + 4: i + 8]), + byteintarray_to_float(message[i: i + 4]), + byteintarray_to_float(message[i + 4: i + 8]), ) self.CurrentFrame.ppcData[self.iSaveCounter] = data self.iSaveCounter += 1 if self.iSaveCounter == self.iLenDataperFrame: # Frame Full - self.CurrentFrame.timestamp2 = four_byte_to_int(message[7:11]) + self.CurrentFrame.timestamp2 = byteintarray_to_float(message[7:11]) if bSave: self.save_data_frame(sSavePath, self.CurrentFrame) if bDeleteFrame: @@ -266,6 +274,8 @@ def interpret_data_input(self, message, bSave=False, bDeleteFrame=False, sSavePa self.reset_new_data_frame() # extract + +# -------------------------------------------------------------------------------------------------------------------- # @dataclass class EITFrame: """ @@ -299,7 +309,13 @@ def __hex__(self): return hex(self.ppcData) + + + +# -------------------------------------------------------------------------------------------------------------------- # +# -------------------------------------------------------------------------------------------------------------------- # def make_eitframes_hex(FrameList): + #todo, so funktionierts nicht result= [] for f in FrameList: result.append(hex(f.ppcData)) @@ -324,29 +340,3 @@ def load_eit_frames_into_nparray(path): -def four_byte_to_int(bytelist): - return TWOPOWER24*bytelist[0] + TWOPOWER16* bytelist[1] +TWOPOWER8* bytelist[2] + bytelist[3] - -def two_byte_to_int(bytelist): - return TWOPOWER8* bytelist[0] + bytelist[1] - -def bytelist_to_int(bytelist): - r= bytelist[-1] - for j in range(2,len(bytelist)): - r+= bytelist[-j]* 2**((j-1)*8) - return r - - -def fourbytetst(bytes_array): - bytes_array = [int(b, 16) for b in bytes_array] - bytes_array = bytes(bytes_array) - s= struct.unpack("!f", bytes(bytes_array)) - return struct.unpack("!f", bytes(bytes_array))[0] - -def byteintlist_to_int(bytelist): - #faster than four bytes to int at the moment - bytes_array = bytes(bytelist) - return int.from_bytes(bytes_array, byteorder='big') - - - From f1a5c121b945570668789c6f4e060f73aef8db1d Mon Sep 17 00:00:00 2001 From: Patricia Fuchs <140624836+patfuchs@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:04:31 +0100 Subject: [PATCH 29/36] Additional datatype conversions --- sciopy/com_util.py | 108 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 8 deletions(-) diff --git a/sciopy/com_util.py b/sciopy/com_util.py index e9d9614..d783f8b 100644 --- a/sciopy/com_util.py +++ b/sciopy/com_util.py @@ -127,7 +127,7 @@ def length_correction(array: list) -> list: # split in burst count messages split_length = lst.shape[0] // ssms.burst_count for split in range(ssms.burst_count): - split_list.append(lst[split * split_length : (split + 1) * split_length]) + split_list.append(lst[split * split_length: (split + 1) * split_length]) return np.array(split_list) @@ -164,13 +164,50 @@ def bytesarray_to_float(bytes_array: np.ndarray) -> float: Returns ------- float - double precision float + single precision float """ bytes_array = [int(b, 16) for b in bytes_array] bytes_array = bytes(bytes_array) return struct.unpack("!f", bytes(bytes_array))[0] + +def byteintarray_to_float(bytes_array: np.ndarray) -> float: + """ + Converts a bytes array to a float number. Array is array of integers representing bytes. + + Parameters + ---------- + bytes_array : np.ndarray + array of integers former being bytes + + Returns + ------- + float + single precision float + """ + return struct.unpack("!f", bytes(bytes_array))[0] + + +def bytesarray_to_double(bytes_array: np.ndarray) -> float: + """ + Converts a bytes array to a float number. + + Parameters + ---------- + bytes_array : np.ndarray + array of bytes + + Returns + ------- + float + double precision float + """ + bytes_array = [int(b, 16) for b in bytes_array] + bytes_array = bytes(bytes_array) + return struct.unpack("!d", bytes(bytes_array))[0] + + def bytesarray_to_byteslist(bytes_array: np.ndarray) -> list: """ Converts a bytes array to a list of bytes. @@ -207,6 +244,61 @@ def bytesarray_to_int(bytes_array: np.ndarray) -> int: return int.from_bytes(bytes_array, "big") +TWOPOWER24 = 16777216 +TWOPOWER16 = 65536 +TWOPOWER8 = 256 + + +def four_byte_to_int(bytelist): + """ + Converts a list of 4 integers representing bytes to int. + + Parameters + ---------- + bytelist : np.ndarray/list of integers representing bytes MSB first + + Returns + ------- + int + integer number + """ + return TWOPOWER24 * bytelist[0] + TWOPOWER16 * bytelist[1] + TWOPOWER8 * bytelist[2] + bytelist[3] + + +def two_byte_to_int(bytelist): + """ + Converts a list of 2 integers representing bytes to int. + + Parameters + ---------- + bytelist : np.ndarray/list of integers representing bytes MSB first + + Returns + ------- + int + integer number + """ + return TWOPOWER8 * bytelist[0] + bytelist[1] + + +def bytelist_to_int(bytelist): + """ + Converts a list of integers representing bytes MSB first to int. + Parameters + ---------- + bytelist : np.ndarray/list of integers representing bytes MSB first + + Returns + ------- + int + integer number + """ + r = bytelist[-1] + for j in range(2, len(bytelist)): + r += bytelist[-j] * 2 ** ((j - 1) * 8) + return r + + def parse_single_frame(lst_ele: np.ndarray) -> SingleFrame: """ Parse single data to the class SingleFrame. @@ -226,8 +318,8 @@ def parse_single_frame(lst_ele: np.ndarray) -> SingleFrame: for i in range(11, 135, 8): enum += 1 channels[f"ch_{enum}"] = complex( - bytesarray_to_float(lst_ele[i : i + 4]), - bytesarray_to_float(lst_ele[i + 4 : i + 8]), + bytesarray_to_float(lst_ele[i: i + 4]), + bytesarray_to_float(lst_ele[i + 4: i + 8]), ) excitation_stgs = np.array([single_hex_to_int(ele) for ele in lst_ele[3:5]]) @@ -245,7 +337,7 @@ def parse_single_frame(lst_ele: np.ndarray) -> SingleFrame: def split_bursts_in_frames( - split_list: np.ndarray, burst_count: int, channel_group: list + split_list: np.ndarray, burst_count: int, channel_group: list ) -> np.ndarray: """ Takes the splitted list from `reshape_full_message_in_bursts()` and parses the single frames. @@ -256,7 +348,7 @@ def split_bursts_in_frames( channel depending burst frames """ msg_len = 140 # Constant - iC= 0 + iC = 0 frame = [] # Channel group depending frame burst_frame = [] # single burst count frame with channel depending frame subframe_length = split_list.shape[1] // msg_len @@ -269,8 +361,8 @@ def split_bursts_in_frames( frame.append(parsed_sgl_frame) else: - iC+=1 + iC += 1 burst_frame.append(frame) frame = [] # Reset channel depending single burst frame - print("UNUSED CG "+ str(iC)) + print("UNUSED CG " + str(iC)) return np.array(burst_frame) From 6af9b9849cbe4d125d21bb0de71c48ca5d880557 Mon Sep 17 00:00:00 2001 From: Patricia Fuchs <140624836+patfuchs@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:07:27 +0100 Subject: [PATCH 30/36] Refactoring of all datatype conversion into new file --- sciopy/com_util.py | 190 +------------------------------- sciopy/datatype_conversion.py | 197 +++++++++++++++++++++++++++++++++- 2 files changed, 195 insertions(+), 192 deletions(-) diff --git a/sciopy/com_util.py b/sciopy/com_util.py index d783f8b..c775538 100644 --- a/sciopy/com_util.py +++ b/sciopy/com_util.py @@ -13,7 +13,7 @@ import struct import sys from glob import glob - +from .datatype_conversion import * def available_serial_ports() -> list: """ @@ -66,27 +66,6 @@ def clTbt_dp(val: float) -> list: return [int(ele) for ele in struct.pack(">d", val)] -def del_hex_in_list(lst: list) -> np.ndarray: - """ - Delete the hexadecimal 0x python notation. - - Parameters - ---------- - lst : list - list of hexadecimals - - Returns - ------- - np.ndarray - cleared message - """ - return np.array( - [ - "0" + ele.replace("0x", "") if len(ele) == 1 else ele.replace("0x", "") - for ele in lst - ] - ) - def reshape_full_message_in_bursts(lst: list, ssms: EitMeasurementSetup) -> np.ndarray: """ @@ -131,173 +110,6 @@ def length_correction(array: list) -> list: return np.array(split_list) -def single_hex_to_int(str_num: str) -> int: - """ - Delete the hexadecimal 0x python notation. - - Parameters - ---------- - str_num : str - single hexadecimal string - - Returns - ------- - int - integer number - """ - if len(str_num) == 1: - str_num = f"0x0{str_num}" - else: - str_num = f"0x{str_num}" - return int(str_num, 16) - - -def bytesarray_to_float(bytes_array: np.ndarray) -> float: - """ - Converts a bytes array to a float number. - - Parameters - ---------- - bytes_array : np.ndarray - array of bytes - - Returns - ------- - float - single precision float - """ - bytes_array = [int(b, 16) for b in bytes_array] - bytes_array = bytes(bytes_array) - return struct.unpack("!f", bytes(bytes_array))[0] - - - -def byteintarray_to_float(bytes_array: np.ndarray) -> float: - """ - Converts a bytes array to a float number. Array is array of integers representing bytes. - - Parameters - ---------- - bytes_array : np.ndarray - array of integers former being bytes - - Returns - ------- - float - single precision float - """ - return struct.unpack("!f", bytes(bytes_array))[0] - - -def bytesarray_to_double(bytes_array: np.ndarray) -> float: - """ - Converts a bytes array to a float number. - - Parameters - ---------- - bytes_array : np.ndarray - array of bytes - - Returns - ------- - float - double precision float - """ - bytes_array = [int(b, 16) for b in bytes_array] - bytes_array = bytes(bytes_array) - return struct.unpack("!d", bytes(bytes_array))[0] - - -def bytesarray_to_byteslist(bytes_array: np.ndarray) -> list: - """ - Converts a bytes array to a list of bytes. - - Parameters - ---------- - bytes_array : np.ndarray - array of bytes - - Returns - ------- - list - list of bytes - """ - bytes_array = [int(b, 16) for b in bytes_array] - return bytes(bytes_array) - - -def bytesarray_to_int(bytes_array: np.ndarray) -> int: - """ - Converts a bytes array to int number. - - Parameters - ---------- - bytes_array : np.ndarray - array of bytes - - Returns - ------- - int - integer number - """ - bytes_array = bytesarray_to_byteslist(bytes_array) - return int.from_bytes(bytes_array, "big") - - -TWOPOWER24 = 16777216 -TWOPOWER16 = 65536 -TWOPOWER8 = 256 - - -def four_byte_to_int(bytelist): - """ - Converts a list of 4 integers representing bytes to int. - - Parameters - ---------- - bytelist : np.ndarray/list of integers representing bytes MSB first - - Returns - ------- - int - integer number - """ - return TWOPOWER24 * bytelist[0] + TWOPOWER16 * bytelist[1] + TWOPOWER8 * bytelist[2] + bytelist[3] - - -def two_byte_to_int(bytelist): - """ - Converts a list of 2 integers representing bytes to int. - - Parameters - ---------- - bytelist : np.ndarray/list of integers representing bytes MSB first - - Returns - ------- - int - integer number - """ - return TWOPOWER8 * bytelist[0] + bytelist[1] - - -def bytelist_to_int(bytelist): - """ - Converts a list of integers representing bytes MSB first to int. - Parameters - ---------- - bytelist : np.ndarray/list of integers representing bytes MSB first - - Returns - ------- - int - integer number - """ - r = bytelist[-1] - for j in range(2, len(bytelist)): - r += bytelist[-j] * 2 ** ((j - 1) * 8) - return r - def parse_single_frame(lst_ele: np.ndarray) -> SingleFrame: """ diff --git a/sciopy/datatype_conversion.py b/sciopy/datatype_conversion.py index 9356ca6..8821847 100644 --- a/sciopy/datatype_conversion.py +++ b/sciopy/datatype_conversion.py @@ -6,12 +6,203 @@ Date :03.12.2025 14:02 """ - - +import struct +import numpy as np TWOPOWER24 = 16777216 TWOPOWER16 = 65536 TWOPOWER8 = 256 # -------------------------------------------------------------------------------------------------------------------- # -# -------------------------------------------------------------------------------------------------------------------- # \ No newline at end of file +# -------------------------------------------------------------------------------------------------------------------- # +def del_hex_in_list(lst: list) -> np.ndarray: + """ + Delete the hexadecimal 0x python notation. + + Parameters + ---------- + lst : list + list of hexadecimals + + Returns + ------- + np.ndarray + cleared message + """ + return np.array( + [ + "0" + ele.replace("0x", "") if len(ele) == 1 else ele.replace("0x", "") + for ele in lst + ] + ) + + +# -------------------------------------------------------------------------------------------------------------------- # +def single_hex_to_int(str_num: str) -> int: + """ + Delete the hexadecimal 0x python notation. + + Parameters + ---------- + str_num : str + single hexadecimal string + + Returns + ------- + int + integer number + """ + if len(str_num) == 1: + str_num = f"0x0{str_num}" + else: + str_num = f"0x{str_num}" + return int(str_num, 16) + + +# -------------------------------------------------------------------------------------------------------------------- # +def bytesarray_to_float(bytes_array: np.ndarray) -> float: + """ + Converts a bytes array to a float number. + + Parameters + ---------- + bytes_array : np.ndarray + array of bytes + + Returns + ------- + float + single precision float + """ + bytes_array = [int(b, 16) for b in bytes_array] + bytes_array = bytes(bytes_array) + return struct.unpack("!f", bytes(bytes_array))[0] + + +# -------------------------------------------------------------------------------------------------------------------- # +def byteintarray_to_float(bytes_array: np.ndarray) -> float: + """ + Converts a bytes array to a float number. Array is array of integers representing bytes. + + Parameters + ---------- + bytes_array : np.ndarray + array of integers former being bytes + + Returns + ------- + float + single precision float + """ + return struct.unpack("!f", bytes(bytes_array))[0] + + +# -------------------------------------------------------------------------------------------------------------------- # +def bytesarray_to_double(bytes_array: np.ndarray) -> float: + """ + Converts a bytes array to a float number. + + Parameters + ---------- + bytes_array : np.ndarray + array of bytes + + Returns + ------- + float + double precision float + """ + bytes_array = [int(b, 16) for b in bytes_array] + bytes_array = bytes(bytes_array) + return struct.unpack("!d", bytes(bytes_array))[0] + + +# -------------------------------------------------------------------------------------------------------------------- # +def bytesarray_to_byteslist(bytes_array: np.ndarray) -> list: + """ + Converts a bytes array to a list of bytes. + + Parameters + ---------- + bytes_array : np.ndarray + array of bytes + + Returns + ------- + list + list of bytes + """ + bytes_array = [int(b, 16) for b in bytes_array] + return bytes(bytes_array) + + +# -------------------------------------------------------------------------------------------------------------------- # +def bytesarray_to_int(bytes_array: np.ndarray) -> int: + """ + Converts a bytes array to int number. + + Parameters + ---------- + bytes_array : np.ndarray + array of bytes + + Returns + ------- + int + integer number + """ + bytes_array = bytesarray_to_byteslist(bytes_array) + return int.from_bytes(bytes_array, "big") + + +# -------------------------------------------------------------------------------------------------------------------- # +def four_byte_to_int(bytelist): + """ + Converts a list of 4 integers representing bytes to int. + + Parameters + ---------- + bytelist : np.ndarray/list of integers representing bytes MSB first + + Returns + ------- + int + integer number + """ + return TWOPOWER24 * bytelist[0] + TWOPOWER16 * bytelist[1] + TWOPOWER8 * bytelist[2] + bytelist[3] + + +# -------------------------------------------------------------------------------------------------------------------- # +def two_byte_to_int(bytelist): + """ + Converts a list of 2 integers representing bytes to int. + + Parameters + ---------- + bytelist : np.ndarray/list of integers representing bytes MSB first + + Returns + ------- + int + integer number + """ + return TWOPOWER8 * bytelist[0] + bytelist[1] + + +# -------------------------------------------------------------------------------------------------------------------- # +def bytelist_to_int(bytelist): + """ + Converts a list of integers representing bytes MSB first to int. + Parameters + ---------- + bytelist : np.ndarray/list of integers representing bytes MSB first + + Returns + ------- + int + integer number + """ + r = bytelist[-1] + for j in range(2, len(bytelist)): + r += bytelist[-j] * 2 ** ((j - 1) * 8) + return r From b78068d8b315a7962523d4708e93cb17c53f39a7 Mon Sep 17 00:00:00 2001 From: Patricia Fuchs <140624836+patfuchs@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:40:08 +0100 Subject: [PATCH 31/36] Documentation, Refactoring and new data saving functions --- sciopy/EIT_16_32_64_128.py | 18 +- sciopy/sciopy_dataclasses.py | 32 ++- sciopy/usb_message_parser.py | 387 ++++++++++++++++++++++------------- 3 files changed, 282 insertions(+), 155 deletions(-) diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index 53ebc46..e3356e8 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -29,7 +29,8 @@ } from .sciopy_dataclasses import EitMeasurementSetup -from sciopydev.sciopy.usb_message_parser import MessageParser, make_eitframes_hex, get_data_as_matrix +from sciopydev.sciopy.usb_message_parser import (MessageParser, make_eitframes_hex, get_data_as_matrix, + make_results_folder) class EIT_16_32_64_128: @@ -581,7 +582,7 @@ def StartStopMeasurementFast(self, return_as="pot_mat"): elif return_as == "pot_mat": return self.get_data_as_matrix() - #todo check + def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData:bool=False, bDeleteData:bool=False, sSavepath:str="C/", bResultsFolder=True): """ @@ -599,8 +600,8 @@ def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData: - "pot_mat": Returns the processed data as a matrix using `get_data_as_matrix()`. Default is "pot_mat". - else: data is only stored - bSaveData (bool): Specifies if a the measured data is saved in NPZ format - bDeleteData (bool): Specifies if a the measured data is deleted out of memory after each EITframe, with + bSaveData (bool): Specifies if the measured data is saved in NPZ format + bDeleteData (bool): Specifies if the measured data is deleted out of memory after each EITframe, with bSaveData=True, measured data is saved and then removed from RAM sSavepath (str): Specifies the sPath where the measured data is saved. @@ -610,14 +611,13 @@ def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData: # Start measurement self.cMessageParser.clear_out_data() - if bResultsFolder: - self.cMessageParser.make_results_folder(bResultsFolder, bSaveData, sSavepath) + sCurrentPath= make_results_folder(bResultsFolder, bSaveData, sSavepath) # No new path is created if bResultsFolder=False self.send_message(bytearray([0xB4, 0x01, 0x01, 0xB4])) self.cMessageParser.bPrintMessages = False if timeout != 0: self.cMessageParser.read_usb_for_seconds(timeout, bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavePath=sSavepath,bResultsFolder=False ) + sSavePath=sCurrentPath,bResultsFolder=False ) else: if self.setup.burst_count ==0: print("Burst count for this setup needs to be >=1") @@ -628,7 +628,7 @@ def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData: self.send_message(bytearray([0xB4, 0x01, 0x00, 0xB4])) # All data is returned if wanted data = self.cMessageParser.read_usb_till_timeout(bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavePath=sSavepath, bResultsFolder=False) + sSavePath=sCurrentPath, bResultsFolder=False) if bDeleteData: return @@ -639,6 +639,8 @@ def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData: elif return_as == "eitframe": return data + + def get_data_as_matrix(self): """ Converts the raw EIT data into a 3D matrix of potentials. diff --git a/sciopy/sciopy_dataclasses.py b/sciopy/sciopy_dataclasses.py index 9bd1a01..0a19d5f 100644 --- a/sciopy/sciopy_dataclasses.py +++ b/sciopy/sciopy_dataclasses.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from typing import List, Tuple, Union - +import numpy.typing as npt @dataclass class EitMeasurementSetup: @@ -211,3 +211,33 @@ class PreperationConfig: lpath: str spath: str n_samples: int + + + +# -------------------------------------------------------------------------------------------------------------------- # +@dataclass +class EITFrame: + """ + This class is for parsing the whole EIT excitation stages (and frequency stages). + Defined by Sciospec:"EIT -16,32,64,128", Chapter 5.4.1 + + Parameters + ---------- + n_el = Number of used electrodes + excitation_stgs : np.array [[int1, int2]] , Features the [ESout, ESin] injection electrodes + frequency_stgs : List[str] # todo + timestamp1 : int Timestamp of the very first measured channel group in this frame, milli seconds? + timestamp2 : int Timestamp of the very last measured channel group in this frame, milli seconds? + ppcData : np.array [[Complex measured data]] + for e in used excitations stages, + for f in used frequency stages: + for c in all used channels: -> insert complex value + """ + n_el: int # Number of used electrodes + excitation_stgs: npt.NDArray[int] # Num Excitation Settings X 2 + frequency_stgs: npt.NDArray[int] # List of Frequency-Sweep Settings, + timestamp1: int # [ms] + timestamp2: int + ppcData: npt.NDArray[complex] # Channels 1-(64) all channel groups combined + + diff --git a/sciopy/usb_message_parser.py b/sciopy/usb_message_parser.py index 52eff20..73b6361 100644 --- a/sciopy/usb_message_parser.py +++ b/sciopy/usb_message_parser.py @@ -1,6 +1,6 @@ """ -Project :quali_effects_eit_measurements -Directory: src +Project :sciopy +Directory: sciopy/sciopy File : usb_message_parser.py Author :Patricia Fuchs Date :17.11.2025 09:04 @@ -14,12 +14,9 @@ import os from pandas.core.interchange import dataframe import struct -from .sciopy_dataclasses import EitMeasurementSetup +from .sciopy_dataclasses import EitMeasurementSetup, EITFrame from .com_util import bytesarray_to_float, byteintarray_to_float, two_byte_to_int -TWOPOWER24 = 16777216 -TWOPOWER16 = 65536 -TWOPOWER8 = 256 # -------------------------------------------------------------------------------------------------------------------- # # -------------------------------------------------------------------------------------------------------------------- # msg_dict = { @@ -35,50 +32,56 @@ } +# -------------------------------------------------------------------------------------------------------------------- # def byte_parser(): - # Return empty lists, while message is incomplete, else full usb-message as list [int[hex-format], int [hex-format], ..] + """ + Generator to parse each input byte by byte + + Returns: + Empty lists, while message is incomplete, else full usb-message as list [int[hex-format], int [hex-format], ..] + """ # Initialization piCurrMess = [] fMesstype = None iCurrLen = 0 - data = yield # Data of dataclass Bytes + data = yield # Data of dataclass Bytes while True: - piCurrMess.extend(data) # Starting Message = Message Type - # Automatic conversion to integers within list - fMesstype = data # Save the Byte + piCurrMess.extend(data) # Starting Message = Message Type + # Automatic conversion to integers within list + fMesstype = data # Save the Byte - data = yield [] # 2 Byte = Length of Data within message + data = yield [] # 2 Byte = Length of Data within message iCurrLen = int.from_bytes(data) - # print(iCurrLen) #todo raus piCurrMess.extend(data) - data = yield [] # Next iCurrLen Bytes = Actual Data Bytes + data = yield [] # Next iCurrLen Bytes = Actual Data Bytes for i in range(iCurrLen): piCurrMess.extend(data) data = yield [] - if fMesstype != data: # Last Byte != Message Type - print(f"Current message not complete for Starting messagetype {hex(fMesstype)} and ending type {hex(data[0])}") + if fMesstype != data: # Last Byte != Message Type + print( + f"Current message not complete for Starting messagetype {hex(fMesstype)} and ending type {hex(data[0])}") piCurrMess.extend(data) iCurrLen = 0 - data = yield piCurrMess # Return fully parsed message as list + data = yield piCurrMess # Return fully parsed message as list piCurrMess = [] - -#todo doku +# -------------------------------------------------------------------------------------------------------------------- # +# -------------------------------------------------------------------------------------------------------------------- # class MessageParser: + """ + Parses byte wise USB messages from an Sciospec EIT Device and sorts them according to the message type + """ + def __init__(self, device, eitsetup=None, devicetype="FS"): # General setup - - self.bPrintMessages = False self.iNPZSaveIndex = 1 - self.iSaveCounter = 0 - self.ppiMessages = [] # Unused + self.iSaveCounter = 0 # Unused self.ppcData = [] self.iInjIndex = 0 - self.sCurrentPath="" # Device setup self.cDevice = device @@ -91,7 +94,6 @@ def __init__(self, device, eitsetup=None, devicetype="FS"): self.device_read = self.read_hs self.init_parser() - # Setup related changes self.CurrentFrame = None self.iMaxChannelGroups = 1 @@ -99,57 +101,113 @@ def __init__(self, device, eitsetup=None, devicetype="FS"): self.iNumFreqSettings = 1 self.iLenDataperFrame = 1 self.iMessagesperFrame = 1 - self.setup= eitsetup + self.setup = eitsetup self.set_measurement_setup(eitsetup) - + # ---------------------------------------------------------------------------------------------------------------- # def set_measurement_setup(self, setup: EitMeasurementSetup): + """ + Gets EIT Setup and sets up the data frame accordingly + Args: + setup: EITMeasurementSetup object + """ self.setup = setup if setup != None: - self.iMaxChannelGroups = setup.n_el//16 - self.iNumExcitationSettings = setup.n_el # todo should be independently set - self.iNumFreqSettings = 1 # todo + self.iMaxChannelGroups = setup.n_el // 16 + self.iNumExcitationSettings = setup.n_el # todo should be independently set + self.iNumFreqSettings = 1 # todo self.iLenDataperFrame = self.iMaxChannelGroups * 16 * self.iNumExcitationSettings * self.iNumFreqSettings self.iMessagesperFrame = self.iMaxChannelGroups * self.iNumExcitationSettings * self.iNumFreqSettings # ALL needed self.reset_new_data_frame() + # ---------------------------------------------------------------------------------------------------------------- # + def reset_new_data_frame(self): + """ + Resets the Current EITFrame. + """ + self.iInjIndex = 0 + self.iSaveCounter = 0 + self.CurrentFrame = EITFrame(n_el=self.setup.n_el, + excitation_stgs=np.zeros((self.iNumExcitationSettings, 2), dtype=int), + frequency_stgs=np.zeros((self.iNumFreqSettings,), dtype=int), + # todo fill in setup freq settings + timestamp1=0, + timestamp2=0, + ppcData=np.zeros(self.iMaxChannelGroups * 16 * self.iNumExcitationSettings, + dtype=complex)) + + # ---------------------------------------------------------------------------------------------------------------- # def clear_out_data(self): + """ + Deletes saved data frames + """ self.ppcData = [] - + # ---------------------------------------------------------------------------------------------------------------- # def init_parser(self): - self.Parser= byte_parser() + """ + Initializes the parser generator + """ + self.Parser = byte_parser() next(self.Parser) + # ---------------------------------------------------------------------------------------------------------------- # def read_fs(self): + """ + Read out USB connected via FS protocol + Returns: + Byte read from USB + """ return self.cDevice.read() + # ---------------------------------------------------------------------------------------------------------------- # def send_fs(self, tosend): + """ + Sends a message over the USB connected via FS protocol + Args: + tosend: list/array of integers to be sent over the USB connected via FS protocol + """ self.cDevice.write(tosend) + # ---------------------------------------------------------------------------------------------------------------- # def read_hs(self): + """ + Read out USB connected via HS protocol + Returns: + Byte read from USB + """ return self.cDevice.read_data_bytes(size=1024, attempt=150) + # ---------------------------------------------------------------------------------------------------------------- # def send_hs(self, tosend): + """ + Sends a message over the USB connected via HS protocol + Args: + tosend: list/array of integers to be sent over the USB connected via HS protocol + """ self.cDevice.write_data(tosend) - def make_new_folder(self, bCreateResultsFolder,bSaveData, sSavePath): - if bSaveData and bCreateResultsFolder: - timestr = time.strftime("%Y%m%d-%H%M%S_eit") - path = os.path.join(sSavePath, timestr) - os.mkdir(path) - self.sCurrentPath = path + "/" - else: - self.sCurrentPath = sSavePath - - def read_usb_for_seconds(self, fTime, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/", bResultsFolder=False): - self.make_new_folder(bResultsFolder, bSaveData, sSavePath) - iMessageCount= 0 + # ---------------------------------------------------------------------------------------------------------------- # + def read_usb_for_seconds(self, fTime: float, bSaveData: bool = False, bDeleteDataFrame: bool = False, + sSavePath: str = "C/"): + """ + Reads out the USB connection for fTime seconds, regardless of whether data is received. Data bytes are parsed, + sorted into full messages and then handled according to their Command Tag. Status or requested information is + displayed if wished and measured EIT data is stored, deleted or returned. + Args: + fTime(float): time to read out usb connection (in seconds) + bSaveData: if data should be saved + bDeleteDataFrame: if data frame is deleted after saving data + sSavePath: Path where the data should be saved + Returns: + List of received data eit frames, no Status messages are saved + """ + iMessageCount = 0 bMessageStarted = False - timeout_count= 0 - fEndtime = time.time()+fTime + timeout_count = 0 + fEndtime = time.time() + fTime while time.time() < fEndtime or bMessageStarted: buffer = self.device_read() if buffer: @@ -157,7 +215,7 @@ def read_usb_for_seconds(self, fTime, bSaveData=False, bDeleteDataFrame=False, s if len(message) > 0: bMessageStarted = False - self.interpret_message(message, bSaveData,bDeleteDataFrame,self.sCurrentPath) + self.interpret_message(message, bSaveData, bDeleteDataFrame, sSavePath) iMessageCount += 1 else: bMessageStarted = True @@ -173,16 +231,27 @@ def read_usb_for_seconds(self, fTime, bSaveData=False, bDeleteDataFrame=False, s print(f"{iMessageCount} message(s) received.") return self.ppcData - def read_usb_till_timeout(self, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/", bResultsFolder=False): - self.make_new_folder(bResultsFolder, bSaveData, sSavePath) + # ---------------------------------------------------------------------------------------------------------------- # + def read_usb_till_timeout(self, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/"): + """ + Reads out the USB connection until the connections times out, so for messages received + timeout. Data bytes are parsed, + sorted into full messages and then handled according to their Command Tag. Status or requested information is + displayed if wished and measured EIT data is stored, deleted or returned. + Args: + bSaveData: if data should be saved + bDeleteDataFrame: if data frame is deleted after saving data + sSavePath: Path where the data should be saved + Returns: + List of received data eit frames, no Status messages are saved + """ iMessageCount = 0 timeout_count = 0 while True: buffer = self.device_read() if buffer: - message= self.Parser.send(buffer) - if len(message)>0: - self.interpret_message(message, bSaveData,bDeleteDataFrame,self.sCurrentPath) + message = self.Parser.send(buffer) + if len(message) > 0: + self.interpret_message(message, bSaveData, bDeleteDataFrame, sSavePath) iMessageCount += 1 timeout_count = 0 continue @@ -194,66 +263,56 @@ def read_usb_till_timeout(self, bSaveData=False, bDeleteDataFrame=False, sSaveP print(f"{iMessageCount} message(s) received.") return self.ppcData - - def save_data_frame(self, path, dataframe): - np.savez(path + "eitsample_{0:06d}.npz".format(self.iNPZSaveIndex), - # aorta=aorta_segs[j], - excitation_stgs =dataframe.excitation_stgs, - frequency_stgs=dataframe.frequency_stgs, - timestamp1=dataframe.timestamp1, - timestamp2=dataframe.timestamp2, - ppcData=dataframe.ppcData) - self.iNPZSaveIndex+=1 - - - - # Message Interpreter Sciospec EIT - def interpret_message(self, message, bSaveData=False, bDeleteDataFrame=False, sSavePath= "C/"): - mess_hex = [hex(receive) for receive in message] - if message[0] == 180: # DATA 0XB4 + # ---------------------------------------------------------------------------------------------------------------- # + def interpret_message(self, message, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/"): + """ + Message interpreter for USB messages from the Sciospec EIT Device. Status messages or requested information is + displayed, recorded EIT data is separetely stored. + Args: + message: [Byte1:int, Byte2:int, ...], structured like [Command Tag, Message Length, Message Info, Command Tag] + bSaveData: When the message is EIT data, if it should be saved + bDeleteDataFrame: When the message is EIT data, if it should be deleted from RAM after saving + sSavePath: When the message is EIT data, save path + """ + if message[0] == 180: # DATA 0XB4 self.interpret_data_input(message, bSaveData, bDeleteDataFrame, sSavePath) else: mess_hex = [hex(receive) for receive in message] if self.bPrintMessages: - if message[0] == 24: # 0x24 Acknowledgement Message + if message[0] == 24: # 0x24 Acknowledgement Message try: - print("Message: " +str(mess_hex) +" -> "+ msg_dict[mess_hex[2]]) + print("Message: " + str(mess_hex) + " -> " + msg_dict[mess_hex[2]]) except: - print("Message: " +str(mess_hex) +" -> "+msg_dict["0x01"]) + print("Message: " + str(mess_hex) + " -> " + msg_dict["0x01"]) else: - print("Unknown received message: "+str(mess_hex)) - - def reset_new_data_frame(self): - self.iInjIndex= 0 - self.iSaveCounter= 0 - self.CurrentFrame = EITFrame(channel_group=self.iMaxChannelGroups, - excitation_stgs=np.zeros((self.iNumExcitationSettings, 2), dtype=int), - frequency_stgs=np.zeros((self.iNumFreqSettings,), dtype=int), # todo fill in setup freq settings - timestamp1=0, - timestamp2= 0, - ppcData= np.zeros(self.iMaxChannelGroups * 16 * self.iNumExcitationSettings, dtype= complex)) - - + print("Unknown received message: " + str(mess_hex)) + # ---------------------------------------------------------------------------------------------------------------- # def interpret_data_input(self, message, bSave=False, bDeleteFrame=False, sSavePath="C/"): - # Look after channel group and if data already started, append - # ChannelGROUP - + """ + Interpreter of received messages with measured data. + Args: + message: Received message + bSave: If data should be saved + bDeleteFrame: If data should be deleted from RAM after saving + sSavePath: Save path + """ # EXCITATIONSETTING freq_group = two_byte_to_int(message[5:7]) - if message[2] <= self.iMaxChannelGroups:# Necessary, since sometimes all four channel groups are send + if message[2] <= self.iMaxChannelGroups: # Necessary, since all four channel groups are send if message[2] == 1 and freq_group == 1: self.CurrentFrame.excitation_stgs[self.iInjIndex] = [message[3], message[4]] self.iInjIndex += 1 - - # FREQUENCY ROW is set through eitsetup - # self.CurrentFrame.frequency_stgs = self.iNumFreqSettings - - #TIMESTAMP + + # FREQUENCY ROW is set through eitsetup + #TODO input not the number of the frequency row, but all injected frequencies, beforehand + # self.CurrentFrame.frequency_stgs = self.iNumFreqSettings + + # TIMESTAMP if self.iSaveCounter == 0: self.CurrentFrame.timestamp1 = (message[7:11]) - # Data Handling + # Data Handling for i in range(11, 135, 8): data = complex( byteintarray_to_float(message[i: i + 4]), @@ -265,78 +324,114 @@ def interpret_data_input(self, message, bSave=False, bDeleteFrame=False, sSavePa # Frame Full self.CurrentFrame.timestamp2 = byteintarray_to_float(message[7:11]) if bSave: - self.save_data_frame(sSavePath, self.CurrentFrame) + save_data_frame(sSavePath, self.CurrentFrame, self.iNPZSaveIndex) + self.iNPZSaveIndex += 1 if bDeleteFrame: del self.CurrentFrame else: - self.ppcData.append(self.CurrentFrame) - + self.ppcData.append(self.CurrentFrame) self.reset_new_data_frame() - # extract - - -# -------------------------------------------------------------------------------------------------------------------- # -@dataclass -class EITFrame: - """ - This class is for parsing the whole EIT excitation stages. - - Parameters - ---------- - start_tag : str - has to be 'b4' - channel_group : int - channel group: CG=1 -> Channels 1-16, CG=2 -> Channels 17-32 up to CG=4 - excitation_stgs : List[str] - excitation setting: [ESout, ESin] - frequency_row : List[str] - frequency row - timestamp : int - milli seconds - - end_tag : str - has to be 'b4' - """ - - channel_group: int # todo weglassen - excitation_stgs: npt.NDArray[int] # Num Excitation Settings X 2 - frequency_stgs: npt.NDArray[int] # List of Frequency-Sweep Settings, - timestamp1: int # [ms] - timestamp2: int - ppcData: npt.NDArray[complex] # Channels 1-(64) all channel groups combined - - def __hex__(self): - return hex(self.ppcData) - - # -------------------------------------------------------------------------------------------------------------------- # # -------------------------------------------------------------------------------------------------------------------- # def make_eitframes_hex(FrameList): - #todo, so funktionierts nicht - result= [] + #todo, not working + result = [] for f in FrameList: result.append(hex(f.ppcData)) return result +# -------------------------------------------------------------------------------------------------------------------- # +def make_results_folder(bCreateResultsFolder: bool, bSaveData: bool, sSavePath: str): + """ + Creates a new data results folder, if data should be saved. Then stores the path in the class + Args: + bCreateResultsFolder: If folder shall be created + bSaveData(bool): If data should be saved, folder is only created if data should be saved + sSavePath(str): Path where the folder should be created + """ + if bSaveData and bCreateResultsFolder: + timestr = time.strftime("%Y%m%d-%H%M%S_eit") + path = os.path.join(sSavePath, timestr) + os.mkdir(path) + return path + "/" + else: + return sSavePath + + +# -------------------------------------------------------------------------------------------------------------------- # def get_data_as_matrix(FrameList): - result= [] + """ + List of EITFrames to be reshaped into matrix of [Number frames, num injection settings, n_el] + Args: + FrameList: List of EITFrames to be reshaped into matrix + + Returns: + np.array of eit data of shape [Number frames, num injection settings, n_el] + """ + result = [] for f in FrameList: - L = len(f.ppcData)//len(f.excitation_stgs) - result.append(np.reshape(f.ppcData,(len(f.excitation_stgs), L))) + L = len(f.ppcData) // len(f.excitation_stgs) + result.append(np.reshape(f.ppcData, (len(f.excitation_stgs), L))) return np.array(result) -def load_eit_frames(path,names): - # Load NPZ data into list of (EITFrame) - pass +# -------------------------------------------------------------------------------------------------------------------- # +def save_data_frame(path: str, dataframe: EITFrame, iNPZSaveIndex:int): + """ + Saves a single EIT frame in a npz-file. Based on the EITframe class. Saves it at self.NPZSaveIndex + Args: + path: Where to save the EIT frame + dataframe: EITFrame to be saved + iNPZSaveIndex: Index of the EIT frame to be saved + """ + np.savez(path + "eitsample_{0:06d}.npz".format(iNPZSaveIndex), + n_el=dataframe.n_el, + excitation_stgs=dataframe.excitation_stgs, + frequency_stgs=dataframe.frequency_stgs, + timestamp1=dataframe.timestamp1, + timestamp2=dataframe.timestamp2, + ppcData=dataframe.ppcData) + -def load_eit_frames_into_nparray(path): - # Loads NPZ Data into nparray of [NumFrames X Sizeof Frequency and Excitation Settings] - pass +# -------------------------------------------------------------------------------------------------------------------- # +def load_eit_frames(path): + """ + Loads NPZ eit frames and stores them in a list of EITFrame + Args: + path: Path of the NPZ eit frames + + Returns: + List of EITFrame + """ + loaded = [] + files = os.listdir(path) + files = sorted(files) + for f in files: + l = np.load(os.path.join(path, f), allow_pickle=True) + e = EITFrame(n_el=l['n_el'], excitation_stgs=l['excitation_stgs'], + frequency_stgs=l['frequency_stgs'], + timestamp1=l['timestamp1'], + timestamp2=l['timestamp2'], + ppcData=l['ppcData']) + loaded.append(e) + return loaded +# -------------------------------------------------------------------------------------------------------------------- # +def load_eit_frames_into_nparray(path): + """ + Load NPZ eit frames, retrieves the complex data and stores it in a numpy array. + Args: + path: Path of the NPZ eit frames + Returns: np.array(ppcData) + """ + loaded= load_eit_frames(path) + l = [] + for frame in loaded: + l.append(frame.ppcData) + return np.array(l) From 3e584d94f99d76808e3487ca5cb1c56fbc12963e Mon Sep 17 00:00:00 2001 From: Patricia Fuchs <140624836+patfuchs@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:45:41 +0100 Subject: [PATCH 32/36] Black formatting --- sciopy/EIT_16_32_64_128.py | 80 ++++++++++++++------ sciopy/com_util.py | 11 ++- sciopy/datatype_conversion.py | 8 +- sciopy/device_interface.py | 55 ++++++++++++++ sciopy/sciopy_dataclasses.py | 5 +- sciopy/usb_message_parser.py | 134 ++++++++++++++++++++++------------ 6 files changed, 212 insertions(+), 81 deletions(-) create mode 100644 sciopy/device_interface.py diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index e3356e8..80833e1 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -29,8 +29,12 @@ } from .sciopy_dataclasses import EitMeasurementSetup -from sciopydev.sciopy.usb_message_parser import (MessageParser, make_eitframes_hex, get_data_as_matrix, - make_results_folder) +from sciopydev.sciopy.usb_message_parser import ( + MessageParser, + make_eitframes_hex, + get_data_as_matrix, + make_results_folder, +) class EIT_16_32_64_128: @@ -152,7 +156,6 @@ def disconnect_device(self): """ self.device.close() - def send_message(self, message): """ Wrapper function to send a byte array to the device. Communication method is based on the defined serial @@ -166,7 +169,6 @@ def send_message(self, message): elif self.serial_protocol == "FS": self.device.write(message) - def read_message(self): """ Wrapper function to read single bytes from the device. Communication method is based on the defined serial @@ -253,7 +255,9 @@ def write_command_string(self, command): """ self.cMessageParser.bPrintMessages = self.print_msg self.send_message(command) - self.cMessageParser.read_usb_till_timeout(bSaveData=False, bDeleteDataFrame=True) + self.cMessageParser.read_usb_till_timeout( + bSaveData=False, bDeleteDataFrame=True + ) # --- sciospec device commands @@ -482,8 +486,9 @@ def ResetMeasurementSetup(self): self.write_command_string(bytearray([0xB0, 0x01, 0x01, 0xB0])) self.print_msg = False - - def update_measurement_mode(self, meamode: str = "skip4", boundary: str = "external"): + def update_measurement_mode( + self, meamode: str = "skip4", boundary: str = "external" + ): """ Updates the measurement modes out of the options "singleended", "skip0", "skip2" or "skip4" @@ -492,12 +497,19 @@ def update_measurement_mode(self, meamode: str = "skip4", boundary: str = "exter boundary (str): Return if the boundary for skip patterns is internal of a channel group or external for all optional channels """ - meamodeoptions = {"singleended": 0x01, "skip0": 0x02, "skip2": 0x03, "skip4": 0x04} + meamodeoptions = { + "singleended": 0x01, + "skip0": 0x02, + "skip2": 0x03, + "skip4": 0x04, + } self.print_msg = True try: cmd = meamodeoptions[meamode] except: - print("Option for measurement mode is unknown. Measurement mode ist set to single-ended.") + print( + "Option for measurement mode is unknown. Measurement mode ist set to single-ended." + ) cmd = 0x01 if boundary == "external": bnd = 0x02 @@ -505,7 +517,7 @@ def update_measurement_mode(self, meamode: str = "skip4", boundary: str = "exter bnd = 0x01 self.write_command_string(bytearray([0xB0, 0x03, 0x08, cmd, bnd, 0xB0])) self.print_msg = False - #todo read out ACK messages + # todo read out ACK messages def GetMeasurementSetup(self, setup_of: str): """ @@ -542,7 +554,7 @@ def GetMeasurementSetup(self, setup_of: str): print("TBD: Translation") self.print_msg = False - #todo + # todo def StartStopMeasurementFast(self, return_as="pot_mat"): """ Starts and stops a measurement process using the configured serial protocol (HS or FS). @@ -582,9 +594,15 @@ def StartStopMeasurementFast(self, return_as="pot_mat"): elif return_as == "pot_mat": return self.get_data_as_matrix() - - def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData:bool=False, bDeleteData:bool=False, - sSavepath:str="C/", bResultsFolder=True): + def StartStopMeasurementNew( + self, + timeout: int = 0, + return_as="pot_mat", + bSaveData: bool = False, + bDeleteData: bool = False, + sSavepath: str = "C/", + bResultsFolder=True, + ): """ 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. @@ -611,24 +629,40 @@ def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData: # Start measurement self.cMessageParser.clear_out_data() - sCurrentPath= make_results_folder(bResultsFolder, bSaveData, sSavepath) # No new path is created if bResultsFolder=False - + sCurrentPath = make_results_folder( + bResultsFolder, bSaveData, sSavepath + ) # No new path is created if bResultsFolder=False + self.send_message(bytearray([0xB4, 0x01, 0x01, 0xB4])) self.cMessageParser.bPrintMessages = False if timeout != 0: - self.cMessageParser.read_usb_for_seconds(timeout, bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavePath=sCurrentPath,bResultsFolder=False ) + self.cMessageParser.read_usb_for_seconds( + timeout, + bSaveData=bSaveData, + bDeleteDataFrame=bDeleteData, + sSavePath=sCurrentPath, + bResultsFolder=False, + ) else: - if self.setup.burst_count ==0: + if self.setup.burst_count == 0: print("Burst count for this setup needs to be >=1") return - self.cMessageParser.read_usb_till_timeout(bSaveData=bSaveData, bDeleteDataFrame=bDeleteData,sSavePath=sSavepath, bResultsFolder=False) + self.cMessageParser.read_usb_till_timeout( + bSaveData=bSaveData, + bDeleteDataFrame=bDeleteData, + sSavePath=sSavepath, + bResultsFolder=False, + ) # Stop measurement self.send_message(bytearray([0xB4, 0x01, 0x00, 0xB4])) # All data is returned if wanted - data = self.cMessageParser.read_usb_till_timeout(bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavePath=sCurrentPath, bResultsFolder=False) + data = self.cMessageParser.read_usb_till_timeout( + bSaveData=bSaveData, + bDeleteDataFrame=bDeleteData, + sSavePath=sCurrentPath, + bResultsFolder=False, + ) if bDeleteData: return @@ -639,8 +673,6 @@ def StartStopMeasurementNew(self, timeout:int=0, return_as="pot_mat", bSaveData: elif return_as == "eitframe": return data - - def get_data_as_matrix(self): """ Converts the raw EIT data into a 3D matrix of potentials. diff --git a/sciopy/com_util.py b/sciopy/com_util.py index c775538..89851e1 100644 --- a/sciopy/com_util.py +++ b/sciopy/com_util.py @@ -15,6 +15,7 @@ from glob import glob from .datatype_conversion import * + def available_serial_ports() -> list: """ Lists serial port names. @@ -66,7 +67,6 @@ def clTbt_dp(val: float) -> list: return [int(ele) for ele in struct.pack(">d", val)] - def reshape_full_message_in_bursts(lst: list, ssms: EitMeasurementSetup) -> np.ndarray: """ Takes the full message buffer and splits this message depeding on the measurement configuration into the @@ -106,11 +106,10 @@ def length_correction(array: list) -> list: # split in burst count messages split_length = lst.shape[0] // ssms.burst_count for split in range(ssms.burst_count): - split_list.append(lst[split * split_length: (split + 1) * split_length]) + split_list.append(lst[split * split_length : (split + 1) * split_length]) return np.array(split_list) - def parse_single_frame(lst_ele: np.ndarray) -> SingleFrame: """ Parse single data to the class SingleFrame. @@ -130,8 +129,8 @@ def parse_single_frame(lst_ele: np.ndarray) -> SingleFrame: for i in range(11, 135, 8): enum += 1 channels[f"ch_{enum}"] = complex( - bytesarray_to_float(lst_ele[i: i + 4]), - bytesarray_to_float(lst_ele[i + 4: i + 8]), + bytesarray_to_float(lst_ele[i : i + 4]), + bytesarray_to_float(lst_ele[i + 4 : i + 8]), ) excitation_stgs = np.array([single_hex_to_int(ele) for ele in lst_ele[3:5]]) @@ -149,7 +148,7 @@ def parse_single_frame(lst_ele: np.ndarray) -> SingleFrame: def split_bursts_in_frames( - split_list: np.ndarray, burst_count: int, channel_group: list + split_list: np.ndarray, burst_count: int, channel_group: list ) -> np.ndarray: """ Takes the splitted list from `reshape_full_message_in_bursts()` and parses the single frames. diff --git a/sciopy/datatype_conversion.py b/sciopy/datatype_conversion.py index 8821847..c177847 100644 --- a/sciopy/datatype_conversion.py +++ b/sciopy/datatype_conversion.py @@ -13,6 +13,7 @@ TWOPOWER16 = 65536 TWOPOWER8 = 256 + # -------------------------------------------------------------------------------------------------------------------- # # -------------------------------------------------------------------------------------------------------------------- # def del_hex_in_list(lst: list) -> np.ndarray: @@ -169,7 +170,12 @@ def four_byte_to_int(bytelist): int integer number """ - return TWOPOWER24 * bytelist[0] + TWOPOWER16 * bytelist[1] + TWOPOWER8 * bytelist[2] + bytelist[3] + return ( + TWOPOWER24 * bytelist[0] + + TWOPOWER16 * bytelist[1] + + TWOPOWER8 * bytelist[2] + + bytelist[3] + ) # -------------------------------------------------------------------------------------------------------------------- # diff --git a/sciopy/device_interface.py b/sciopy/device_interface.py new file mode 100644 index 0000000..a753f0e --- /dev/null +++ b/sciopy/device_interface.py @@ -0,0 +1,55 @@ +""" +Project :sciopy +Directory: sciopy/sciopy +File : device_interface.py +Author :Patricia Fuchs +Date :26.11.2025 14:04 +""" + +try: + import serial +except ImportError: + print("Could not import module: serial") + +# -------------------------------------------------------------------------------------------------------------------- # +# -------------------------------------------------------------------------------------------------------------------- # + + +class DeviceInterface: + def __init__(self): + self.sProtocol = "None" + + def send_data(self, data): + pass + + def read_data(self): + return None + + +import serial + + +class USB_FS_Device(DeviceInterface): + def __init__(self, port: str, baudrate: int = 9600, timeout: int = 9000): + super().__init__() + self.sProtocol = "FS" + self.device = serial.Serial( + port=port, + baudrate=baudrate, + timeout=timeout, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + bytesize=serial.EIGHTBITS, + ) + self.name = self.device.name + + def send_data(self, data): + self.device.write(data) + + def read_data(self): + return self.device.read() + + +class USB_HS_Device(DeviceInterface): + def __init__(self, port: str, baudrate: int = 9600, timeout: int = 9000): + super().__init__() diff --git a/sciopy/sciopy_dataclasses.py b/sciopy/sciopy_dataclasses.py index 0a19d5f..ba99bba 100644 --- a/sciopy/sciopy_dataclasses.py +++ b/sciopy/sciopy_dataclasses.py @@ -4,6 +4,7 @@ from typing import List, Tuple, Union import numpy.typing as npt + @dataclass class EitMeasurementSetup: """ @@ -213,7 +214,6 @@ class PreperationConfig: n_samples: int - # -------------------------------------------------------------------------------------------------------------------- # @dataclass class EITFrame: @@ -233,11 +233,10 @@ class EITFrame: for f in used frequency stages: for c in all used channels: -> insert complex value """ + n_el: int # Number of used electrodes excitation_stgs: npt.NDArray[int] # Num Excitation Settings X 2 frequency_stgs: npt.NDArray[int] # List of Frequency-Sweep Settings, timestamp1: int # [ms] timestamp2: int ppcData: npt.NDArray[complex] # Channels 1-(64) all channel groups combined - - diff --git a/sciopy/usb_message_parser.py b/sciopy/usb_message_parser.py index 73b6361..82b51f4 100644 --- a/sciopy/usb_message_parser.py +++ b/sciopy/usb_message_parser.py @@ -5,6 +5,7 @@ Author :Patricia Fuchs Date :17.11.2025 09:04 """ + import numpy as np import time @@ -61,7 +62,8 @@ def byte_parser(): if fMesstype != data: # Last Byte != Message Type print( - f"Current message not complete for Starting messagetype {hex(fMesstype)} and ending type {hex(data[0])}") + f"Current message not complete for Starting messagetype {hex(fMesstype)} and ending type {hex(data[0])}" + ) piCurrMess.extend(data) iCurrLen = 0 data = yield piCurrMess # Return fully parsed message as list @@ -114,10 +116,19 @@ def set_measurement_setup(self, setup: EitMeasurementSetup): self.setup = setup if setup != None: self.iMaxChannelGroups = setup.n_el // 16 - self.iNumExcitationSettings = setup.n_el # todo should be independently set - self.iNumFreqSettings = 1 # todo - self.iLenDataperFrame = self.iMaxChannelGroups * 16 * self.iNumExcitationSettings * self.iNumFreqSettings - self.iMessagesperFrame = self.iMaxChannelGroups * self.iNumExcitationSettings * self.iNumFreqSettings + self.iNumExcitationSettings = setup.n_el # todo should be independently set + self.iNumFreqSettings = 1 # todo + self.iLenDataperFrame = ( + self.iMaxChannelGroups + * 16 + * self.iNumExcitationSettings + * self.iNumFreqSettings + ) + self.iMessagesperFrame = ( + self.iMaxChannelGroups + * self.iNumExcitationSettings + * self.iNumFreqSettings + ) # ALL needed self.reset_new_data_frame() @@ -129,14 +140,17 @@ def reset_new_data_frame(self): """ self.iInjIndex = 0 self.iSaveCounter = 0 - self.CurrentFrame = EITFrame(n_el=self.setup.n_el, - excitation_stgs=np.zeros((self.iNumExcitationSettings, 2), dtype=int), - frequency_stgs=np.zeros((self.iNumFreqSettings,), dtype=int), - # todo fill in setup freq settings - timestamp1=0, - timestamp2=0, - ppcData=np.zeros(self.iMaxChannelGroups * 16 * self.iNumExcitationSettings, - dtype=complex)) + self.CurrentFrame = EITFrame( + n_el=self.setup.n_el, + excitation_stgs=np.zeros((self.iNumExcitationSettings, 2), dtype=int), + frequency_stgs=np.zeros((self.iNumFreqSettings,), dtype=int), + # todo fill in setup freq settings + timestamp1=0, + timestamp2=0, + ppcData=np.zeros( + self.iMaxChannelGroups * 16 * self.iNumExcitationSettings, dtype=complex + ), + ) # ---------------------------------------------------------------------------------------------------------------- # def clear_out_data(self): @@ -190,8 +204,13 @@ def send_hs(self, tosend): self.cDevice.write_data(tosend) # ---------------------------------------------------------------------------------------------------------------- # - def read_usb_for_seconds(self, fTime: float, bSaveData: bool = False, bDeleteDataFrame: bool = False, - sSavePath: str = "C/"): + def read_usb_for_seconds( + self, + fTime: float, + bSaveData: bool = False, + bDeleteDataFrame: bool = False, + sSavePath: str = "C/", + ): """ Reads out the USB connection for fTime seconds, regardless of whether data is received. Data bytes are parsed, sorted into full messages and then handled according to their Command Tag. Status or requested information is @@ -215,7 +234,9 @@ def read_usb_for_seconds(self, fTime: float, bSaveData: bool = False, bDeleteDat if len(message) > 0: bMessageStarted = False - self.interpret_message(message, bSaveData, bDeleteDataFrame, sSavePath) + self.interpret_message( + message, bSaveData, bDeleteDataFrame, sSavePath + ) iMessageCount += 1 else: bMessageStarted = True @@ -232,7 +253,9 @@ def read_usb_for_seconds(self, fTime: float, bSaveData: bool = False, bDeleteDat return self.ppcData # ---------------------------------------------------------------------------------------------------------------- # - def read_usb_till_timeout(self, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/"): + def read_usb_till_timeout( + self, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/" + ): """ Reads out the USB connection until the connections times out, so for messages received + timeout. Data bytes are parsed, sorted into full messages and then handled according to their Command Tag. Status or requested information is @@ -251,7 +274,9 @@ def read_usb_till_timeout(self, bSaveData=False, bDeleteDataFrame=False, sSavePa if buffer: message = self.Parser.send(buffer) if len(message) > 0: - self.interpret_message(message, bSaveData, bDeleteDataFrame, sSavePath) + self.interpret_message( + message, bSaveData, bDeleteDataFrame, sSavePath + ) iMessageCount += 1 timeout_count = 0 continue @@ -264,7 +289,9 @@ def read_usb_till_timeout(self, bSaveData=False, bDeleteDataFrame=False, sSavePa return self.ppcData # ---------------------------------------------------------------------------------------------------------------- # - def interpret_message(self, message, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/"): + def interpret_message( + self, message, bSaveData=False, bDeleteDataFrame=False, sSavePath="C/" + ): """ Message interpreter for USB messages from the Sciospec EIT Device. Status messages or requested information is displayed, recorded EIT data is separetely stored. @@ -281,14 +308,18 @@ def interpret_message(self, message, bSaveData=False, bDeleteDataFrame=False, sS if self.bPrintMessages: if message[0] == 24: # 0x24 Acknowledgement Message try: - print("Message: " + str(mess_hex) + " -> " + msg_dict[mess_hex[2]]) + print( + "Message: " + str(mess_hex) + " -> " + msg_dict[mess_hex[2]] + ) except: print("Message: " + str(mess_hex) + " -> " + msg_dict["0x01"]) else: print("Unknown received message: " + str(mess_hex)) # ---------------------------------------------------------------------------------------------------------------- # - def interpret_data_input(self, message, bSave=False, bDeleteFrame=False, sSavePath="C/"): + def interpret_data_input( + self, message, bSave=False, bDeleteFrame=False, sSavePath="C/" + ): """ Interpreter of received messages with measured data. Args: @@ -299,24 +330,29 @@ def interpret_data_input(self, message, bSave=False, bDeleteFrame=False, sSavePa """ # EXCITATIONSETTING freq_group = two_byte_to_int(message[5:7]) - if message[2] <= self.iMaxChannelGroups: # Necessary, since all four channel groups are send + if ( + message[2] <= self.iMaxChannelGroups + ): # Necessary, since all four channel groups are send if message[2] == 1 and freq_group == 1: - self.CurrentFrame.excitation_stgs[self.iInjIndex] = [message[3], message[4]] + self.CurrentFrame.excitation_stgs[self.iInjIndex] = [ + message[3], + message[4], + ] self.iInjIndex += 1 - # FREQUENCY ROW is set through eitsetup - #TODO input not the number of the frequency row, but all injected frequencies, beforehand - # self.CurrentFrame.frequency_stgs = self.iNumFreqSettings + # FREQUENCY ROW is set through eitsetup + # TODO input not the number of the frequency row, but all injected frequencies, beforehand + # self.CurrentFrame.frequency_stgs = self.iNumFreqSettings - # TIMESTAMP + # TIMESTAMP if self.iSaveCounter == 0: - self.CurrentFrame.timestamp1 = (message[7:11]) + self.CurrentFrame.timestamp1 = message[7:11] - # Data Handling + # Data Handling for i in range(11, 135, 8): data = complex( - byteintarray_to_float(message[i: i + 4]), - byteintarray_to_float(message[i + 4: i + 8]), + byteintarray_to_float(message[i : i + 4]), + byteintarray_to_float(message[i + 4 : i + 8]), ) self.CurrentFrame.ppcData[self.iSaveCounter] = data self.iSaveCounter += 1 @@ -333,11 +369,10 @@ def interpret_data_input(self, message, bSave=False, bDeleteFrame=False, sSavePa self.reset_new_data_frame() - # -------------------------------------------------------------------------------------------------------------------- # # -------------------------------------------------------------------------------------------------------------------- # def make_eitframes_hex(FrameList): - #todo, not working + # todo, not working result = [] for f in FrameList: result.append(hex(f.ppcData)) @@ -380,7 +415,7 @@ def get_data_as_matrix(FrameList): # -------------------------------------------------------------------------------------------------------------------- # -def save_data_frame(path: str, dataframe: EITFrame, iNPZSaveIndex:int): +def save_data_frame(path: str, dataframe: EITFrame, iNPZSaveIndex: int): """ Saves a single EIT frame in a npz-file. Based on the EITframe class. Saves it at self.NPZSaveIndex Args: @@ -388,13 +423,15 @@ def save_data_frame(path: str, dataframe: EITFrame, iNPZSaveIndex:int): dataframe: EITFrame to be saved iNPZSaveIndex: Index of the EIT frame to be saved """ - np.savez(path + "eitsample_{0:06d}.npz".format(iNPZSaveIndex), - n_el=dataframe.n_el, - excitation_stgs=dataframe.excitation_stgs, - frequency_stgs=dataframe.frequency_stgs, - timestamp1=dataframe.timestamp1, - timestamp2=dataframe.timestamp2, - ppcData=dataframe.ppcData) + np.savez( + path + "eitsample_{0:06d}.npz".format(iNPZSaveIndex), + n_el=dataframe.n_el, + excitation_stgs=dataframe.excitation_stgs, + frequency_stgs=dataframe.frequency_stgs, + timestamp1=dataframe.timestamp1, + timestamp2=dataframe.timestamp2, + ppcData=dataframe.ppcData, + ) # -------------------------------------------------------------------------------------------------------------------- # @@ -412,11 +449,14 @@ def load_eit_frames(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'], - ppcData=l['ppcData']) + e = EITFrame( + n_el=l["n_el"], + excitation_stgs=l["excitation_stgs"], + frequency_stgs=l["frequency_stgs"], + timestamp1=l["timestamp1"], + timestamp2=l["timestamp2"], + ppcData=l["ppcData"], + ) loaded.append(e) return loaded @@ -430,7 +470,7 @@ def load_eit_frames_into_nparray(path): Returns: np.array(ppcData) """ - loaded= load_eit_frames(path) + loaded = load_eit_frames(path) l = [] for frame in loaded: l.append(frame.ppcData) From 16bfa2528c48ac754effd3fd7582996a65dc481d Mon Sep 17 00:00:00 2001 From: Patricia Fuchs <140624836+patfuchs@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:55:03 +0100 Subject: [PATCH 33/36] Updated timestamp saving --- sciopy/EIT_16_32_64_128.py | 7 ++++--- sciopy/sciopy_dataclasses.py | 5 ++++- sciopy/usb_message_parser.py | 5 +++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index 80833e1..d41e8ca 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -418,8 +418,9 @@ def SetMeasurementSetup(self, setup: EitMeasurementSetup): elif setup.gain == 1_000: self.write_command_string(bytearray([0xB0, 0x03, 0x09, 0x01, 0x03, 0xB0])) - # Single ended mode: - self.write_command_string(bytearray([0xB0, 0x03, 0x08, 0x01, 0x01, 0xB0])) + # Single ended mode as standard setup, if else configured, skip patterns are possible: + self.update_measurement_mode(setup.mea_mode, boundary=setup.mea_mode_boundary) + #self.write_command_string(bytearray([0xB0, 0x03, 0x08, 0x01, 0x01, 0xB0])) # Excitation switch type: self.write_command_string(bytearray([0xB0, 0x02, 0x0C, 0x01, 0xB0])) @@ -487,7 +488,7 @@ def ResetMeasurementSetup(self): self.print_msg = False def update_measurement_mode( - self, meamode: str = "skip4", boundary: str = "external" + self, meamode: str = "singleended", boundary: str = "internal" ): """ Updates the measurement modes out of the options "singleended", "skip0", "skip2" or "skip4" diff --git a/sciopy/sciopy_dataclasses.py b/sciopy/sciopy_dataclasses.py index ba99bba..400fef2 100644 --- a/sciopy/sciopy_dataclasses.py +++ b/sciopy/sciopy_dataclasses.py @@ -30,7 +30,7 @@ class EitMeasurementSetup: gain: int adc_range: int mea_mode: str = "singleended" - mea_mode_boundary: str = "external" + mea_mode_boundary: str = "internal" # TBD: lin/log/sweep @@ -228,6 +228,8 @@ class EITFrame: frequency_stgs : List[str] # todo timestamp1 : int Timestamp of the very first measured channel group in this frame, milli seconds? timestamp2 : int Timestamp of the very last measured channel group in this frame, milli seconds? + timestamp_pc : int Timestamp of the receiving computer for further data synchronisation from datetime.now(). + timestamp() ppcData : np.array [[Complex measured data]] for e in used excitations stages, for f in used frequency stages: @@ -239,4 +241,5 @@ class EITFrame: frequency_stgs: npt.NDArray[int] # List of Frequency-Sweep Settings, timestamp1: int # [ms] timestamp2: int + timestamp_pc: int ppcData: npt.NDArray[complex] # Channels 1-(64) all channel groups combined diff --git a/sciopy/usb_message_parser.py b/sciopy/usb_message_parser.py index 82b51f4..0c8dad9 100644 --- a/sciopy/usb_message_parser.py +++ b/sciopy/usb_message_parser.py @@ -17,6 +17,7 @@ import struct from .sciopy_dataclasses import EitMeasurementSetup, EITFrame from .com_util import bytesarray_to_float, byteintarray_to_float, two_byte_to_int +from datatime import datetime # -------------------------------------------------------------------------------------------------------------------- # # -------------------------------------------------------------------------------------------------------------------- # @@ -147,6 +148,7 @@ def reset_new_data_frame(self): # todo fill in setup freq settings timestamp1=0, timestamp2=0, + timestamp_pc=0, ppcData=np.zeros( self.iMaxChannelGroups * 16 * self.iNumExcitationSettings, dtype=complex ), @@ -347,6 +349,7 @@ def interpret_data_input( # TIMESTAMP if self.iSaveCounter == 0: self.CurrentFrame.timestamp1 = message[7:11] + self.CurrentFrame.timestamp_pc = datetime.now().timestamp() # Data Handling for i in range(11, 135, 8): @@ -430,6 +433,7 @@ def save_data_frame(path: str, dataframe: EITFrame, iNPZSaveIndex: int): frequency_stgs=dataframe.frequency_stgs, timestamp1=dataframe.timestamp1, timestamp2=dataframe.timestamp2, + timestamp_pc = dataframe.timestamp_pc, ppcData=dataframe.ppcData, ) @@ -455,6 +459,7 @@ def load_eit_frames(path): frequency_stgs=l["frequency_stgs"], timestamp1=l["timestamp1"], timestamp2=l["timestamp2"], + timestamp_pc=l["timestamp_pc"], ppcData=l["ppcData"], ) loaded.append(e) From 0becb7e0aa8260c624a9f0e86cc792b19c58ca8a Mon Sep 17 00:00:00 2001 From: Patricia Fuchs <140624836+patfuchs@users.noreply.github.com> Date: Fri, 19 Dec 2025 21:05:13 +0100 Subject: [PATCH 34/36] Updating --- sciopy/EIT_16_32_64_128.py | 15 ++++++--------- sciopy/usb_message_parser.py | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index d41e8ca..3c155fc 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -595,14 +595,14 @@ def StartStopMeasurementFast(self, return_as="pot_mat"): elif return_as == "pot_mat": return self.get_data_as_matrix() - def StartStopMeasurementNew( + def StartStopMeasurement( self, timeout: int = 0, return_as="pot_mat", bSaveData: bool = False, bDeleteData: bool = False, - sSavepath: str = "C/", - bResultsFolder=True, + sSavePath: str = "C/", + bResultsFolder=False, ): """ Starts and stops a measurement process using the configured serial protocol (HS or FS). @@ -631,7 +631,7 @@ def StartStopMeasurementNew( # Start measurement self.cMessageParser.clear_out_data() sCurrentPath = make_results_folder( - bResultsFolder, bSaveData, sSavepath + bResultsFolder, bSaveData, sSavePath ) # No new path is created if bResultsFolder=False self.send_message(bytearray([0xB4, 0x01, 0x01, 0xB4])) @@ -641,8 +641,7 @@ def StartStopMeasurementNew( timeout, bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavePath=sCurrentPath, - bResultsFolder=False, + sSavePath=sCurrentPath ) else: if self.setup.burst_count == 0: @@ -651,8 +650,7 @@ def StartStopMeasurementNew( self.cMessageParser.read_usb_till_timeout( bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavePath=sSavepath, - bResultsFolder=False, + sSavePath=sCurrentPath ) # Stop measurement @@ -662,7 +660,6 @@ def StartStopMeasurementNew( bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, sSavePath=sCurrentPath, - bResultsFolder=False, ) if bDeleteData: diff --git a/sciopy/usb_message_parser.py b/sciopy/usb_message_parser.py index 0c8dad9..22644b6 100644 --- a/sciopy/usb_message_parser.py +++ b/sciopy/usb_message_parser.py @@ -17,7 +17,7 @@ import struct from .sciopy_dataclasses import EitMeasurementSetup, EITFrame from .com_util import bytesarray_to_float, byteintarray_to_float, two_byte_to_int -from datatime import datetime +from datetime import datetime # -------------------------------------------------------------------------------------------------------------------- # # -------------------------------------------------------------------------------------------------------------------- # From d263b59ab46f614ee3b8c27b128359a6eff829fa Mon Sep 17 00:00:00 2001 From: Patricia Fuchs <140624836+patfuchs@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:29:49 +0100 Subject: [PATCH 35/36] Black formatting --- sciopy/EIT_16_32_64_128.py | 6 +++--- sciopy/usb_message_parser.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index 3c155fc..9bbd446 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -420,7 +420,7 @@ def SetMeasurementSetup(self, setup: EitMeasurementSetup): # Single ended mode as standard setup, if else configured, skip patterns are possible: self.update_measurement_mode(setup.mea_mode, boundary=setup.mea_mode_boundary) - #self.write_command_string(bytearray([0xB0, 0x03, 0x08, 0x01, 0x01, 0xB0])) + # self.write_command_string(bytearray([0xB0, 0x03, 0x08, 0x01, 0x01, 0xB0])) # Excitation switch type: self.write_command_string(bytearray([0xB0, 0x02, 0x0C, 0x01, 0xB0])) @@ -641,7 +641,7 @@ def StartStopMeasurement( timeout, bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavePath=sCurrentPath + sSavePath=sCurrentPath, ) else: if self.setup.burst_count == 0: @@ -650,7 +650,7 @@ def StartStopMeasurement( self.cMessageParser.read_usb_till_timeout( bSaveData=bSaveData, bDeleteDataFrame=bDeleteData, - sSavePath=sCurrentPath + sSavePath=sCurrentPath, ) # Stop measurement diff --git a/sciopy/usb_message_parser.py b/sciopy/usb_message_parser.py index 22644b6..90dbdb3 100644 --- a/sciopy/usb_message_parser.py +++ b/sciopy/usb_message_parser.py @@ -433,7 +433,7 @@ def save_data_frame(path: str, dataframe: EITFrame, iNPZSaveIndex: int): frequency_stgs=dataframe.frequency_stgs, timestamp1=dataframe.timestamp1, timestamp2=dataframe.timestamp2, - timestamp_pc = dataframe.timestamp_pc, + timestamp_pc=dataframe.timestamp_pc, ppcData=dataframe.ppcData, ) From 9975a52d9cf9f7a6c5a29bbc5a57f5bf81f5b0b5 Mon Sep 17 00:00:00 2001 From: Patricia Fuchs <140624836+patfuchs@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:41:06 +0100 Subject: [PATCH 36/36] Example for message parser --- examples/EIT-16-256-MessageParser.ipynb | 261 ++++++++++++++++++++++++ sciopy/EIT_16_32_64_128.py | 4 +- sciopy/__init__.py | 2 + 3 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 examples/EIT-16-256-MessageParser.ipynb diff --git a/examples/EIT-16-256-MessageParser.ipynb b/examples/EIT-16-256-MessageParser.ipynb new file mode 100644 index 0000000..209d4d3 --- /dev/null +++ b/examples/EIT-16-256-MessageParser.ipynb @@ -0,0 +1,261 @@ +{ + "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", + "# 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(return_as=\"eitframe\", bSaveData=False, bDeleteData=False,\n", + " sSavePath=savepath, bResultsFolder=False)\n", + "print(data.shape)\n", + "\n", + "# 2) Continuous measurement\n", + "sciospec.update_BurstCount(0)\n", + "data= sciospec.StartStopMeasurement(timeout= 5,return_as=\"eitframe\", bSaveData=False, bDeleteData=False,\n", + " sSavePath=savepath,bResultsFolder=False) # measured for five seconds\n", + "print(data.shape)\n", + "\n", + "\n", + "\n", + "# 3) Save data in real time and create an additional folder for storage\n", + "data= sciospec.StartStopMeasurement(timeout= 5,return_as=\"eitframe\", bSaveData=True, bDeleteData=False,\n", + " sSavePath=savepath,bResultsFolder=True) # measured for five seconds\n", + "print(data.shape)\n", + "\n", + "\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(bCreateResultsFolder=True, bSaveData=True, sSavePath=savepath)\n", + "sciospec.StartStopMeasurement(timeout= 5,return_as=\"eitframe\", bSaveData=True, bDeleteData=True,\n", + " sSavePath=sCurrPath,bResultsFolder=False) # measured for five seconds\n", + "time.sleep(3)\n", + "sciospec.StartStopMeasurement(timeout= 5,return_as=\"eitframe\", bSaveData=True, bDeleteData=True,\n", + " sSavePath=sCurrPath,bResultsFolder=False) # measured for five seconds\n", + "time.sleep(3)\n", + "sciospec.StartStopMeasurement(timeout= 5,return_as=\"eitframe\", bSaveData=True, bDeleteData=True,\n", + " sSavePath=sCurrPath,bResultsFolder=False) # measured for five seconds\n", + "\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, \n" + ], + "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(return_as=\"eitframe\", bSaveData=False, bDeleteData=False,\n", + " sSavePath=savepath, bResultsFolder=False)\n", + "data_pot=np.abs(data[2])\n", + "\n" + ], + "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(return_as=\"eitframe\", bSaveData=False, bDeleteData=False,\n", + " sSavePath=savepath, bResultsFolder=False)\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": [ + "\n", + "# 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)\n", + "\n", + "\n", + "\n" + ], + "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/sciopy/EIT_16_32_64_128.py b/sciopy/EIT_16_32_64_128.py index 9bbd446..c2ba788 100644 --- a/sciopy/EIT_16_32_64_128.py +++ b/sciopy/EIT_16_32_64_128.py @@ -555,7 +555,6 @@ def GetMeasurementSetup(self, setup_of: str): print("TBD: Translation") self.print_msg = False - # todo def StartStopMeasurementFast(self, return_as="pot_mat"): """ Starts and stops a measurement process using the configured serial protocol (HS or FS). @@ -622,7 +621,8 @@ def StartStopMeasurement( bSaveData (bool): Specifies if the measured data is saved in NPZ format bDeleteData (bool): Specifies if the measured data is deleted out of memory after each EITframe, with bSaveData=True, measured data is saved and then removed from RAM - sSavepath (str): Specifies the sPath where the measured data is saved. + sSavePath (str): Specifies the sPath where the measured data is saved. + bResultsFolder (bool): Specifies if additionally a folder in sSavePath is created to store the data in Returns: list or matrix: The measurement data in the format specified by `return_as`. diff --git a/sciopy/__init__.py b/sciopy/__init__.py index 1bda5a2..f8bebb4 100644 --- a/sciopy/__init__.py +++ b/sciopy/__init__.py @@ -6,6 +6,7 @@ from .EIT_16_32_64_128 import EIT_16_32_64_128, EitMeasurementSetup from .ISX_3 import ISX_3, EisMeasurementSetup +from .usb_message_parser import make_results_folder __all__ = [ @@ -14,4 +15,5 @@ "EitMeasurementSetup", "ISX_3", "EisMeasurementSetup", + "make_results_folder", ]