From 39e2423467ccb45e8cf4095d97b0de94e79ba7fd Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Tue, 29 Apr 2025 21:49:37 +0200 Subject: [PATCH 1/5] pr comment --- .vscode/launch.json | 2 +- examples/utils/fastplotlib_utils.py | 14 +++++--------- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index b484509..ac009b5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "debugpy", "request": "launch", // "program": "src/__main__.py", - "module": "examples.mid_level.example_mid_level", + "module": "examples.dyscom.example_dyscom_fastplotlib", "justMyCode": false, // "args": ["COM3"], "console": "integratedTerminal" diff --git a/examples/utils/fastplotlib_utils.py b/examples/utils/fastplotlib_utils.py index 539857f..2ff9435 100644 --- a/examples/utils/fastplotlib_utils.py +++ b/examples/utils/fastplotlib_utils.py @@ -48,7 +48,7 @@ def append_value(self, value: float): def update_plot(self): - """This function is call in context of main thread""" + """This function is called in context of main thread""" super().update_plot() try: new_x_data, new_y_data = self._data_queue.get_nowait() @@ -71,19 +71,15 @@ def __init__(self, channels: dict[int, tuple[str, str]], max_value_count: int): # calc layout for sub plots x_dimension, y_dimension = self._calc_layout_dimension(len(channels)) - # length of names must match number of sub plots - names = [x[0] for x in channels.values()] - names.extend([""] * ((x_dimension * y_dimension) - len(channels))) - # create figure - self._figure = fpl.Figure(size=(1024, 768), shape=(y_dimension, x_dimension), names=names, ) + self._figure = fpl.Figure(size=(1024, 768), shape=(y_dimension, x_dimension)) sub_plot_counter = 0 for key, value in channels.items(): x_pos, y_pos = self._calc_layout_pos(sub_plot_counter, len(channels)) sub_plot = self._figure[y_pos, x_pos] - # setting name here does not work - # sub_plot.name = value[0] + sub_plot.title = value[0] + self._data[key] = FastPlotLibValueChannel(sub_plot, max_value_count, value[1]) sub_plot_counter += 1 @@ -95,7 +91,7 @@ def __init__(self, channels: dict[int, tuple[str, str]], max_value_count: int): def _animation(self, figure: fpl.Figure): - """This function is call in context of main thread""" + """This function is called in context of main thread""" for x in self._data.values(): x.update_plot() diff --git a/pyproject.toml b/pyproject.toml index 7447226..ea3e2b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "science_mode_4" -version = "0.0.12" +version = "0.0.13" authors = [ { name="Marc Hofmann", email="marc-hofmann@gmx.de" }, ] From 25cdd7a6e934ee6b5abe44049948d8d916dff9eb Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Sun, 4 May 2025 21:30:19 +0200 Subject: [PATCH 2/5] wip --- .vscode/launch.json | 2 +- README.md | 3 ++- examples/utils/fastplotlib_utils.py | 8 +------- examples/utils/plot_base.py | 8 -------- examples/utils/pyplot_utils.py | 12 +++--------- 5 files changed, 7 insertions(+), 26 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/README.md b/README.md index c652ece..0c50985 100644 --- a/README.md +++ b/README.md @@ -102,4 +102,5 @@ Python 3.11 or higher ## 0.0.12 - Dyscom init - - Added channel settings register \ No newline at end of file + - Added channel settings register +- Some bugfixes \ No newline at end of file diff --git a/examples/utils/fastplotlib_utils.py b/examples/utils/fastplotlib_utils.py index 2ff9435..f507db9 100644 --- a/examples/utils/fastplotlib_utils.py +++ b/examples/utils/fastplotlib_utils.py @@ -74,16 +74,10 @@ def __init__(self, channels: dict[int, tuple[str, str]], max_value_count: int): # create figure self._figure = fpl.Figure(size=(1024, 768), shape=(y_dimension, x_dimension)) - sub_plot_counter = 0 - for key, value in channels.items(): - x_pos, y_pos = self._calc_layout_pos(sub_plot_counter, len(channels)) - sub_plot = self._figure[y_pos, x_pos] + for (key, value), sub_plot in zip(channels.items(), list(self._figure)): sub_plot.title = value[0] - self._data[key] = FastPlotLibValueChannel(sub_plot, max_value_count, value[1]) - sub_plot_counter += 1 - # set animation function that is called regularly to update plots self._figure.add_animations(self._animation) # show figure diff --git a/examples/utils/plot_base.py b/examples/utils/plot_base.py index abcdbb0..1fd8fb9 100644 --- a/examples/utils/plot_base.py +++ b/examples/utils/plot_base.py @@ -64,11 +64,3 @@ def _calc_layout_dimension(self, channel_count: int) -> tuple[int, int]: equal in both directions""" layouts = {1: [1, 1], 2: [2, 1], 3: [3, 1], 4: [2, 2], 5: [3, 2]} return layouts[channel_count] - - - def _calc_layout_pos(self, index: int, channel_count: int) -> tuple[int, int]: - """Calculates from a 1 dimensional index a 2 dimensional position""" - cols, _ = self._calc_layout_dimension(channel_count) - x = index % cols - y = index // cols - return [x, y] diff --git a/examples/utils/pyplot_utils.py b/examples/utils/pyplot_utils.py index 73901d0..415773b 100644 --- a/examples/utils/pyplot_utils.py +++ b/examples/utils/pyplot_utils.py @@ -74,15 +74,9 @@ def __init__(self, channels: dict[int, tuple[str, str]], max_value_count: int): 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) - sub_plot_counter = 0 - - for key, value in channels.items(): - x_pos, y_pos = self._calc_layout_pos(sub_plot_counter, len(channels)) - ax = self._axes[y_pos, x_pos] - sub_plot_counter += 1 - - ax.set(xlabel="Samples", ylabel=value[0], title=value[0]) - self._data[key] = PyPlotValueChannel(ax, max_value_count, value[1]) + 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) From 4d27143628801b45f40a7341d1fea39c1826ba21 Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Wed, 7 May 2025 17:46:23 +0200 Subject: [PATCH 3/5] bugfix for example keyboard class under linux --- HINTS.md | 12 ++++++++++++ README.md | 5 ++++- examples/utils/example_utils.py | 6 +++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/HINTS.md b/HINTS.md index fe1e0fe..50c6be9 100644 --- a/HINTS.md +++ b/HINTS.md @@ -75,6 +75,18 @@ 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 +- On Windows + - Install [usbipd-win](https://github.com/dorssel/usbipd-win) + - `usbipd list` + - `usbipd bind --busid ` +- On Linux + - Install _usbip_ + - `sudo apt install linux-tools-generic usbip` + - `sudo usbip attach -r -b ` + - In case of permission error + - `sudo chmod 666 /dev/ttyACMx` + # Deviation from Instruction for Use ## Dyscom commands diff --git a/README.md b/README.md index 0c50985..0b7164d 100644 --- a/README.md +++ b/README.md @@ -103,4 +103,7 @@ Python 3.11 or higher ## 0.0.12 - Dyscom init - Added channel settings register -- Some bugfixes \ No newline at end of file +- Some bugfixes + +## 0.0.13 +- Fixed error with example keyboard utils under Linux \ No newline at end of file diff --git a/examples/utils/example_utils.py b/examples/utils/example_utils.py index 9d2c771..e5d8223 100644 --- a/examples/utils/example_utils.py +++ b/examples/utils/example_utils.py @@ -2,6 +2,7 @@ import sys import threading +import os from typing import Callable from getch import getch @@ -21,7 +22,10 @@ def run(self): while True: # getch() returns a bytes object key_raw = getch() - key = bytes.decode(key_raw) + if os.name == "nt": + key = bytes.decode(key_raw) + else: + key = key_raw # handle ctrl+c if key == "\x03": raise KeyboardInterrupt From b728fe045b33168e3ef04d710082ab94bea56f1b Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Wed, 7 May 2025 22:13:23 +0200 Subject: [PATCH 4/5] enhanced example low level plot to show all channels --- .pylintrc | 2 +- .vscode/launch.json | 2 +- examples/low_level/example_low_level_plot.py | 84 ++++++++++++-------- examples/utils/plot_base.py | 10 ++- examples/utils/pyplot_utils.py | 2 +- 5 files changed, 63 insertions(+), 37 deletions(-) diff --git a/.pylintrc b/.pylintrc index 0bb322d..5d1be9c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,5 +1,5 @@ [MAIN] -max-line-length=140 +max-line-length=150 max-attributes=15 [DESIGN] diff --git a/.vscode/launch.json b/.vscode/launch.json index 057b277..164c071 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_pyplot", + "module": "examples.low_level.example_low_level_plot", "justMyCode": false, // "args": ["COM3"], "console": "integratedTerminal" diff --git a/examples/low_level/example_low_level_plot.py b/examples/low_level/example_low_level_plot.py index 5ea2da0..7621b56 100644 --- a/examples/low_level/example_low_level_plot.py +++ b/examples/low_level/example_low_level_plot.py @@ -3,21 +3,53 @@ import asyncio import sys -import matplotlib.pyplot as plt -import numpy as np - from science_mode_4 import DeviceP24 from science_mode_4 import ChannelPoint, Commands from science_mode_4 import SerialPortConnection from science_mode_4 import PacketLowLevelChannelConfigAck from science_mode_4 import Connector, Channel from science_mode_4 import LowLevelHighVoltageSource, LowLevelMode +from science_mode_4 import LayerLowLevel from examples.utils.example_utils import ExampleUtils +from examples.utils.pyplot_utils import PyPlotHelper + + +def send_channel_config(low_level_layer: LayerLowLevel): + """Sends channel update""" + # device can store up to 10 channel config commands + for connector in Connector: + for channel in Channel: + factor = channel + 1 + # send_channel_config does not wait for an acknowledge + low_level_layer.send_channel_config(True, channel, connector, + [ChannelPoint(500 * factor, 10 * factor), + ChannelPoint(500 * factor, 0), + ChannelPoint(500 * factor, -10 * factor), + ChannelPoint(500 * factor, 0)]) + + +def calc_plot_index(connector: Connector, channel: Channel) -> int: + """Calculates index for plot from connector and channel""" + return connector * len(Channel) + channel + + +def get_channel_color(channel: Channel) -> str: + """Retrieves color from channel""" + color = channel.name + if color == "WHITE": + color = "PINK" + return color async def main() -> int: """Main function""" + plots_info: dict[int, tuple[str, str]] = {} + 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) + # get comport from command line argument com_port = ExampleUtils.get_comport_from_commandline_argument() # create serial port connection @@ -37,44 +69,32 @@ async def main() -> int: # call init low level and enable measurement await low_level_layer.init(LowLevelMode.STIM_CURRENT, LowLevelHighVoltageSource.STANDARD) - # send one channel config so we get only one acknowledge - low_level_layer.send_channel_config(True, Channel.RED, Connector.GREEN, - [ChannelPoint(1000, 40), ChannelPoint(1000, 0), - ChannelPoint(1000, -20)]) + for _ in range(3): + # send 8 channel config so we get only 8 acknowledges + send_channel_config(low_level_layer) - # wait for stimulation to happen - await asyncio.sleep(0.1) + # wait for stimulation to happen + await asyncio.sleep(0.25) - measurement_sample_time = 0 - measurement_samples: list[float] = [] - # get new data from connection - ack = low_level_layer.packet_buffer.get_packet_from_buffer() - if ack: - if ack.command == Commands.LOW_LEVEL_CHANNEL_CONFIG_ACK: - ll_config_ack: PacketLowLevelChannelConfigAck = ack - measurement_sample_time = ll_config_ack.sampling_time_in_microseconds - measurement_samples.extend(ll_config_ack.measurement_samples) + # process all acknowledges and append values to plot data + while True: + ack = low_level_layer.packet_buffer.get_packet_from_buffer() + if ack: + if ack.command == Commands.LOW_LEVEL_CHANNEL_CONFIG_ACK: + ll_config_ack: PacketLowLevelChannelConfigAck = ack + plot_helper.append_values(calc_plot_index(ll_config_ack.connector, ll_config_ack.channel), + ll_config_ack.measurement_samples) + else: + break # call stop low level await low_level_layer.stop() + # update plot with measured values + plot_helper.update() # close serial port connection connection.close() - # show plot - _, ax = plt.subplots() - # use measurement_sample_time as time frame for x-axis - # this may be wrong because measurement_sample_time seems to long and - # does not match actual stimulation duration - ax.plot(np.linspace(0, measurement_sample_time, len(measurement_samples)), - measurement_samples) - - ax.set(xlabel="Sample Time (µs)", ylabel="Current (mA)", - title="Current measurement") - ax.grid() - - plt.show() - return 0 diff --git a/examples/utils/plot_base.py b/examples/utils/plot_base.py index 1fd8fb9..1c33ccd 100644 --- a/examples/utils/plot_base.py +++ b/examples/utils/plot_base.py @@ -50,11 +50,17 @@ def data(self) -> dict[int, PlotValueChannel]: return self._data - def append_value(self, channel: int, value: float) -> tuple[float, float]: + def append_value(self, channel: int, value: float): """Append value to channel buffer""" self._data[channel].append_value(value) + def append_values(self, channel: int, values: list[float]): + """Append values to channel buffer""" + for x in values: + self.append_value(channel, x) + + def update(self): """Update plot""" @@ -62,5 +68,5 @@ def update(self): 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""" - layouts = {1: [1, 1], 2: [2, 1], 3: [3, 1], 4: [2, 2], 5: [3, 2]} + layouts = {1: [1, 1], 2: [2, 1], 3: [3, 1], 4: [2, 2], 5: [3, 2], 6: [3, 2], 7: [4, 2], 8: [4, 2]} return layouts[channel_count] diff --git a/examples/utils/pyplot_utils.py b/examples/utils/pyplot_utils.py index 415773b..7c1de75 100644 --- a/examples/utils/pyplot_utils.py +++ b/examples/utils/pyplot_utils.py @@ -57,7 +57,7 @@ def update_plot(self): if len(new_x_data) > 1: self._axes.set_xlim(new_x_data[0], new_x_data[-1]) - offset = (new_maximum - new_minimum) * 0.1 + offset = (new_maximum - new_minimum) * 0.1 + 0.1 self._axes.set_ylim(bottom=new_minimum - offset, top=new_maximum + offset) except Empty: # No new data in the queue From 5db22633893748fb7513929ce66f2f79d1d35226 Mon Sep 17 00:00:00 2001 From: Marc Hofmann Date: Wed, 7 May 2025 22:24:53 +0200 Subject: [PATCH 5/5] documentation --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0b7164d..866c1cb 100644 --- a/README.md +++ b/README.md @@ -106,4 +106,5 @@ Python 3.11 or higher - Some bugfixes ## 0.0.13 -- Fixed error with example keyboard utils under Linux \ No newline at end of file +- Fixed error with example keyboard utils under Linux +- Enhance example low level plot to show all channels \ No newline at end of file