From c6f73eb98ba601342159fe77ef0c569ffcf29d4a Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Sat, 31 May 2025 10:21:23 +0200 Subject: [PATCH 1/3] improved examples --- .vscode/launch.json | 2 +- examples/dyscom/example_dyscom_fastplotlib.py | 4 ++-- examples/dyscom/example_dyscom_pyplot.py | 9 ++++++--- examples/dyscom/example_dyscom_write_csv.py | 4 ++-- examples/low_level/example_low_level_plot.py | 14 +++++++++----- examples/utils/example_utils.py | 19 +++++++++++++++++++ examples/utils/plot_base.py | 5 +++++ examples/utils/pyplot_utils.py | 18 ++++++++++++++++-- 8 files changed, 60 insertions(+), 15 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 164c071..ac009b5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "debugpy", "request": "launch", // "program": "src/__main__.py", - "module": "examples.low_level.example_low_level_plot", + "module": "examples.dyscom.example_dyscom_fastplotlib", "justMyCode": false, // "args": ["COM3"], "console": "integratedTerminal" diff --git a/examples/dyscom/example_dyscom_fastplotlib.py b/examples/dyscom/example_dyscom_fastplotlib.py index 1cc2415..becab64 100644 --- a/examples/dyscom/example_dyscom_fastplotlib.py +++ b/examples/dyscom/example_dyscom_fastplotlib.py @@ -62,14 +62,14 @@ async def device_communication() -> int: await dyscom.start() # loop for some time - for x in range(1000): + for x in range(5000): # check if we closed window if not is_window_open: break # check operation mode from time to time, this function is not waiting for response # so we have to handle it by ourself later - if x % 100 == 0: + if x % 500 == 0: dyscom.send_get_operation_mode() live_data_counter = 0 diff --git a/examples/dyscom/example_dyscom_pyplot.py b/examples/dyscom/example_dyscom_pyplot.py index 9a03abb..6e3fb24 100644 --- a/examples/dyscom/example_dyscom_pyplot.py +++ b/examples/dyscom/example_dyscom_pyplot.py @@ -49,9 +49,10 @@ async def main() -> int: # start dyscom measurement await dyscom.start() - for x in range(1000): + # loop for some time + for x in range(5000): # check operation mode from time to time - if x % 100 == 0: + if x % 500 == 0: dyscom.send_get_operation_mode() live_data_counter = 0 @@ -70,7 +71,7 @@ async def main() -> int: break # reduce framerate further - if sld.number % 60 == 0: + if sld.number % 10 == 0: plot_helper.append_value(0, sld.samples[0].value) plot_helper.update() @@ -88,6 +89,8 @@ async def main() -> int: # close serial port connection connection.close() + print("Close plot window to quit") + plot_helper.loop() return 0 diff --git a/examples/dyscom/example_dyscom_write_csv.py b/examples/dyscom/example_dyscom_write_csv.py index 28b2517..8546646 100644 --- a/examples/dyscom/example_dyscom_write_csv.py +++ b/examples/dyscom/example_dyscom_write_csv.py @@ -64,10 +64,10 @@ async def device_communication() -> int: total_count = 0 # loop for some time - for x in range(1000): + for x in range(5000): # check operation mode from time to time, this function is not waiting for response # so we have to handle it by ourself later - if x % 100 == 0: + if x % 500 == 0: dyscom.send_get_operation_mode() live_data_counter = 0 diff --git a/examples/low_level/example_low_level_plot.py b/examples/low_level/example_low_level_plot.py index 7621b56..c2cbd2b 100644 --- a/examples/low_level/example_low_level_plot.py +++ b/examples/low_level/example_low_level_plot.py @@ -37,7 +37,7 @@ def get_channel_color(channel: Channel) -> str: """Retrieves color from channel""" color = channel.name if color == "WHITE": - color = "PINK" + color = "PURPLE" return color @@ -48,7 +48,7 @@ async def main() -> int: for connector in Connector: for channel in Channel: plots_info[calc_plot_index(connector, channel)] = f"Connector {connector.name}, channel {channel.name}", get_channel_color(channel) - plot_helper = PyPlotHelper(plots_info, 2500) + plot_helper = PyPlotHelper(plots_info, 500) # get comport from command line argument com_port = ExampleUtils.get_comport_from_commandline_argument() @@ -74,7 +74,7 @@ async def main() -> int: send_channel_config(low_level_layer) # wait for stimulation to happen - await asyncio.sleep(0.25) + await asyncio.sleep(1.0) # process all acknowledges and append values to plot data while True: @@ -82,19 +82,23 @@ async def main() -> int: if ack: if ack.command == Commands.LOW_LEVEL_CHANNEL_CONFIG_ACK: ll_config_ack: PacketLowLevelChannelConfigAck = ack + # update plot with measured values plot_helper.append_values(calc_plot_index(ll_config_ack.connector, ll_config_ack.channel), ll_config_ack.measurement_samples) + plot_helper.update() else: break + await asyncio.sleep(0.1) + # call stop low level await low_level_layer.stop() - # update plot with measured values - plot_helper.update() # close serial port connection connection.close() + print("Close plot window to quit") + plot_helper.loop() return 0 diff --git a/examples/utils/example_utils.py b/examples/utils/example_utils.py index e5d8223..30b015e 100644 --- a/examples/utils/example_utils.py +++ b/examples/utils/example_utils.py @@ -3,6 +3,7 @@ import sys import threading import os +import asyncio from typing import Callable from getch import getch @@ -56,3 +57,21 @@ def get_comport_from_commandline_argument() -> str: com_port = sys.argv[1] return com_port + + + @staticmethod + async def wait_for_any_key_pressed(cb: Callable[[], None] | None): + # keyboard func + def input_callback(input_value: str) -> bool: + """Callback call from keyboard input thread""" + # quit on any key + return True + + # create keyboard input thread for non blocking console input + keyboard_input_thread = KeyboardInputThread(input_callback) + + # now we can start stimulation + while keyboard_input_thread.is_alive(): + if cb is not None: + cb() + await asyncio.sleep(0.1) \ No newline at end of file diff --git a/examples/utils/plot_base.py b/examples/utils/plot_base.py index 1c33ccd..60fd281 100644 --- a/examples/utils/plot_base.py +++ b/examples/utils/plot_base.py @@ -65,6 +65,11 @@ def update(self): """Update plot""" + def loop(self): + """Run event loop until plot window closed""" + pass + + def _calc_layout_dimension(self, channel_count: int) -> tuple[int, int]: """Calculates layout for a specific number of channels, tries to grow equal in both directions""" diff --git a/examples/utils/pyplot_utils.py b/examples/utils/pyplot_utils.py index 7c1de75..770622f 100644 --- a/examples/utils/pyplot_utils.py +++ b/examples/utils/pyplot_utils.py @@ -71,17 +71,21 @@ class PyPlotHelper(PlotHelper): def __init__(self, channels: dict[int, tuple[str, str]], max_value_count: int): super().__init__() + self._window_closed = False x_dimension, y_dimension = self._calc_layout_dimension(len(channels)) self._figure, self._axes = plt.subplots(y_dimension, x_dimension, constrained_layout=True, squeeze=False) + self._figure.canvas.mpl_connect("close_event", self._on_close) for (key, value), sub_plot in zip(channels.items(), self._axes.flat): sub_plot.set(xlabel="Samples", ylabel=value[0], title=value[0]) self._data[key] = PyPlotValueChannel(sub_plot, max_value_count, value[1]) # interactive mode and show plot - self._animation_result = animation.FuncAnimation(self._figure, self._animation, interval=100) - # plt.ion() + self._animation_result = animation.FuncAnimation(self._figure, self._animation, + interval=100, save_count=max_value_count) + plt.ion() plt.show(block=False) + self.update() def update(self): @@ -89,7 +93,17 @@ def update(self): plt.pause(0.0001) + def loop(self): + """Run event loop until plot window closed""" + while not self._window_closed: + self.update() + + def _animation(self, frame: int, *fargs: tuple): # pylint:disable=unused-argument """This function is call in context of main thread""" for x in self._data.values(): x.update_plot() + + + def _on_close(self, event): + self._window_closed = True \ No newline at end of file From 76afdc2e120a063f250c14d2471806cc97bc7dcd Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Sun, 1 Jun 2025 09:46:04 +0200 Subject: [PATCH 2/3] more improved examples --- .vscode/launch.json | 2 +- HINTS.md | 19 ++++++++++++------- README.md | 5 ++++- examples/dyscom/example_dyscom_pyplot.py | 2 +- examples/utils/example_utils.py | 6 ++++-- examples/utils/fastplotlib_utils.py | 2 +- examples/utils/plot_base.py | 1 - examples/utils/pyplot_utils.py | 11 ++++++----- pyproject.toml | 2 +- .../utils/serial_port_connection.py | 4 ++-- 10 files changed, 32 insertions(+), 22 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ac009b5..057b277 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "debugpy", "request": "launch", // "program": "src/__main__.py", - "module": "examples.dyscom.example_dyscom_fastplotlib", + "module": "examples.dyscom.example_dyscom_pyplot", "justMyCode": false, // "args": ["COM3"], "console": "integratedTerminal" diff --git a/HINTS.md b/HINTS.md index 50c6be9..5f3175d 100644 --- a/HINTS.md +++ b/HINTS.md @@ -21,7 +21,7 @@ This page describes implementation details. - Each device has an instance of a _PacketBuffer_ - Should be used to read packets from connection - Handles extraction of packets from byte stream -- Most functions communicating with the device are async functions, because they wait for a matching acknowledge and return values from acknowledge +- 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 - The async functions connection buffer handling is always identical: - Clear buffer @@ -30,7 +30,6 @@ This page describes implementation details. - More data remains in connection 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 - - _PacketBuffer_ reads data from connection and separates packets from data stream ## Logging - Library creates a custom logger, see class _Logger_ @@ -40,10 +39,10 @@ This page describes implementation details. - For better performance, disable logger - `logger().disabled = True` -## General layer +## General layer (all devices) - Contains functions to get common information like device serial or firmware version -## Mid level layer +## Mid level layer (P24) - Contains functions for mid level stimulation - This mode is good to let the device stimulate a predefined pattern until _stop()_ is send - Usage @@ -52,7 +51,7 @@ This page describes implementation details. - Call _get_current_data()_ every 1.5s to keep stimulation ongoing - Call _stop()_ to end stimulation and leave mid level mode -## Low level layer +## Low level layer (P24) - Contains functions for low level stimulation - This mode is good to react to a external trigger to change stimulation pattern - Without _send_channel_config()_ the device will not stimulate @@ -63,7 +62,7 @@ This page describes implementation details. - It stops stimulation when stimulation pattern is over - Call _stop()_ to leave low level mode -## Dyscom layer +## Dyscom layer (I24) - Contains functions for dyscom level - This mode is used by I24 to measure EMG or BI - Usage @@ -75,7 +74,9 @@ This page describes implementation details. - Call _power_module()_ to power off measurement module - IMPORTANT: all storage related functions are untested -# Using USB under Linux with Hyper-V +# Platform hints + +## Using USB under Linux with Hyper-V - On Windows - Install [usbipd-win](https://github.com/dorssel/usbipd-win) - `usbipd list` @@ -87,6 +88,10 @@ This page describes implementation details. - In case of permission error - `sudo chmod 666 /dev/ttyACMx` +## Using MacOS under VirtualBox +- https://www.reddit.com/r/macOSVMs/comments/1gb8egp/macos_sonoma_virtualbox_bootloop_afterduring/?rdt=48615 + + # Deviation from Instruction for Use ## Dyscom commands diff --git a/README.md b/README.md index 866c1cb..c9e57dd 100644 --- a/README.md +++ b/README.md @@ -107,4 +107,7 @@ Python 3.11 or higher ## 0.0.13 - Fixed error with example keyboard utils under Linux -- Enhance example low level plot to show all channels \ No newline at end of file +- Enhanced example low level plot to show all channels + +## 0.0.14 +- Improved examples \ No newline at end of file diff --git a/examples/dyscom/example_dyscom_pyplot.py b/examples/dyscom/example_dyscom_pyplot.py index 6e3fb24..b333c39 100644 --- a/examples/dyscom/example_dyscom_pyplot.py +++ b/examples/dyscom/example_dyscom_pyplot.py @@ -71,7 +71,7 @@ async def main() -> int: break # reduce framerate further - if sld.number % 10 == 0: + if sld.number % 60 == 0: plot_helper.append_value(0, sld.samples[0].value) plot_helper.update() diff --git a/examples/utils/example_utils.py b/examples/utils/example_utils.py index 30b015e..f67b32d 100644 --- a/examples/utils/example_utils.py +++ b/examples/utils/example_utils.py @@ -61,8 +61,10 @@ def get_comport_from_commandline_argument() -> str: @staticmethod async def wait_for_any_key_pressed(cb: Callable[[], None] | None): + """Helper function to wait for any key press""" + # keyboard func - def input_callback(input_value: str) -> bool: + def input_callback(_input_value: str) -> bool: """Callback call from keyboard input thread""" # quit on any key return True @@ -74,4 +76,4 @@ def input_callback(input_value: str) -> bool: while keyboard_input_thread.is_alive(): if cb is not None: cb() - await asyncio.sleep(0.1) \ No newline at end of file + await asyncio.sleep(0.1) diff --git a/examples/utils/fastplotlib_utils.py b/examples/utils/fastplotlib_utils.py index f507db9..885ee33 100644 --- a/examples/utils/fastplotlib_utils.py +++ b/examples/utils/fastplotlib_utils.py @@ -23,7 +23,7 @@ def __init__(self, sub_plot, max_value_count: int, color: str): self._line = sub_plot.add_line(y_data, name="values", colors=color) # this queue is used to synchronize data between background and main thread - self._data_queue = Queue(maxsize=1) + self._data_queue = Queue(maxsize=0) def append_value(self, value: float): diff --git a/examples/utils/plot_base.py b/examples/utils/plot_base.py index 60fd281..a02321f 100644 --- a/examples/utils/plot_base.py +++ b/examples/utils/plot_base.py @@ -67,7 +67,6 @@ def update(self): def loop(self): """Run event loop until plot window closed""" - pass def _calc_layout_dimension(self, channel_count: int) -> tuple[int, int]: diff --git a/examples/utils/pyplot_utils.py b/examples/utils/pyplot_utils.py index 770622f..6235f54 100644 --- a/examples/utils/pyplot_utils.py +++ b/examples/utils/pyplot_utils.py @@ -3,6 +3,7 @@ from queue import Empty, Full, Queue import matplotlib.pyplot as plt from matplotlib import animation +from matplotlib.backend_bases import CloseEvent from .plot_base import PlotHelper, PlotValueChannel @@ -27,7 +28,7 @@ def __init__(self, axes: plt.Axes, max_value_count: int, color: str): self._maximum = None # this queue is used to synchronize data between background and main thread - self._data_queue = Queue(maxsize=1) + self._data_queue = Queue(maxsize=0) def append_value(self, value: float): @@ -81,7 +82,7 @@ def __init__(self, channels: dict[int, tuple[str, str]], max_value_count: int): self._data[key] = PyPlotValueChannel(sub_plot, max_value_count, value[1]) # interactive mode and show plot - self._animation_result = animation.FuncAnimation(self._figure, self._animation, + self._animation_result = animation.FuncAnimation(self._figure, self._animation, interval=100, save_count=max_value_count) plt.ion() plt.show(block=False) @@ -99,11 +100,11 @@ def loop(self): self.update() - def _animation(self, frame: int, *fargs: tuple): # pylint:disable=unused-argument + def _animation(self, _frame: int, *_fargs: tuple): """This function is call in context of main thread""" for x in self._data.values(): x.update_plot() - def _on_close(self, event): - self._window_closed = True \ No newline at end of file + def _on_close(self, _event: CloseEvent): + self._window_closed = True diff --git a/pyproject.toml b/pyproject.toml index ea3e2b7..539eaa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "science_mode_4" -version = "0.0.13" +version = "0.0.14" authors = [ { name="Marc Hofmann", email="marc-hofmann@gmx.de" }, ] diff --git a/src/science_mode_4/utils/serial_port_connection.py b/src/science_mode_4/utils/serial_port_connection.py index 81643ea..e1cf76d 100644 --- a/src/science_mode_4/utils/serial_port_connection.py +++ b/src/science_mode_4/utils/serial_port_connection.py @@ -58,8 +58,8 @@ def clear_buffer(self): def _read_intern(self) -> bytes: - result = [] + result = bytes() if self._ser.in_waiting > 0: result = self._ser.read_all() - return bytes(result) + return result From 4fb3b67e90d22b562aeafeac45bede8cda85dcaf Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Sun, 1 Jun 2025 20:43:45 +0200 Subject: [PATCH 3/3] wip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c9e57dd..a0c32bb 100644 --- a/README.md +++ b/README.md @@ -110,4 +110,4 @@ Python 3.11 or higher - Enhanced example low level plot to show all channels ## 0.0.14 -- Improved examples \ No newline at end of file +- Improved examples under Linux/MacOS \ No newline at end of file