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: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[MAIN]
max-line-length=140
max-line-length=150
max-attributes=15

[DESIGN]
Expand Down
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"type": "debugpy",
"request": "launch",
// "program": "src/__main__.py",
"module": "examples.mid_level.example_mid_level",
"module": "examples.low_level.example_low_level_plot",
"justMyCode": false,
// "args": ["COM3"],
"console": "integratedTerminal"
Expand Down
12 changes: 12 additions & 0 deletions HINTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <BUSID>`
- On Linux
- Install _usbip_
- `sudo apt install linux-tools-generic usbip`
- `sudo usbip attach -r <host-ip> -b <BUSID>`
- In case of permission error
- `sudo chmod 666 /dev/ttyACMx`

# Deviation from Instruction for Use

## Dyscom commands
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,9 @@ Python 3.11 or higher

## 0.0.12
- Dyscom init
- Added channel settings register
- Added channel settings register
- Some bugfixes

## 0.0.13
- Fixed error with example keyboard utils under Linux
- Enhance example low level plot to show all channels
84 changes: 52 additions & 32 deletions examples/low_level/example_low_level_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down
6 changes: 5 additions & 1 deletion examples/utils/example_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import sys
import threading
import os
from typing import Callable
from getch import getch

Expand All @@ -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
Expand Down
22 changes: 6 additions & 16 deletions examples/utils/fastplotlib_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -71,22 +71,12 @@ 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, )

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]
self._data[key] = FastPlotLibValueChannel(sub_plot, max_value_count, value[1])
self._figure = fpl.Figure(size=(1024, 768), shape=(y_dimension, x_dimension))

sub_plot_counter += 1
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])

# set animation function that is called regularly to update plots
self._figure.add_animations(self._animation)
Expand All @@ -95,7 +85,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()

Expand Down
18 changes: 8 additions & 10 deletions examples/utils/plot_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,23 @@ 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"""


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]


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]
14 changes: 4 additions & 10 deletions examples/utils/pyplot_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
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.12"
version = "0.0.13"
authors = [
{ name="Marc Hofmann", email="marc-hofmann@gmx.de" },
]
Expand Down