From e52fbf4db54222e43d871ae35144cbae5d05f644 Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Thu, 8 Jan 2026 22:11:50 +0100 Subject: [PATCH 1/2] Bug fixes for LayerMidLevel.get_current_data() and SerialPortConnection._read_intern() --- HINTS.md | 2 ++ README.md | 5 ++++ .../mid_level/example_mid_level_update.py | 23 +++++++++++++++---- pyproject.toml | 2 +- .../mid_level/mid_level_current_data.py | 11 ++++++++- .../mid_level/mid_level_layer.py | 11 +++++---- .../utils/serial_port_connection.py | 9 ++++++-- 7 files changed, 49 insertions(+), 14 deletions(-) diff --git a/HINTS.md b/HINTS.md index eaee21e..b234ded 100644 --- a/HINTS.md +++ b/HINTS.md @@ -23,11 +23,13 @@ This page describes implementation details. - Handles extraction of packets from byte stream - Most functions communicating with the device are async functions using name schema _xxx_, because they wait for a matching acknowledge and return values from acknowledge - If no matching acknowledge or no acknowledge arrives in time, an exception is raised + - If a command has an error information and an error is set, an exception is raised - The async functions connection buffer handling is always identical: - Clear buffer - Send command - Process incoming data until the expected acknowledge arrives - More data remains in connection buffer + - Do not call async functions in parallel (e.g. from different event loops), because each function expects specific commands from the device and clears incoming data buffer - Additionally functions with naming schema _send_xxx_ are normal functions not waiting for acknowledge - The acknowledge needs to handled manually by using _PacketBuffer_ object from device diff --git a/README.md b/README.md index df881a0..b653912 100644 --- a/README.md +++ b/README.md @@ -134,3 +134,8 @@ Python 3.11 or higher ## 0.0.19 - Fixed error in ByteBuilder when printing object + +## 0.0.20 +- Changed return value of LayerMidLevel.get_current_data() +- Added locking to example_mid_level_update to synchronize call of async functions from different event loops +- Added error handling in SerialPortConnection._read_intern() to prevent ClearComErrors diff --git a/examples/mid_level/example_mid_level_update.py b/examples/mid_level/example_mid_level_update.py index 737bb8c..5f31242 100644 --- a/examples/mid_level/example_mid_level_update.py +++ b/examples/mid_level/example_mid_level_update.py @@ -2,17 +2,25 @@ import sys import asyncio +import threading from science_mode_4 import DeviceP24 from science_mode_4 import MidLevelChannelConfiguration from science_mode_4 import ChannelPoint from science_mode_4 import SerialPortConnection +from science_mode_4 import ResultAndError from examples.utils.example_utils import ExampleUtils, KeyboardInputThread async def main() -> int: """Main function""" + # lock is used to avoid interference of async functions of mid level layer + # mid_level.update() and mid_level.get_current_data() may run at the same + # time if a key is pressed at the moment when mid_level.get_current_data() is running + # (because they run on different event loops) + lock = threading.Lock() + # some points p1: ChannelPoint = ChannelPoint(200, 20) p2: ChannelPoint = ChannelPoint(100, 0) @@ -42,7 +50,8 @@ def input_callback(input_value: str) -> bool: if cc is not None: # toggle active cc.is_active = not cc.is_active - asyncio.run(mid_level.update(channel_config)) + with lock: + asyncio.run(mid_level.update(channel_config)) else: print("Channel config is None") else: @@ -74,15 +83,19 @@ def input_callback(input_value: str) -> bool: # get mid level layer to call mid level commands mid_level = device.get_layer_mid_level() - # call init mid level, we want to stop on all stimulation errors - await mid_level.init(True) + # call init mid level, we do not want to stop on all stimulation errors to be able to + # see errors during get_current_data + await mid_level.init(False) # set stimulation pattern, P24 device will now stimulate according this pattern await mid_level.update(channel_config) + possible_errors = [ResultAndError.ELECTRODE_ERROR, ResultAndError.PULSE_TIMEOUT_ERROR, ResultAndError.PULSE_LOW_CURRENT_ERROR] while keyboard_input_thread.is_alive(): # we have to call get_current_data() every 1.5s to keep stimulation ongoing - update = await mid_level.get_current_data() # pylint:disable=unused-variable - # print(update) + with lock: + _, _, channel_error = await mid_level.get_current_data() + if any(v in possible_errors for v in channel_error): + print(f"Channel with error: {[(i, v.name) for i, v in enumerate(channel_error) if v != ResultAndError.NO_ERROR]}") await asyncio.sleep(1) diff --git a/pyproject.toml b/pyproject.toml index 77be0da..70bff76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "science_mode_4" -version = "0.0.19" +version = "0.0.20" authors = [ { name="Marc Hofmann", email="marc-hofmann@gmx.de" }, ] diff --git a/src/science_mode_4/mid_level/mid_level_current_data.py b/src/science_mode_4/mid_level/mid_level_current_data.py index c2e33c5..5a3da8c 100644 --- a/src/science_mode_4/mid_level/mid_level_current_data.py +++ b/src/science_mode_4/mid_level/mid_level_current_data.py @@ -1,5 +1,7 @@ """Provides packet classes for mid level GetCurrentData""" +from typing import NamedTuple + from science_mode_4.protocol.commands import Commands from science_mode_4.protocol.packet import Packet, PacketAck from science_mode_4.protocol.types import ResultAndError @@ -7,6 +9,13 @@ from science_mode_4.utils.byte_builder import ByteBuilder +class MidLevelGetCurrentDataResult(NamedTuple): + """Result for mid level get current data""" + result_error: ResultAndError + is_stimulation_active_per_channel: list[bool] + channel_error: list[ResultAndError] + + class PacketMidLevelGetCurrentData(Packet): """Packet for mid level GetCurrentData""" @@ -65,6 +74,6 @@ def is_stimulation_active_per_channel(self) -> list[bool]: @property - def channel_error(self) -> list[int]: + def channel_error(self) -> list[ResultAndError]: """Getter for ChannelError""" return self._channel_error diff --git a/src/science_mode_4/mid_level/mid_level_layer.py b/src/science_mode_4/mid_level/mid_level_layer.py index f8f20ab..3bb2200 100644 --- a/src/science_mode_4/mid_level/mid_level_layer.py +++ b/src/science_mode_4/mid_level/mid_level_layer.py @@ -2,7 +2,8 @@ from science_mode_4.layer import Layer from science_mode_4.utils.logger import logger -from .mid_level_current_data import PacketMidLevelGetCurrentData, PacketMidLevelGetCurrentDataAck +from .mid_level_current_data import MidLevelGetCurrentDataResult, PacketMidLevelGetCurrentData, \ + PacketMidLevelGetCurrentDataAck from .mid_level_stop import PacketMidLevelStop, PacketMidLevelStopAck from .mid_level_update import PacketMidLevelUpdate, PacketMidLevelUpdateAck from .mid_level_types import MidLevelChannelConfiguration @@ -41,12 +42,12 @@ async def update(self, channel_configuration: list[MidLevelChannelConfiguration] logger().info("Mid level update") - async def get_current_data(self) -> list[bool]: + async def get_current_data(self) -> MidLevelGetCurrentDataResult: """Send mid level get current data command and waits for response""" p = PacketMidLevelGetCurrentData() ack: PacketMidLevelGetCurrentDataAck = await self.send_packet_and_wait(p) self._check_result_error(ack.result_error, "MidLevelGetCurrentData") - if True in ack.channel_error: - raise ValueError(f"Error mid level get current data channel error {ack.channel_error}") logger().info("Mid level get current data, active channels: %s", ack.is_stimulation_active_per_channel) - return ack.is_stimulation_active_per_channel + return MidLevelGetCurrentDataResult(ack.result_error, + ack.is_stimulation_active_per_channel, + ack.channel_error) diff --git a/src/science_mode_4/utils/serial_port_connection.py b/src/science_mode_4/utils/serial_port_connection.py index e1cf76d..7724b22 100644 --- a/src/science_mode_4/utils/serial_port_connection.py +++ b/src/science_mode_4/utils/serial_port_connection.py @@ -5,6 +5,7 @@ import serial.tools.list_ports import serial.tools.list_ports_common +from science_mode_4.utils.logger import logger from .connection import Connection @@ -59,7 +60,11 @@ def clear_buffer(self): def _read_intern(self) -> bytes: result = bytes() - if self._ser.in_waiting > 0: - result = self._ser.read_all() + try: + # may raise a ClearCommError / The device does not recognize the command. + if self._ser.in_waiting > 0: + result = self._ser.read_all() + except serial.SerialException as e: + logger().warning(e) return result From e5f020fc204605c8fe014006787fdd7055892ee8 Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Thu, 8 Jan 2026 22:14:17 +0100 Subject: [PATCH 2/2] linter --- examples/mid_level/example_mid_level_update.py | 2 +- src/science_mode_4/utils/serial_port_connection.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/mid_level/example_mid_level_update.py b/examples/mid_level/example_mid_level_update.py index 5f31242..5a27b42 100644 --- a/examples/mid_level/example_mid_level_update.py +++ b/examples/mid_level/example_mid_level_update.py @@ -83,7 +83,7 @@ def input_callback(input_value: str) -> bool: # get mid level layer to call mid level commands mid_level = device.get_layer_mid_level() - # call init mid level, we do not want to stop on all stimulation errors to be able to + # call init mid level, we do not want to stop on all stimulation errors to be able to # see errors during get_current_data await mid_level.init(False) # set stimulation pattern, P24 device will now stimulate according this pattern diff --git a/src/science_mode_4/utils/serial_port_connection.py b/src/science_mode_4/utils/serial_port_connection.py index 7724b22..33f77f8 100644 --- a/src/science_mode_4/utils/serial_port_connection.py +++ b/src/science_mode_4/utils/serial_port_connection.py @@ -61,7 +61,7 @@ def clear_buffer(self): def _read_intern(self) -> bytes: result = bytes() try: - # may raise a ClearCommError / The device does not recognize the command. + # may raise a ClearCommError / The device does not recognize the command. if self._ser.in_waiting > 0: result = self._ser.read_all() except serial.SerialException as e: