From 906a6453611b06cdebbcb116eff0f53baf58caf6 Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Tue, 28 Oct 2025 22:01:00 +0100 Subject: [PATCH 1/4] fixed error in mid level update --- .vscode/launch.json | 4 +- README.md | 4 +- .../mid_level/example_mid_level_update.py | 94 +++++++++++++++++++ pyproject.toml | 2 +- .../mid_level/mid_level_layer.py | 4 +- .../mid_level/mid_level_types.py | 24 +++++ .../mid_level/mid_level_update.py | 4 +- 7 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 examples/mid_level/example_mid_level_update.py diff --git a/.vscode/launch.json b/.vscode/launch.json index f906b3e..c1f71fb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,8 +8,8 @@ "name": "Python Debugger: Current File", "type": "debugpy", "request": "launch", - "program": "src/__main__.py", - // "module": "examples.dyscom.example_dyscom_send_file", + // "program": "src/__main__.py", + "module": "examples.mid_level.example_mid_level_update", "justMyCode": false, // "args": ["COM3"], "console": "integratedTerminal" diff --git a/README.md b/README.md index ff9c164..e1011c9 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ Python 3.11 or higher - Demonstrates how to use mid level layer, where a stimulation pattern is send to the stimulator and the device automatically executes the pattern by itself for 15s - `python -m examples.mid_level.example_mid_level` - Demonstrates how to use mid level layer, where a stimulation pattern is send to the stimulator and the device automatically executes the pattern by itself until user ends stimulation by keyboard + - `python -m examples.mid_level.example_mid_level_update` + - Demonstrates how to use mid level layer, to toggle stimulation channels by keyboard - Low level layer - `python -m examples.low_level.example_low_level` - Demonstrates how to use low level layer, where host has to trigger stimulation manually, in this case by pressing a key @@ -128,4 +130,4 @@ Python 3.11 or higher - Added sample that demonstrates how to read measurement data files from I24 devices ## 0.0.18 -- More documentation \ No newline at end of file +- Fixed error for mid level update when not using all channels diff --git a/examples/mid_level/example_mid_level_update.py b/examples/mid_level/example_mid_level_update.py new file mode 100644 index 0000000..1c213e0 --- /dev/null +++ b/examples/mid_level/example_mid_level_update.py @@ -0,0 +1,94 @@ +"""Provides an example how to use mid level layer""" + +import sys +import asyncio + +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 examples.utils.example_utils import ExampleUtils, KeyboardInputThread + + +async def main() -> int: + """Main function""" + + # some points + p1: ChannelPoint = ChannelPoint(200, 20) + p2: ChannelPoint = ChannelPoint(100, 0) + p3: ChannelPoint = ChannelPoint(200, -20) + # channel configuration, we want to ignore first and last two channels + # we need to pad list with None to achieve correct indices + # [None, None, ChannelConfig, ChannelConfig, ChannelConfig, ChannelConfig] + channel_config = [MidLevelChannelConfiguration(False, 3, 20, [p1, p2, p3]) for x in range(4)] + channel_config.insert(0, None) + channel_config.insert(0, None) + + # keyboard is our trigger to end program + def input_callback(input_value: str) -> bool: + """Callback call from keyboard input thread""" + # print(f"Input value {input_value}") + + if input_value == "q": + # end keyboard input thread + return True + elif input_value >= '1' and input_value <= '8': + index = int(input_value) - 1 + if index >= 0 and index < len(channel_config): + cc = channel_config[index] + if cc is not None: + cc.is_active = not cc.is_active + asyncio.run(mid_level.update(channel_config)) + else: + print("Channel config is None") + else: + print("Invalid channel config index") + + return False + + + print("Invalid command") + return False + + print("Usage: press 1-8 to toggle channel, press q to quit") + # create keyboard input thread for non blocking console input + keyboard_input_thread = KeyboardInputThread(input_callback) + + # get comport from command line argument + com_port = ExampleUtils.get_comport_from_commandline_argument() + # create serial port connection + connection = SerialPortConnection(com_port) + # open connection, now we can read and write data + connection.open() + + # create science mode device + device = DeviceP24(connection) + # call initialize to get basic information (serial, versions) and stop any active stimulation/measurement + # to have a defined state + await device.initialize() + + # 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) + # set stimulation pattern, P24 device will now stimulate according this pattern + await mid_level.update(channel_config) + + 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) + + await asyncio.sleep(1) + + # call stop mid level + await mid_level.stop() + + # close serial port connection + connection.close() + return 0 + + +if __name__ == "__main__": + res = asyncio.run(main()) + sys.exit(res) diff --git a/pyproject.toml b/pyproject.toml index 3c58ea4..019fcd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "science_mode_4" -version = "0.0.17" +version = "0.0.18" authors = [ { name="Marc Hofmann", email="marc-hofmann@gmx.de" }, ] 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 347404f..f8f20ab 100644 --- a/src/science_mode_4/mid_level/mid_level_layer.py +++ b/src/science_mode_4/mid_level/mid_level_layer.py @@ -31,7 +31,9 @@ async def stop(self): async def update(self, channel_configuration: list[MidLevelChannelConfiguration]): - """Send mid level update command and waits for response""" + """Send mid level update command and waits for response. + channel_configuration describes configuration for each channel, channel number is the index in list, + if some channels are not used, use None for this index""" p = PacketMidLevelUpdate() p.channel_configuration = channel_configuration ack: PacketMidLevelUpdateAck = await self.send_packet_and_wait(p) diff --git a/src/science_mode_4/mid_level/mid_level_types.py b/src/science_mode_4/mid_level/mid_level_types.py index eb0822b..9dc9885 100644 --- a/src/science_mode_4/mid_level/mid_level_types.py +++ b/src/science_mode_4/mid_level/mid_level_types.py @@ -43,19 +43,43 @@ def is_active(self) -> bool: return self._is_active + @is_active.setter + def is_active(self, is_active: bool): + """Setter for is active""" + self._is_active = is_active + + @property def ramp(self) -> int: """Getter for ramp""" return self._ramp + @ramp.setter + def ramp(self, ramp: int): + """Setter for ramp""" + self._ramp = ramp + + @property def period_in_ms(self) -> int: """Getter for period""" return self._period_in_ms + @period_in_ms.setter + def period_in_ms(self, period_in_ms: int): + """Setter for period in ms""" + self._period_in_ms = period_in_ms + + @property def points(self) -> list[ChannelPoint]: """Getter for points""" return self._points + + + @points.setter + def points(self, points: list[ChannelPoint]): + """Setter for points""" + self._points = points diff --git a/src/science_mode_4/mid_level/mid_level_update.py b/src/science_mode_4/mid_level/mid_level_update.py index 6567147..dc921c7 100644 --- a/src/science_mode_4/mid_level/mid_level_update.py +++ b/src/science_mode_4/mid_level/mid_level_update.py @@ -26,6 +26,8 @@ def channel_configuration(self) -> list[MidLevelChannelConfiguration]: @channel_configuration.setter def channel_configuration(self, channel_configuration: list[MidLevelChannelConfiguration]): """Setter for channel configuration""" + if len(channel_configuration) > 8: + raise ValueError(f"Mid level support 8 channels at max, given length {len(channel_configuration)}") self._channel_configuration = channel_configuration @@ -37,7 +39,7 @@ def get_data(self) -> bytes: for x in range(8): c: MidLevelChannelConfiguration | None = self._channel_configuration[x] if x < len(self._channel_configuration) else None - if c: + if c is not None and c.is_active: bb.append_bytes(c.get_data()) return bb.get_bytes() From e0f670b749a13049ce642512bd3ea60c957b36e5 Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Wed, 29 Oct 2025 19:35:20 +0100 Subject: [PATCH 2/4] linter hints --- examples/mid_level/example_mid_level_update.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/mid_level/example_mid_level_update.py b/examples/mid_level/example_mid_level_update.py index 1c213e0..cf56e8a 100644 --- a/examples/mid_level/example_mid_level_update.py +++ b/examples/mid_level/example_mid_level_update.py @@ -32,9 +32,9 @@ def input_callback(input_value: str) -> bool: if input_value == "q": # end keyboard input thread return True - elif input_value >= '1' and input_value <= '8': + if "1" <= input_value <= "8": index = int(input_value) - 1 - if index >= 0 and index < len(channel_config): + if 0 <= index < len(channel_config): cc = channel_config[index] if cc is not None: cc.is_active = not cc.is_active @@ -45,7 +45,7 @@ def input_callback(input_value: str) -> bool: print("Invalid channel config index") return False - + print("Invalid command") return False From e7a881ec2e740c25575cc7a64d8d704cc31ff814 Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Wed, 29 Oct 2025 19:42:58 +0100 Subject: [PATCH 3/4] comments --- examples/mid_level/example_mid_level_update.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/mid_level/example_mid_level_update.py b/examples/mid_level/example_mid_level_update.py index cf56e8a..2b02392 100644 --- a/examples/mid_level/example_mid_level_update.py +++ b/examples/mid_level/example_mid_level_update.py @@ -17,7 +17,8 @@ async def main() -> int: p1: ChannelPoint = ChannelPoint(200, 20) p2: ChannelPoint = ChannelPoint(100, 0) p3: ChannelPoint = ChannelPoint(200, -20) - # channel configuration, we want to ignore first and last two channels + # channel configuration + # we want to ignore first and last two channels (just for demonstration purpose) # we need to pad list with None to achieve correct indices # [None, None, ChannelConfig, ChannelConfig, ChannelConfig, ChannelConfig] channel_config = [MidLevelChannelConfiguration(False, 3, 20, [p1, p2, p3]) for x in range(4)] @@ -34,9 +35,12 @@ def input_callback(input_value: str) -> bool: return True if "1" <= input_value <= "8": index = int(input_value) - 1 + # check if index is in range of channel_config if 0 <= index < len(channel_config): cc = channel_config[index] + # check if index contains a ChannelConfiguration object if cc is not None: + # toggle active cc.is_active = not cc.is_active asyncio.run(mid_level.update(channel_config)) else: From 494350ea117c63bba67220742dbac230834d619c Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Wed, 29 Oct 2025 19:47:07 +0100 Subject: [PATCH 4/4] comments again --- examples/mid_level/example_mid_level_update.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/mid_level/example_mid_level_update.py b/examples/mid_level/example_mid_level_update.py index 2b02392..737bb8c 100644 --- a/examples/mid_level/example_mid_level_update.py +++ b/examples/mid_level/example_mid_level_update.py @@ -18,7 +18,7 @@ async def main() -> int: p2: ChannelPoint = ChannelPoint(100, 0) p3: ChannelPoint = ChannelPoint(200, -20) # channel configuration - # we want to ignore first and last two channels (just for demonstration purpose) + # we want to ignore first and last two channels (just for demonstration purpose how to handle unused channels) # we need to pad list with None to achieve correct indices # [None, None, ChannelConfig, ChannelConfig, ChannelConfig, ChannelConfig] channel_config = [MidLevelChannelConfiguration(False, 3, 20, [p1, p2, p3]) for x in range(4)] @@ -55,6 +55,7 @@ def input_callback(input_value: str) -> bool: return False print("Usage: press 1-8 to toggle channel, press q to quit") + print("Only channels 3-6 have a channel configuration (see comments where configuration is created)") # create keyboard input thread for non blocking console input keyboard_input_thread = KeyboardInputThread(input_callback)