Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions HINTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
23 changes: 18 additions & 5 deletions examples/mid_level/example_mid_level_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
]
Expand Down
11 changes: 10 additions & 1 deletion src/science_mode_4/mid_level/mid_level_current_data.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
"""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
from science_mode_4.utils.bit_vector import BitVector
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"""

Expand Down Expand Up @@ -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
11 changes: 6 additions & 5 deletions src/science_mode_4/mid_level/mid_level_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
9 changes: 7 additions & 2 deletions src/science_mode_4/utils/serial_port_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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