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 .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.low_level.example_low_level_plot",
"module": "examples.dyscom.example_dyscom_pyplot",
"justMyCode": false,
// "args": ["COM3"],
"console": "integratedTerminal"
Expand Down
19 changes: 12 additions & 7 deletions HINTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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_
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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`
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- Enhanced example low level plot to show all channels

## 0.0.14
- Improved examples under Linux/MacOS
4 changes: 2 additions & 2 deletions examples/dyscom/example_dyscom_fastplotlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions examples/dyscom/example_dyscom_pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down
4 changes: 2 additions & 2 deletions examples/dyscom/example_dyscom_write_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 9 additions & 5 deletions examples/low_level/example_low_level_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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()
Expand All @@ -74,27 +74,31 @@ 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:
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
# 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


Expand Down
21 changes: 21 additions & 0 deletions examples/utils/example_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sys
import threading
import os
import asyncio
from typing import Callable
from getch import getch

Expand Down Expand Up @@ -56,3 +57,23 @@ 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):
"""Helper function to wait for any key press"""

# 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)
2 changes: 1 addition & 1 deletion examples/utils/fastplotlib_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions examples/utils/plot_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ def update(self):
"""Update plot"""


def loop(self):
"""Run event loop until plot window closed"""


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"""
Expand Down
23 changes: 19 additions & 4 deletions examples/utils/pyplot_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -71,25 +72,39 @@ 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):
"""Wait for a short time to process events"""
plt.pause(0.0001)


def _animation(self, frame: int, *fargs: tuple): # pylint:disable=unused-argument
def loop(self):
"""Run event loop until plot window closed"""
while not self._window_closed:
self.update()


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: CloseEvent):
self._window_closed = True
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.13"
version = "0.0.14"
authors = [
{ name="Marc Hofmann", email="marc-hofmann@gmx.de" },
]
Expand Down
4 changes: 2 additions & 2 deletions src/science_mode_4/utils/serial_port_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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