pyForceDAQ 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyforcedaq-2.0.0/PKG-INFO +15 -0
- pyforcedaq-2.0.0/pyproject.toml +27 -0
- pyforcedaq-2.0.0/src/pyforcedaq/__init__.py +43 -0
- pyforcedaq-2.0.0/src/pyforcedaq/__main__.py +42 -0
- pyforcedaq-2.0.0/src/pyforcedaq/_lib/__init__.py +1 -0
- pyforcedaq-2.0.0/src/pyforcedaq/_lib/lsl.py +56 -0
- pyforcedaq-2.0.0/src/pyforcedaq/_lib/misc.py +126 -0
- pyforcedaq-2.0.0/src/pyforcedaq/_lib/polling_time_profile.py +52 -0
- pyforcedaq-2.0.0/src/pyforcedaq/_lib/process_priority_manager.py +148 -0
- pyforcedaq-2.0.0/src/pyforcedaq/_lib/timer.py +45 -0
- pyforcedaq-2.0.0/src/pyforcedaq/_lib/types.py +400 -0
- pyforcedaq-2.0.0/src/pyforcedaq/_lib/udp_connection.py +326 -0
- pyforcedaq-2.0.0/src/pyforcedaq/daq/__init__.py +19 -0
- pyforcedaq-2.0.0/src/pyforcedaq/daq/_daq_read_Analog_pydaqmx.py +114 -0
- pyforcedaq-2.0.0/src/pyforcedaq/daq/_daq_read_analog_nidaqmx.py +84 -0
- pyforcedaq-2.0.0/src/pyforcedaq/daq/_mock_sensor.py +80 -0
- pyforcedaq-2.0.0/src/pyforcedaq/daq/_pyATIDAQ.py +306 -0
- pyforcedaq-2.0.0/src/pyforcedaq/daq/config.py +13 -0
- pyforcedaq-2.0.0/src/pyforcedaq/extras/__init__.py +0 -0
- pyforcedaq-2.0.0/src/pyforcedaq/extras/convert.py +275 -0
- pyforcedaq-2.0.0/src/pyforcedaq/extras/expyriment_daq_control.py +246 -0
- pyforcedaq-2.0.0/src/pyforcedaq/extras/opensesame_daq_control.py +280 -0
- pyforcedaq-2.0.0/src/pyforcedaq/extras/read_force_data.py +89 -0
- pyforcedaq-2.0.0/src/pyforcedaq/extras/remote_control.py +93 -0
- pyforcedaq-2.0.0/src/pyforcedaq/force/__init__.py +13 -0
- pyforcedaq-2.0.0/src/pyforcedaq/force/_log.py +18 -0
- pyforcedaq-2.0.0/src/pyforcedaq/force/data_recorder.py +400 -0
- pyforcedaq-2.0.0/src/pyforcedaq/force/sensor.py +200 -0
- pyforcedaq-2.0.0/src/pyforcedaq/force/sensor_process.py +251 -0
- pyforcedaq-2.0.0/src/pyforcedaq/gui/__init__.py +6 -0
- pyforcedaq-2.0.0/src/pyforcedaq/gui/_gui_status.py +306 -0
- pyforcedaq-2.0.0/src/pyforcedaq/gui/_layout.py +104 -0
- pyforcedaq-2.0.0/src/pyforcedaq/gui/_level_indicator.py +59 -0
- pyforcedaq-2.0.0/src/pyforcedaq/gui/_pg_surface.py +100 -0
- pyforcedaq-2.0.0/src/pyforcedaq/gui/_plotter.py +234 -0
- pyforcedaq-2.0.0/src/pyforcedaq/gui/_run.py +522 -0
- pyforcedaq-2.0.0/src/pyforcedaq/gui/_scaling.py +71 -0
- pyforcedaq-2.0.0/src/pyforcedaq/gui/_settings.py +98 -0
- pyforcedaq-2.0.0/src/pyforcedaq/gui/forceDAQ_logo.png +0 -0
- pyforcedaq-2.0.0/src/pyforcedaq/gui/launcher.py +249 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pyForceDAQ
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Requires-Dist: atiiaftt>=0.1.1
|
|
5
|
+
Requires-Dist: expyriment>=1.0.1
|
|
6
|
+
Requires-Dist: freesimplegui>=5.2.0.post1
|
|
7
|
+
Requires-Dist: icecream>=2.2.0
|
|
8
|
+
Requires-Dist: nidaqmx>=1.5.0
|
|
9
|
+
Requires-Dist: numpy>=2.4.4
|
|
10
|
+
Requires-Dist: psutil>=7.2.2
|
|
11
|
+
Requires-Dist: pydaqmx>=1.4.7
|
|
12
|
+
Requires-Dist: pylsl>=1.18.2
|
|
13
|
+
Requires-Dist: pyxdf>=1.17.4
|
|
14
|
+
Requires-Dist: tomlkit>=0.15.0
|
|
15
|
+
Requires-Python: >=3.13.0, <3.14
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyForceDAQ"
|
|
3
|
+
version = "2.0.0"
|
|
4
|
+
requires-python = ">=3.13.0, <3.14"
|
|
5
|
+
dependencies = [
|
|
6
|
+
"atiiaftt>=0.1.1",
|
|
7
|
+
"expyriment>=1.0.1",
|
|
8
|
+
"freesimplegui>=5.2.0.post1",
|
|
9
|
+
"icecream>=2.2.0",
|
|
10
|
+
"nidaqmx>=1.5.0",
|
|
11
|
+
"numpy>=2.4.4",
|
|
12
|
+
"psutil>=7.2.2",
|
|
13
|
+
"pydaqmx>=1.4.7",
|
|
14
|
+
"pylsl>=1.18.2",
|
|
15
|
+
"pyxdf>=1.17.4",
|
|
16
|
+
"tomlkit>=0.15.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[tool.uv.extra-build-dependencies]
|
|
20
|
+
pygame = ["setuptools._distutils.msvccompiler"]
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.11.15,<0.12.0"]
|
|
24
|
+
build-backend = "uv_build"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
pyforcedaq = "pyforcedaq.__main__:cli"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""DAQ tool to record response force data
|
|
2
|
+
|
|
3
|
+
Oliver Lindemann, 2026
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
launch the GUI force from your Python program:
|
|
7
|
+
``
|
|
8
|
+
from pyforcedaq import gui
|
|
9
|
+
|
|
10
|
+
gui.run_with_options(remote_control=False,
|
|
11
|
+
ask_filename=True,
|
|
12
|
+
calibration_file="FT_sensor1.cal")
|
|
13
|
+
``
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
import relevant stuff to program your own force:
|
|
17
|
+
``
|
|
18
|
+
from pyforcedaq import force
|
|
19
|
+
``
|
|
20
|
+
|
|
21
|
+
import relevant stuff for remote control of the GUI force:
|
|
22
|
+
``
|
|
23
|
+
from pyforcedaq import remote_control
|
|
24
|
+
``
|
|
25
|
+
|
|
26
|
+
For function to support data handling see the folder pyForceDAQ/analysis
|
|
27
|
+
|
|
28
|
+
Oliver Lindemann
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
__version__ = "2.0.beta"
|
|
32
|
+
__author__ = "Oliver Lindemann"
|
|
33
|
+
|
|
34
|
+
import sys as _sys
|
|
35
|
+
|
|
36
|
+
USE_MOCK_SENSOR = False # <-- change for usage in lab to False
|
|
37
|
+
|
|
38
|
+
if _sys.version_info[0] != 3 or _sys.version_info[1]<12:
|
|
39
|
+
raise RuntimeError("pyForceDAQ {0} ".format(__version__) +
|
|
40
|
+
"is not compatible with Python {0}.{1}. ".format(
|
|
41
|
+
_sys.version_info[0],
|
|
42
|
+
_sys.version_info[1]) +
|
|
43
|
+
"Please use Python 3.12+.")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
import argparse
|
|
3
|
+
|
|
4
|
+
from . import __author__, __version__, gui
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def cli():
|
|
8
|
+
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
prog="forcedaq",
|
|
11
|
+
description="Command-line interface for pyforceDAQ",
|
|
12
|
+
epilog=f"Author: {__author__}",
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
parser.add_argument("SETTINGS_FILE", nargs="?", default="", help="settings file")
|
|
19
|
+
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"-l", "--launcher",
|
|
22
|
+
action="store_true",
|
|
23
|
+
default=False,
|
|
24
|
+
help="Run with launcher GUI to edit settings and start recording",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
if args.launcher:
|
|
30
|
+
if len(args.SETTINGS_FILE) > 0:
|
|
31
|
+
print("Can't use launcher and settings file")
|
|
32
|
+
exit()
|
|
33
|
+
|
|
34
|
+
from .gui import launcher
|
|
35
|
+
return launcher.run()
|
|
36
|
+
else:
|
|
37
|
+
gui.run(args.SETTINGS_FILE)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__": # required because of threading
|
|
42
|
+
cli() # gui.run(), gui.run_with_options()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__author__ = "Oliver Lindemann"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from pylsl import (
|
|
2
|
+
StreamInfo,
|
|
3
|
+
StreamOutlet,
|
|
4
|
+
cf_double64,
|
|
5
|
+
cf_float32,
|
|
6
|
+
cf_int8,
|
|
7
|
+
cf_int16,
|
|
8
|
+
cf_int32,
|
|
9
|
+
cf_int64,
|
|
10
|
+
cf_string,
|
|
11
|
+
cf_undefined,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def init(
|
|
16
|
+
name: str,
|
|
17
|
+
n_channels: int,
|
|
18
|
+
stream_id: str,
|
|
19
|
+
freq: int,
|
|
20
|
+
channel_format: int,
|
|
21
|
+
metadata: dict | None = None,
|
|
22
|
+
) -> StreamOutlet:
|
|
23
|
+
"""
|
|
24
|
+
Initialise a LSL stream
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
name: name of the stream
|
|
28
|
+
n_channels: number of channels per sample
|
|
29
|
+
channel_format: format/type of each channel (ex: string, int, ...)
|
|
30
|
+
same format for each channel
|
|
31
|
+
stream_id: unique identifier of the stream
|
|
32
|
+
content_type: content type of stream. By convention LSL uses the content
|
|
33
|
+
types defined in the XDF file format specification where
|
|
34
|
+
applicable
|
|
35
|
+
freq: sampling rate in Hz
|
|
36
|
+
|
|
37
|
+
Return:
|
|
38
|
+
outlet: StreamOulet to push samples with LSL
|
|
39
|
+
"""
|
|
40
|
+
# LSL
|
|
41
|
+
# StreamInfo takes name t
|
|
42
|
+
info = StreamInfo(name, "force",
|
|
43
|
+
channel_count=n_channels,
|
|
44
|
+
nominal_srate=freq,
|
|
45
|
+
channel_format=channel_format,
|
|
46
|
+
source_id=stream_id)
|
|
47
|
+
|
|
48
|
+
# Check if there is metadata to add to the lsl stream
|
|
49
|
+
if metadata:
|
|
50
|
+
# Get xml object of the stream created earlier
|
|
51
|
+
xml_info = info.desc()
|
|
52
|
+
# Add meta data to xml object
|
|
53
|
+
for key, data in metadata.items():
|
|
54
|
+
xml_info.append_child_value(key, str(data))
|
|
55
|
+
|
|
56
|
+
return StreamOutlet(info)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from os import listdir, path
|
|
2
|
+
|
|
3
|
+
from .timer import get_time_ms
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def N2g(N):
|
|
7
|
+
kg = N/9.81
|
|
8
|
+
return kg*1000
|
|
9
|
+
|
|
10
|
+
class MinMaxDetector(object):
|
|
11
|
+
|
|
12
|
+
def __init__(self, start_value, duration):
|
|
13
|
+
self._minmax = [start_value, start_value]
|
|
14
|
+
self._duration = duration
|
|
15
|
+
self._level_change_time = None
|
|
16
|
+
|
|
17
|
+
def process(self, value):
|
|
18
|
+
"""Returns minmax (tuple) for a number of samples after the first
|
|
19
|
+
level change has occurred, otherwise None"""
|
|
20
|
+
|
|
21
|
+
if self._level_change_time is not None:
|
|
22
|
+
if (get_time_ms() - self._level_change_time) >= self._duration:
|
|
23
|
+
return tuple(self._minmax)
|
|
24
|
+
|
|
25
|
+
if value > self._minmax[1]:
|
|
26
|
+
self._minmax[1] = value
|
|
27
|
+
elif value < self._minmax[0]:
|
|
28
|
+
self._minmax[0] = value
|
|
29
|
+
|
|
30
|
+
elif self._minmax[0] != value: # level change just occurred
|
|
31
|
+
self._level_change_time = get_time_ms()
|
|
32
|
+
return self.process(value)
|
|
33
|
+
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def is_sampling_for_minmax(self):
|
|
38
|
+
"""true true if currently sampling for minmax"""
|
|
39
|
+
return (self._level_change_time is not None) and \
|
|
40
|
+
(get_time_ms() - self._level_change_time) < self._duration
|
|
41
|
+
|
|
42
|
+
def find_calibration_file(calibration_folder, sensor_name,
|
|
43
|
+
calibration_suffix=".cal"):
|
|
44
|
+
|
|
45
|
+
needle = 'Serial="{0}"'.format(sensor_name)
|
|
46
|
+
for x in listdir(path.abspath(calibration_folder)):
|
|
47
|
+
filename = path.join(calibration_folder, x)
|
|
48
|
+
if path.isfile(filename) and filename.endswith(calibration_suffix):
|
|
49
|
+
with open(filename, "r") as fl:
|
|
50
|
+
for l in fl:
|
|
51
|
+
if l.find(needle)>0:
|
|
52
|
+
return filename
|
|
53
|
+
raise RuntimeError("Can't find calibration file for sensor '{0}' : {1}.".format(
|
|
54
|
+
sensor_name, filename))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
#Sensor History with moving average filtering and distance, velocity
|
|
58
|
+
class SensorHistory(object):
|
|
59
|
+
"""The Sensory History keeps track of the last n recorded sample and
|
|
60
|
+
calculates online the moving average (running mean).
|
|
61
|
+
|
|
62
|
+
SensorHistory.moving_average
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, history_size, number_of_parameter):
|
|
67
|
+
self.history = [[0] * number_of_parameter] * history_size
|
|
68
|
+
self.moving_average = [0] * number_of_parameter
|
|
69
|
+
self._correction_cnt = 0
|
|
70
|
+
self._previous_moving_average = self.moving_average
|
|
71
|
+
|
|
72
|
+
def __str__(self):
|
|
73
|
+
return str(self.history)
|
|
74
|
+
|
|
75
|
+
def update(self, values):
|
|
76
|
+
"""Update history and calculate moving average
|
|
77
|
+
|
|
78
|
+
(correct for accumulated rounding errors ever 10000 samples)
|
|
79
|
+
|
|
80
|
+
Parameter
|
|
81
|
+
---------
|
|
82
|
+
values : list of values for all sensor parameters
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
self._previous_moving_average = self.moving_average
|
|
87
|
+
pop = self.history.pop(0)
|
|
88
|
+
self.history.append(values)
|
|
89
|
+
# pop first element and calc moving average
|
|
90
|
+
if self._correction_cnt > 10000:
|
|
91
|
+
self._correction_cnt = 0
|
|
92
|
+
self.moving_average = self.calc_history_average()
|
|
93
|
+
else:
|
|
94
|
+
self._correction_cnt += 1
|
|
95
|
+
self.moving_average = list(map(
|
|
96
|
+
lambda x: x[0] + (float(x[1] - x[2]) / len(self.history)),
|
|
97
|
+
zip(self.moving_average, values, pop)))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def calc_history_average(self):
|
|
101
|
+
"""Calculate history averages for all sensor parameter.
|
|
102
|
+
|
|
103
|
+
The method is more time consuming than calling the property
|
|
104
|
+
`moving_average`. It is does however not suffer from accumulated
|
|
105
|
+
rounding-errors such as moving average.
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
s = [float(0)] * self.number_of_parameter
|
|
110
|
+
for t in self.history:
|
|
111
|
+
s = list(map(lambda x: x[0] + x[1], zip(s, t)))
|
|
112
|
+
return list(map(lambda x: x / len(self.history), s))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def history_size(self):
|
|
117
|
+
return len(self.history)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def number_of_parameter(self):
|
|
121
|
+
return len(self.history[0])
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def previous_moving_average(self):
|
|
125
|
+
return self._previous_moving_average
|
|
126
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from .timer import get_time_ms
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PollingTimeProfile(object):
|
|
7
|
+
|
|
8
|
+
def __init__(self, timing_range=10):
|
|
9
|
+
self._last = None
|
|
10
|
+
self._timing_range = 10
|
|
11
|
+
self._zero_cnt = 0
|
|
12
|
+
|
|
13
|
+
#self._zero_time_polling_frequency = {}
|
|
14
|
+
self.profile_frequency = np.array([0] * (timing_range + 1))
|
|
15
|
+
|
|
16
|
+
def stop(self):
|
|
17
|
+
self._last = None
|
|
18
|
+
|
|
19
|
+
def update(self, time_ms):
|
|
20
|
+
if self._last is not None:
|
|
21
|
+
d = time_ms - self._last
|
|
22
|
+
if d > self._timing_range:
|
|
23
|
+
d = self._timing_range
|
|
24
|
+
self.profile_frequency[d] += 1
|
|
25
|
+
|
|
26
|
+
#if d == 0:
|
|
27
|
+
# self._zero_cnt += 1
|
|
28
|
+
#elif self._zero_cnt > 0:
|
|
29
|
+
# try:
|
|
30
|
+
# self._zero_time_polling_frequency[self._zero_cnt] += 1
|
|
31
|
+
# except:
|
|
32
|
+
# self._zero_time_polling_frequency[self._zero_cnt] = 1
|
|
33
|
+
# self._zero_cnt = 0
|
|
34
|
+
|
|
35
|
+
self._last = time_ms
|
|
36
|
+
|
|
37
|
+
def tick(self):
|
|
38
|
+
self.update(get_time_ms())
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def profile_percent(self):
|
|
42
|
+
n = np.sum(self.profile_frequency)
|
|
43
|
+
return self.profile_frequency / n
|
|
44
|
+
|
|
45
|
+
def get_profile_str(self):
|
|
46
|
+
rtn = str(list(self.profile_frequency)
|
|
47
|
+
).replace("[", "").replace("]", "").replace(" ", "")
|
|
48
|
+
return "polling profile [{}]".format(rtn)
|
|
49
|
+
|
|
50
|
+
#@property
|
|
51
|
+
#def zero_time_polling_frequency(self):
|
|
52
|
+
# return np.array(list(self._zero_time_polling_frequency.items()))
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import gc
|
|
4
|
+
import sys
|
|
5
|
+
import psutil
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from .types import PollingPriority
|
|
9
|
+
|
|
10
|
+
_REALTIME_PRIORITY_CLASS = -18
|
|
11
|
+
_HIGH_PRIORITY_CLASS = -10
|
|
12
|
+
|
|
13
|
+
class ProcessPriorityManager(object):
|
|
14
|
+
|
|
15
|
+
platform = sys.platform
|
|
16
|
+
pybits = 32 + int(sys.maxsize > 2 ** 32) * 32
|
|
17
|
+
main_process_id = psutil.Process().pid
|
|
18
|
+
_normal_nice_value = psutil.Process().nice() # usied on Linux
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._subprocs = []
|
|
22
|
+
|
|
23
|
+
def add_subprocess(self, process):
|
|
24
|
+
"""add process or list of processes"""
|
|
25
|
+
|
|
26
|
+
if isinstance(process, (list, tuple)):
|
|
27
|
+
for x in process:
|
|
28
|
+
self.add_subprocess(x)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
pid = process.pid
|
|
33
|
+
self._subprocs.append(process)
|
|
34
|
+
except:
|
|
35
|
+
logging.warning("Can't add process: {}".format(process))
|
|
36
|
+
|
|
37
|
+
def get_subprocess_priorities(self):
|
|
38
|
+
"""returns list with priorities"""
|
|
39
|
+
|
|
40
|
+
rtn = []
|
|
41
|
+
for p in self._subprocs:
|
|
42
|
+
if p.pid is not None:
|
|
43
|
+
rtn.append(get_priority(p.pid))
|
|
44
|
+
else:
|
|
45
|
+
rtn.append(None)
|
|
46
|
+
|
|
47
|
+
return rtn
|
|
48
|
+
|
|
49
|
+
def set_subprocess_priorities(self, level, disable_gc=False):
|
|
50
|
+
|
|
51
|
+
rtn = []
|
|
52
|
+
for p in self._subprocs:
|
|
53
|
+
if p.pid is not None:
|
|
54
|
+
rtn.append(set_priority(level=level, process_id=p.pid,
|
|
55
|
+
disable_gc=disable_gc))
|
|
56
|
+
else:
|
|
57
|
+
rtn.append(False)
|
|
58
|
+
|
|
59
|
+
return rtn
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def get_main_priority():
|
|
64
|
+
"""
|
|
65
|
+
returning main process priority, if subprocess_id=None
|
|
66
|
+
Priority: 'normal', 'high', or 'realtime'
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
return get_priority(ProcessPriorityManager.main_process_id)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def set_main_priority(level, disable_gc=False):
|
|
73
|
+
"""
|
|
74
|
+
changing main process, if subprocess_id=None
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
return set_priority(level=level,
|
|
78
|
+
process_id=ProcessPriorityManager.main_process_id,
|
|
79
|
+
disable_gc=disable_gc)
|
|
80
|
+
|
|
81
|
+
def get_priority(process_id):
|
|
82
|
+
"""
|
|
83
|
+
returning main process priority, if subprocess_id=None
|
|
84
|
+
Priority: 'normal', 'high', or 'realtime'
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
process = psutil.Process(process_id)
|
|
89
|
+
except:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
proc_priority = process.nice()
|
|
93
|
+
if ProcessPriorityManager.platform == 'win32':
|
|
94
|
+
if proc_priority == psutil.HIGH_PRIORITY_CLASS:
|
|
95
|
+
return PollingPriority.HIGH
|
|
96
|
+
elif proc_priority == psutil.REALTIME_PRIORITY_CLASS:
|
|
97
|
+
return PollingPriority.REALTIME
|
|
98
|
+
|
|
99
|
+
else:
|
|
100
|
+
if proc_priority <= _REALTIME_PRIORITY_CLASS:
|
|
101
|
+
return PollingPriority.REALTIME
|
|
102
|
+
elif proc_priority <= _HIGH_PRIORITY_CLASS:
|
|
103
|
+
return PollingPriority.HIGH
|
|
104
|
+
|
|
105
|
+
return PollingPriority.NORMAL
|
|
106
|
+
|
|
107
|
+
def set_priority(level, process_id, disable_gc):
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
process = psutil.Process(process_id)
|
|
111
|
+
except:
|
|
112
|
+
return False
|
|
113
|
+
nice_val = ProcessPriorityManager._normal_nice_value
|
|
114
|
+
level = PollingPriority.get_priority(level)
|
|
115
|
+
|
|
116
|
+
if level == PollingPriority.NORMAL:
|
|
117
|
+
disable_gc = False
|
|
118
|
+
elif level == PollingPriority.HIGH:
|
|
119
|
+
nice_val = _HIGH_PRIORITY_CLASS
|
|
120
|
+
if ProcessPriorityManager.platform == 'win32':
|
|
121
|
+
nice_val = psutil.HIGH_PRIORITY_CLASS
|
|
122
|
+
elif level == PollingPriority.REALTIME:
|
|
123
|
+
nice_val = _REALTIME_PRIORITY_CLASS
|
|
124
|
+
if ProcessPriorityManager.platform == 'win32':
|
|
125
|
+
nice_val = psutil.REALTIME_PRIORITY_CLASS
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
process.nice(nice_val)
|
|
129
|
+
if disable_gc:
|
|
130
|
+
gc.disable()
|
|
131
|
+
else:
|
|
132
|
+
gc.enable()
|
|
133
|
+
except psutil.AccessDenied:
|
|
134
|
+
logging.warning('Could not set process {} priority '
|
|
135
|
+
'to {} ({})'.format(process.pid, nice_val, level))
|
|
136
|
+
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# def getProcessAffinities(): TODO?
|
|
141
|
+
#
|
|
142
|
+
# curproc_affinity = SubProcessPriorityManager.current_process.cpu_affinity()
|
|
143
|
+
# return curproc_affinity
|
|
144
|
+
|
|
145
|
+
# @staticmethod
|
|
146
|
+
# def setProcessAffinities(experimentProcessorList, ioHubProcessorList):
|
|
147
|
+
# SubProcessPriorityManager.current_process.cpu_affinity(experimentProcessorList)
|
|
148
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""A high-resolution monotonic timer
|
|
2
|
+
|
|
3
|
+
Python 3.3+ provides the time.perf_counter() function, which is a high-resolution monotonic timer.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__author__ = 'Florian Krause <florian@expyriment.org>, \
|
|
7
|
+
Oliver Lindemann <oliver@expyriment.org>'
|
|
8
|
+
__version__ = ''
|
|
9
|
+
__revision__ = ''
|
|
10
|
+
__date__ = ''
|
|
11
|
+
|
|
12
|
+
from time import perf_counter, sleep
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_time_ms() -> int:
|
|
16
|
+
"""Get high-resolution time stamp (int) """
|
|
17
|
+
return int(1000 * perf_counter())
|
|
18
|
+
|
|
19
|
+
class Timer(object):#
|
|
20
|
+
"""A simple clock class that can be used to measure elapsed time in milliseconds."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, sync_timer=None):
|
|
23
|
+
if sync_timer is None:
|
|
24
|
+
self._init_time = get_time_ms()
|
|
25
|
+
else:
|
|
26
|
+
self._init_time = sync_timer._init_time
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def time(self):
|
|
30
|
+
return get_time_ms() - self._init_time
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def wait(waiting_time):
|
|
34
|
+
"""Wait for a certain amount of milliseconds.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
start = get_time_ms()
|
|
38
|
+
looptime = 200
|
|
39
|
+
if waiting_time > looptime:
|
|
40
|
+
sleep((waiting_time - looptime) / 1000)
|
|
41
|
+
while get_time_ms() < start + waiting_time:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
app_clock = Timer()
|