mesofield 0.3.2b0__py3-none-any.whl
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.
- docs/_static/custom.css +40 -0
- docs/_static/favicon.png +0 -0
- docs/_static/logo.png +0 -0
- docs/api/index.md +70 -0
- docs/conf.py +200 -0
- docs/developer_guide.md +303 -0
- docs/index.md +25 -0
- docs/tutorial.md +4 -0
- docs/user_guide.md +172 -0
- examples/teensy_pulse_generator.py +320 -0
- experiments/pipeline_demo/experiment.json +24 -0
- experiments/pipeline_demo/hardware.yaml +23 -0
- experiments/pipeline_demo/procedure.py +50 -0
- experiments/two_cam_demo/experiment.json +24 -0
- experiments/two_cam_demo/hardware.yaml +58 -0
- experiments/two_cam_demo/load_dataset.py +213 -0
- experiments/two_cam_demo/procedure.py +87 -0
- external/video-codecs/openh264-1.8.0-win64.dll +0 -0
- mesofield/__init__.py +45 -0
- mesofield/__main__.py +11 -0
- mesofield/_version.py +24 -0
- mesofield/base.py +750 -0
- mesofield/cli/__init__.py +57 -0
- mesofield/cli/_richhelp.py +100 -0
- mesofield/cli/acquire.py +254 -0
- mesofield/cli/datakit.py +165 -0
- mesofield/cli/process.py +376 -0
- mesofield/cli/rig.py +108 -0
- mesofield/cli/tools.py +347 -0
- mesofield/config.py +751 -0
- mesofield/data/__init__.py +23 -0
- mesofield/data/batch.py +633 -0
- mesofield/data/manager.py +388 -0
- mesofield/data/writer.py +289 -0
- mesofield/datakit/__init__.py +44 -0
- mesofield/datakit/__main__.py +35 -0
- mesofield/datakit/_utils/_logger.py +5 -0
- mesofield/datakit/_version.py +141 -0
- mesofield/datakit/config.py +50 -0
- mesofield/datakit/core.py +783 -0
- mesofield/datakit/datamodel.py +200 -0
- mesofield/datakit/discover.py +124 -0
- mesofield/datakit/explore.py +651 -0
- mesofield/datakit/notebooks/pupil_dlc.ipynb +2445 -0
- mesofield/datakit/profile.py +535 -0
- mesofield/datakit/shell.py +83 -0
- mesofield/datakit/sources/__init__.py +65 -0
- mesofield/datakit/sources/analysis/mesomap.py +194 -0
- mesofield/datakit/sources/analysis/mesoscope.py +77 -0
- mesofield/datakit/sources/analysis/pupil.py +246 -0
- mesofield/datakit/sources/behavior/__init__.py +0 -0
- mesofield/datakit/sources/behavior/dataqueue.py +281 -0
- mesofield/datakit/sources/behavior/psychopy.py +364 -0
- mesofield/datakit/sources/behavior/treadmill.py +323 -0
- mesofield/datakit/sources/behavior/wheel.py +277 -0
- mesofield/datakit/sources/camera/mesoscope.py +32 -0
- mesofield/datakit/sources/camera/metadata_json.py +130 -0
- mesofield/datakit/sources/camera/pupil.py +28 -0
- mesofield/datakit/sources/camera/suite2p.py +547 -0
- mesofield/datakit/sources/register.py +204 -0
- mesofield/datakit/sources/session/config.py +130 -0
- mesofield/datakit/sources/session/notes.py +63 -0
- mesofield/datakit/sources/session/timestamps.py +58 -0
- mesofield/datakit/timeline.py +306 -0
- mesofield/devices/__init__.py +42 -0
- mesofield/devices/base.py +498 -0
- mesofield/devices/base_camera.py +295 -0
- mesofield/devices/cameras.py +740 -0
- mesofield/devices/daq.py +151 -0
- mesofield/devices/encoder.py +384 -0
- mesofield/devices/mocks.py +275 -0
- mesofield/devices/psychopy_device.py +455 -0
- mesofield/devices/subprocesses/__init__.py +0 -0
- mesofield/devices/subprocesses/psychopy.py +133 -0
- mesofield/devices/treadmill.py +318 -0
- mesofield/engines.py +380 -0
- mesofield/gui/Mesofield_icon.png +0 -0
- mesofield/gui/__init__.py +76 -0
- mesofield/gui/config_wizard.py +724 -0
- mesofield/gui/controller.py +535 -0
- mesofield/gui/dynamic_controller.py +78 -0
- mesofield/gui/maingui.py +427 -0
- mesofield/gui/mdagui.py +285 -0
- mesofield/gui/qt_device_adapter.py +109 -0
- mesofield/gui/speedplotter.py +152 -0
- mesofield/gui/theme.py +445 -0
- mesofield/gui/tiff_viewer.py +1050 -0
- mesofield/gui/viewer.py +691 -0
- mesofield/hardware.py +549 -0
- mesofield/playback.py +1298 -0
- mesofield/processing/__init__.py +12 -0
- mesofield/processing/runner.py +237 -0
- mesofield/processors/__init__.py +13 -0
- mesofield/processors/base.py +287 -0
- mesofield/processors/frame_mean.py +19 -0
- mesofield/protocols.py +378 -0
- mesofield/scaffold/__init__.py +34 -0
- mesofield/scaffold/experiment.py +400 -0
- mesofield/scaffold/rigs.py +121 -0
- mesofield/signals.py +85 -0
- mesofield/utils/__init__.py +0 -0
- mesofield/utils/_logger.py +156 -0
- mesofield/utils/retrofit.py +309 -0
- mesofield/utils/utils.py +217 -0
- mesofield-0.3.2b0.dist-info/METADATA +178 -0
- mesofield-0.3.2b0.dist-info/RECORD +111 -0
- mesofield-0.3.2b0.dist-info/WHEEL +5 -0
- mesofield-0.3.2b0.dist-info/entry_points.txt +2 -0
- mesofield-0.3.2b0.dist-info/licenses/LICENSE +21 -0
- mesofield-0.3.2b0.dist-info/top_level.txt +6 -0
- scripts/bench_frame_processor.py +103 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Shared, data-agnostic timeline utilities backed by the dataqueue master clock.
|
|
2
|
+
|
|
3
|
+
Expects a dataqueue.csv DataSource loader. The dataqueue file contains this structure:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
['queue_elapsed', 'packet_ts', 'device_ts', 'device_id', 'payload']
|
|
7
|
+
queue_elapsed packet_ts ... device_id payload
|
|
8
|
+
0 976929.429661 2025-07-08 16:58:03.627918 ... encoder_COM3 0
|
|
9
|
+
1 976929.439462 2025-07-08 16:58:03.637494 ... encoder_COM3 0
|
|
10
|
+
2 976929.470421 2025-07-08 16:58:03.668444 ... encoder_COM3 0
|
|
11
|
+
3 976929.503343 2025-07-08 16:58:03.701397 ... encoder_COM3 0
|
|
12
|
+
4 976929.503767 2025-07-08 16:58:03.701888 ... encoder_COM3 0
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Callable, ClassVar, Iterable, Optional, Sequence, Union
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import pandas as pd
|
|
25
|
+
|
|
26
|
+
from ._utils._logger import get_logger
|
|
27
|
+
from mesofield.datakit.config import settings
|
|
28
|
+
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
DeviceSelector = Union[str, Sequence[str], Callable[[pd.Series], pd.Series]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class TimelineSlice:
|
|
36
|
+
"""Lightweight view over a subset of the dataqueue.
|
|
37
|
+
|
|
38
|
+
It simply holds the filtered rows plus the selector that produced them so
|
|
39
|
+
callers can reapply the same logic or log what slice they were analysing.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
rows: pd.DataFrame
|
|
43
|
+
selector: Optional[DeviceSelector] = None
|
|
44
|
+
|
|
45
|
+
def queue_elapsed(self) -> pd.Series:
|
|
46
|
+
queue_col = settings.timeline.queue_column
|
|
47
|
+
column = self.rows.get(queue_col)
|
|
48
|
+
if column is None:
|
|
49
|
+
return pd.Series(dtype=np.float64)
|
|
50
|
+
|
|
51
|
+
series = pd.to_numeric(column, errors="coerce")
|
|
52
|
+
return series.dropna().reset_index(drop=True).astype(np.float64)
|
|
53
|
+
|
|
54
|
+
def packet_absolute(self) -> pd.Series:
|
|
55
|
+
"""Return packet_ts as UTC ISO8601 strings."""
|
|
56
|
+
column = self.rows.get("packet_ts")
|
|
57
|
+
if column is None:
|
|
58
|
+
return pd.Series(dtype=str)
|
|
59
|
+
parsed = pd.to_datetime(column, errors="coerce", utc=True)
|
|
60
|
+
valid_mask = parsed.notna()
|
|
61
|
+
result = pd.Series(index=self.rows.index, dtype=str)
|
|
62
|
+
result.loc[valid_mask] = parsed.loc[valid_mask].dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
63
|
+
return result.reset_index(drop=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DataqueueIndex:
|
|
67
|
+
"""Cacheable facade over a session's dataqueue CSV.
|
|
68
|
+
|
|
69
|
+
The class deliberately avoids hard-coding knowledge of specific hardware.
|
|
70
|
+
Consumers supply selectors or parsing callbacks that describe which device
|
|
71
|
+
streams they are interested in. The index exposes common utilities for
|
|
72
|
+
retrieving filtered views and fitting time mappings onto the shared clock.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
_cache: ClassVar[dict[Path, Optional["DataqueueIndex"]]] = {}
|
|
76
|
+
|
|
77
|
+
def __init__(self, directory: Path, dataqueue_path: Path, dataqueue: pd.DataFrame):
|
|
78
|
+
self._directory = directory
|
|
79
|
+
self._dataqueue_path = dataqueue_path
|
|
80
|
+
self._frame = dataqueue
|
|
81
|
+
queue_col = settings.timeline.queue_column
|
|
82
|
+
self._queue_all = self._prepare_queue_series(dataqueue.get(queue_col))
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
# Construction helpers
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
@classmethod
|
|
88
|
+
def for_directory(cls, directory: Optional[Path]) -> Optional["DataqueueIndex"]:
|
|
89
|
+
if directory is None:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
resolved = Path(directory).resolve()
|
|
93
|
+
if resolved in cls._cache:
|
|
94
|
+
return cls._cache[resolved]
|
|
95
|
+
|
|
96
|
+
dq_path = cls._find_dataqueue(resolved)
|
|
97
|
+
if dq_path is None:
|
|
98
|
+
cls._cache[resolved] = None
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
frame = cls._load_dataqueue(dq_path)
|
|
102
|
+
if frame is None or frame.empty:
|
|
103
|
+
cls._cache[resolved] = None
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
index = cls(resolved, dq_path, frame)
|
|
107
|
+
cls._cache[resolved] = index
|
|
108
|
+
logger.debug(
|
|
109
|
+
"DataqueueIndex initialised",
|
|
110
|
+
extra={
|
|
111
|
+
"phase": "dataqueue_index_init",
|
|
112
|
+
"directory": str(resolved),
|
|
113
|
+
"dataqueue": str(dq_path),
|
|
114
|
+
"rows": int(len(frame)),
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
return index
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_path(cls, dataqueue_path: Path) -> "DataqueueIndex":
|
|
121
|
+
resolved = Path(dataqueue_path).resolve()
|
|
122
|
+
frame = cls._load_dataqueue(resolved)
|
|
123
|
+
if frame is None or frame.empty:
|
|
124
|
+
raise FileNotFoundError(f"DataqueueIndex: no readable dataqueue at {resolved}")
|
|
125
|
+
return cls(resolved.parent, resolved, frame)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _find_dataqueue(directory: Path) -> Optional[Path]:
|
|
129
|
+
candidates = sorted(directory.glob(settings.timeline.dataqueue_glob))
|
|
130
|
+
return candidates[0] if candidates else None
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def _load_dataqueue(path: Path) -> Optional[pd.DataFrame]:
|
|
134
|
+
try:
|
|
135
|
+
frame = pd.read_csv(path, low_memory=False)
|
|
136
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
137
|
+
logger.warning(
|
|
138
|
+
"DataqueueIndex: failed to read dataqueue",
|
|
139
|
+
extra={"phase": "dataqueue_index_load", "path": str(path), "error": repr(exc)},
|
|
140
|
+
)
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
queue_col = settings.timeline.queue_column
|
|
144
|
+
if queue_col not in frame.columns:
|
|
145
|
+
logger.warning(
|
|
146
|
+
"DataqueueIndex: dataqueue missing queue_elapsed column",
|
|
147
|
+
extra={"phase": "dataqueue_index_load", "path": str(path)},
|
|
148
|
+
)
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
frame = frame.copy()
|
|
152
|
+
frame[queue_col] = pd.to_numeric(frame[queue_col], errors="coerce")
|
|
153
|
+
frame.dropna(subset=[queue_col], inplace=True)
|
|
154
|
+
frame.reset_index(drop=True, inplace=True)
|
|
155
|
+
return frame
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _prepare_queue_series(series: Optional[pd.Series]) -> pd.Series:
|
|
159
|
+
if series is None:
|
|
160
|
+
return pd.Series(dtype=np.float64)
|
|
161
|
+
prepared = pd.to_numeric(series, errors="coerce").dropna().reset_index(drop=True)
|
|
162
|
+
return prepared.astype(np.float64)
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
# Public accessors
|
|
166
|
+
# ------------------------------------------------------------------
|
|
167
|
+
@property
|
|
168
|
+
def source_path(self) -> Path:
|
|
169
|
+
return self._dataqueue_path
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def directory(self) -> Path:
|
|
173
|
+
return self._directory
|
|
174
|
+
|
|
175
|
+
def dataframe(self) -> pd.DataFrame:
|
|
176
|
+
"""Return a shallow copy of the underlying dataqueue frame."""
|
|
177
|
+
|
|
178
|
+
return self._frame.copy()
|
|
179
|
+
|
|
180
|
+
def device_ids(self) -> Sequence[str]:
|
|
181
|
+
if "device_id" not in self._frame.columns:
|
|
182
|
+
return ()
|
|
183
|
+
ids = self._frame["device_id"].dropna().astype(str)
|
|
184
|
+
# Preserve order of first appearance
|
|
185
|
+
return tuple(dict.fromkeys(ids))
|
|
186
|
+
|
|
187
|
+
def queue_series(self, selector: Optional[DeviceSelector] = None) -> pd.Series:
|
|
188
|
+
"""Return queue_elapsed samples filtered by an optional device selector."""
|
|
189
|
+
|
|
190
|
+
if selector is None:
|
|
191
|
+
return self._queue_all.copy()
|
|
192
|
+
|
|
193
|
+
mask = self._resolve_selector(selector)
|
|
194
|
+
if mask is None:
|
|
195
|
+
return pd.Series(dtype=np.float64)
|
|
196
|
+
queue_col = settings.timeline.queue_column
|
|
197
|
+
series = pd.to_numeric(self._frame.loc[mask, queue_col], errors="coerce")
|
|
198
|
+
series = series.dropna().reset_index(drop=True)
|
|
199
|
+
return series.astype(np.float64)
|
|
200
|
+
|
|
201
|
+
def slice(self, selector: DeviceSelector) -> TimelineSlice:
|
|
202
|
+
"""Return a filtered subset of the dataqueue rows matching the selector."""
|
|
203
|
+
|
|
204
|
+
mask = self._resolve_selector(selector)
|
|
205
|
+
if mask is None:
|
|
206
|
+
rows = self._frame.iloc[0:0].copy()
|
|
207
|
+
else:
|
|
208
|
+
rows = self._frame.loc[mask].copy()
|
|
209
|
+
rows.reset_index(drop=True, inplace=True)
|
|
210
|
+
return TimelineSlice(rows=rows, selector=selector)
|
|
211
|
+
|
|
212
|
+
def absolute_for_device(self, device_pattern: str) -> Optional[tuple[np.ndarray, np.ndarray]]:
|
|
213
|
+
"""Return (elapsed_s, absolute_iso) arrays for devices matching pattern, or None if empty."""
|
|
214
|
+
device_slice = self.slice(
|
|
215
|
+
lambda ids: ids.str.contains(device_pattern, case=False, na=False, regex=False)
|
|
216
|
+
)
|
|
217
|
+
elapsed = device_slice.queue_elapsed().to_numpy(dtype=np.float64)
|
|
218
|
+
absolute = device_slice.packet_absolute().to_numpy(dtype=str)
|
|
219
|
+
if elapsed.size == 0 or absolute.size == 0:
|
|
220
|
+
return None
|
|
221
|
+
origin = elapsed[0] if elapsed.size else 0.0
|
|
222
|
+
return elapsed - origin, absolute
|
|
223
|
+
|
|
224
|
+
# ------------------------------------------------------------------
|
|
225
|
+
# General utilities
|
|
226
|
+
# ------------------------------------------------------------------
|
|
227
|
+
@staticmethod
|
|
228
|
+
def fit_affine(x: Iterable[float], y: Iterable[float]) -> tuple[float, float, float]:
|
|
229
|
+
"""Fit an affine transform y ≈ a * x + b and return (a, b, r²)."""
|
|
230
|
+
|
|
231
|
+
x_arr = np.asarray(list(x), dtype=np.float64)
|
|
232
|
+
y_arr = np.asarray(list(y), dtype=np.float64)
|
|
233
|
+
|
|
234
|
+
if x_arr.size < 2 or y_arr.size < 2:
|
|
235
|
+
raise ValueError("At least two points are required to fit an affine transform")
|
|
236
|
+
|
|
237
|
+
X = np.vstack([x_arr, np.ones_like(x_arr)]).T
|
|
238
|
+
slope, intercept = np.linalg.lstsq(X, y_arr, rcond=None)[0]
|
|
239
|
+
|
|
240
|
+
predicted = slope * x_arr + intercept
|
|
241
|
+
residual = y_arr - predicted
|
|
242
|
+
ss_res = float(np.sum(residual ** 2))
|
|
243
|
+
ss_tot = float(np.sum((y_arr - np.mean(y_arr)) ** 2)) if len(y_arr) > 1 else 0.0
|
|
244
|
+
r2 = 1.0 - ss_res / ss_tot if ss_tot > 0 else float("nan")
|
|
245
|
+
|
|
246
|
+
return float(slope), float(intercept), float(r2)
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def relative(series: Iterable[float]) -> np.ndarray:
|
|
250
|
+
"""Produce a series relative to its first finite value."""
|
|
251
|
+
|
|
252
|
+
values = np.asarray(list(series), dtype=np.float64)
|
|
253
|
+
finite = values[np.isfinite(values)]
|
|
254
|
+
if finite.size == 0:
|
|
255
|
+
return values
|
|
256
|
+
origin = float(finite[0])
|
|
257
|
+
return values - origin
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def fix_32bit_wraparound(values: Iterable[int]) -> np.ndarray:
|
|
261
|
+
"""Correct unsigned 32-bit wraparound for monotonically increasing counters.
|
|
262
|
+
|
|
263
|
+
A wrap is only registered when the backward step exceeds half the
|
|
264
|
+
counter range (2**31). Small backward steps caused by clock jitter,
|
|
265
|
+
duplicate or slightly out-of-order samples are left alone — otherwise
|
|
266
|
+
every µs of jitter would falsely add 2**32 (~4295 s) to the tail of
|
|
267
|
+
the series.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
arr = np.asarray(list(values), dtype=np.int64)
|
|
271
|
+
if arr.size == 0:
|
|
272
|
+
return arr
|
|
273
|
+
|
|
274
|
+
wrap_value = 2 ** 32
|
|
275
|
+
wrap_threshold = 2 ** 31
|
|
276
|
+
corrected = arr.copy()
|
|
277
|
+
offset = 0
|
|
278
|
+
for idx in range(1, len(arr)):
|
|
279
|
+
if arr[idx - 1] - arr[idx] > wrap_threshold:
|
|
280
|
+
offset += wrap_value
|
|
281
|
+
corrected[idx] = arr[idx] + offset
|
|
282
|
+
return corrected
|
|
283
|
+
|
|
284
|
+
# ------------------------------------------------------------------
|
|
285
|
+
# Internal helpers
|
|
286
|
+
# ------------------------------------------------------------------
|
|
287
|
+
def _resolve_selector(self, selector: DeviceSelector) -> Optional[pd.Series]:
|
|
288
|
+
if "device_id" not in self._frame.columns:
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
device_ids = self._frame["device_id"].astype(str)
|
|
292
|
+
|
|
293
|
+
if callable(selector):
|
|
294
|
+
raw_mask = selector(device_ids)
|
|
295
|
+
if isinstance(raw_mask, pd.Series):
|
|
296
|
+
mask = raw_mask.reindex(device_ids.index, fill_value=False)
|
|
297
|
+
else:
|
|
298
|
+
mask = pd.Series(raw_mask, index=device_ids.index)
|
|
299
|
+
elif isinstance(selector, str):
|
|
300
|
+
mask = device_ids == selector
|
|
301
|
+
else:
|
|
302
|
+
candidates = [str(item) for item in selector]
|
|
303
|
+
mask = device_ids.isin(candidates)
|
|
304
|
+
|
|
305
|
+
mask = mask.fillna(False).astype(bool)
|
|
306
|
+
return mask
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from .base import BaseDevice, BaseDataProducer, BaseSerialDevice
|
|
2
|
+
|
|
3
|
+
# Hardware-specific devices require rig-only dependencies (nidaqmx,
|
|
4
|
+
# pymmcore-plus, pyserial, etc.). Import them lazily so analysis-only
|
|
5
|
+
# environments can still reach the base classes.
|
|
6
|
+
try:
|
|
7
|
+
from .daq import Nidaq
|
|
8
|
+
except ImportError:
|
|
9
|
+
Nidaq = None # type: ignore[assignment,misc]
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from .cameras import MMCamera, OpenCVCamera
|
|
13
|
+
except ImportError:
|
|
14
|
+
MMCamera = None # type: ignore[assignment,misc]
|
|
15
|
+
OpenCVCamera = None # type: ignore[assignment,misc]
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from .encoder import SerialWorker
|
|
19
|
+
except ImportError:
|
|
20
|
+
SerialWorker = None # type: ignore[assignment,misc]
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from .treadmill import EncoderSerialInterface
|
|
24
|
+
except ImportError:
|
|
25
|
+
EncoderSerialInterface = None # type: ignore[assignment,misc]
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from .psychopy_device import PsychoPyDevice
|
|
29
|
+
except ImportError:
|
|
30
|
+
PsychoPyDevice = None # type: ignore[assignment,misc]
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"BaseDevice",
|
|
34
|
+
"BaseDataProducer",
|
|
35
|
+
"BaseSerialDevice",
|
|
36
|
+
"Nidaq",
|
|
37
|
+
"MMCamera",
|
|
38
|
+
"OpenCVCamera",
|
|
39
|
+
"SerialWorker",
|
|
40
|
+
"EncoderSerialInterface",
|
|
41
|
+
"PsychoPyDevice",
|
|
42
|
+
]
|