np-services 0.1.69__py3-none-any.whl → 0.1.70__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.
Files changed (207) hide show
  1. np_services/__init__.py +8 -8
  2. np_services/open_ephys.py +377 -378
  3. np_services/protocols.py +185 -185
  4. np_services/proxies.py +1489 -1489
  5. np_services/resources/mvr_connector.py +260 -260
  6. np_services/resources/zro.py +325 -325
  7. np_services/scripts/pretest.py +412 -389
  8. np_services/stim_computer_theme_changer.py +41 -41
  9. np_services/utils.py +167 -167
  10. {np_services-0.1.69.dist-info → np_services-0.1.70.dist-info}/METADATA +5 -5
  11. np_services-0.1.70.dist-info/RECORD +15 -0
  12. {np_services-0.1.69.dist-info → np_services-0.1.70.dist-info}/WHEEL +2 -1
  13. {np_services-0.1.69.dist-info → np_services-0.1.70.dist-info}/entry_points.txt +1 -1
  14. np_services-0.1.70.dist-info/top_level.txt +1 -0
  15. np_services/.mypy_cache/.gitignore +0 -2
  16. np_services/.mypy_cache/3.9/@plugins_snapshot.json +0 -1
  17. np_services/.mypy_cache/3.9/__future__.data.json +0 -1
  18. np_services/.mypy_cache/3.9/__future__.meta.json +0 -1
  19. np_services/.mypy_cache/3.9/_ast.data.json +0 -1
  20. np_services/.mypy_cache/3.9/_ast.meta.json +0 -1
  21. np_services/.mypy_cache/3.9/_codecs.data.json +0 -1
  22. np_services/.mypy_cache/3.9/_codecs.meta.json +0 -1
  23. np_services/.mypy_cache/3.9/_collections_abc.data.json +0 -1
  24. np_services/.mypy_cache/3.9/_collections_abc.meta.json +0 -1
  25. np_services/.mypy_cache/3.9/_ctypes.data.json +0 -1
  26. np_services/.mypy_cache/3.9/_ctypes.meta.json +0 -1
  27. np_services/.mypy_cache/3.9/_decimal.data.json +0 -1
  28. np_services/.mypy_cache/3.9/_decimal.meta.json +0 -1
  29. np_services/.mypy_cache/3.9/_random.data.json +0 -1
  30. np_services/.mypy_cache/3.9/_random.meta.json +0 -1
  31. np_services/.mypy_cache/3.9/_socket.data.json +0 -1
  32. np_services/.mypy_cache/3.9/_socket.meta.json +0 -1
  33. np_services/.mypy_cache/3.9/_thread.data.json +0 -1
  34. np_services/.mypy_cache/3.9/_thread.meta.json +0 -1
  35. np_services/.mypy_cache/3.9/_typeshed/__init__.data.json +0 -1
  36. np_services/.mypy_cache/3.9/_typeshed/__init__.meta.json +0 -1
  37. np_services/.mypy_cache/3.9/_warnings.data.json +0 -1
  38. np_services/.mypy_cache/3.9/_warnings.meta.json +0 -1
  39. np_services/.mypy_cache/3.9/_weakref.data.json +0 -1
  40. np_services/.mypy_cache/3.9/_weakref.meta.json +0 -1
  41. np_services/.mypy_cache/3.9/_weakrefset.data.json +0 -1
  42. np_services/.mypy_cache/3.9/_weakrefset.meta.json +0 -1
  43. np_services/.mypy_cache/3.9/_winapi.data.json +0 -1
  44. np_services/.mypy_cache/3.9/_winapi.meta.json +0 -1
  45. np_services/.mypy_cache/3.9/abc.data.json +0 -1
  46. np_services/.mypy_cache/3.9/abc.meta.json +0 -1
  47. np_services/.mypy_cache/3.9/array.data.json +0 -1
  48. np_services/.mypy_cache/3.9/array.meta.json +0 -1
  49. np_services/.mypy_cache/3.9/atexit.data.json +0 -1
  50. np_services/.mypy_cache/3.9/atexit.meta.json +0 -1
  51. np_services/.mypy_cache/3.9/builtins.data.json +0 -1
  52. np_services/.mypy_cache/3.9/builtins.meta.json +0 -1
  53. np_services/.mypy_cache/3.9/codecs.data.json +0 -1
  54. np_services/.mypy_cache/3.9/codecs.meta.json +0 -1
  55. np_services/.mypy_cache/3.9/collections/__init__.data.json +0 -1
  56. np_services/.mypy_cache/3.9/collections/__init__.meta.json +0 -1
  57. np_services/.mypy_cache/3.9/collections/abc.data.json +0 -1
  58. np_services/.mypy_cache/3.9/collections/abc.meta.json +0 -1
  59. np_services/.mypy_cache/3.9/contextlib.data.json +0 -1
  60. np_services/.mypy_cache/3.9/contextlib.meta.json +0 -1
  61. np_services/.mypy_cache/3.9/ctypes/__init__.data.json +0 -1
  62. np_services/.mypy_cache/3.9/ctypes/__init__.meta.json +0 -1
  63. np_services/.mypy_cache/3.9/datetime.data.json +0 -1
  64. np_services/.mypy_cache/3.9/datetime.meta.json +0 -1
  65. np_services/.mypy_cache/3.9/decimal.data.json +0 -1
  66. np_services/.mypy_cache/3.9/decimal.meta.json +0 -1
  67. np_services/.mypy_cache/3.9/email/__init__.data.json +0 -1
  68. np_services/.mypy_cache/3.9/email/__init__.meta.json +0 -1
  69. np_services/.mypy_cache/3.9/email/charset.data.json +0 -1
  70. np_services/.mypy_cache/3.9/email/charset.meta.json +0 -1
  71. np_services/.mypy_cache/3.9/email/contentmanager.data.json +0 -1
  72. np_services/.mypy_cache/3.9/email/contentmanager.meta.json +0 -1
  73. np_services/.mypy_cache/3.9/email/errors.data.json +0 -1
  74. np_services/.mypy_cache/3.9/email/errors.meta.json +0 -1
  75. np_services/.mypy_cache/3.9/email/header.data.json +0 -1
  76. np_services/.mypy_cache/3.9/email/header.meta.json +0 -1
  77. np_services/.mypy_cache/3.9/email/message.data.json +0 -1
  78. np_services/.mypy_cache/3.9/email/message.meta.json +0 -1
  79. np_services/.mypy_cache/3.9/email/policy.data.json +0 -1
  80. np_services/.mypy_cache/3.9/email/policy.meta.json +0 -1
  81. np_services/.mypy_cache/3.9/enum.data.json +0 -1
  82. np_services/.mypy_cache/3.9/enum.meta.json +0 -1
  83. np_services/.mypy_cache/3.9/errno.data.json +0 -1
  84. np_services/.mypy_cache/3.9/errno.meta.json +0 -1
  85. np_services/.mypy_cache/3.9/fractions.data.json +0 -1
  86. np_services/.mypy_cache/3.9/fractions.meta.json +0 -1
  87. np_services/.mypy_cache/3.9/genericpath.data.json +0 -1
  88. np_services/.mypy_cache/3.9/genericpath.meta.json +0 -1
  89. np_services/.mypy_cache/3.9/importlib/__init__.data.json +0 -1
  90. np_services/.mypy_cache/3.9/importlib/__init__.meta.json +0 -1
  91. np_services/.mypy_cache/3.9/importlib/abc.data.json +0 -1
  92. np_services/.mypy_cache/3.9/importlib/abc.meta.json +0 -1
  93. np_services/.mypy_cache/3.9/importlib/machinery.data.json +0 -1
  94. np_services/.mypy_cache/3.9/importlib/machinery.meta.json +0 -1
  95. np_services/.mypy_cache/3.9/importlib/metadata/__init__.data.json +0 -1
  96. np_services/.mypy_cache/3.9/importlib/metadata/__init__.meta.json +0 -1
  97. np_services/.mypy_cache/3.9/io.data.json +0 -1
  98. np_services/.mypy_cache/3.9/io.meta.json +0 -1
  99. np_services/.mypy_cache/3.9/json/__init__.data.json +0 -1
  100. np_services/.mypy_cache/3.9/json/__init__.meta.json +0 -1
  101. np_services/.mypy_cache/3.9/json/decoder.data.json +0 -1
  102. np_services/.mypy_cache/3.9/json/decoder.meta.json +0 -1
  103. np_services/.mypy_cache/3.9/json/encoder.data.json +0 -1
  104. np_services/.mypy_cache/3.9/json/encoder.meta.json +0 -1
  105. np_services/.mypy_cache/3.9/logging/__init__.data.json +0 -1
  106. np_services/.mypy_cache/3.9/logging/__init__.meta.json +0 -1
  107. np_services/.mypy_cache/3.9/math.data.json +0 -1
  108. np_services/.mypy_cache/3.9/math.meta.json +0 -1
  109. np_services/.mypy_cache/3.9/mmap.data.json +0 -1
  110. np_services/.mypy_cache/3.9/mmap.meta.json +0 -1
  111. np_services/.mypy_cache/3.9/np_services/__init__.data.json +0 -1
  112. np_services/.mypy_cache/3.9/np_services/__init__.meta.json +0 -1
  113. np_services/.mypy_cache/3.9/np_services/config.data.json +0 -1
  114. np_services/.mypy_cache/3.9/np_services/config.meta.json +0 -1
  115. np_services/.mypy_cache/3.9/np_services/protocols.data.json +0 -1
  116. np_services/.mypy_cache/3.9/np_services/protocols.meta.json +0 -1
  117. np_services/.mypy_cache/3.9/np_services/zro.data.json +0 -1
  118. np_services/.mypy_cache/3.9/np_services/zro.meta.json +0 -1
  119. np_services/.mypy_cache/3.9/ntpath.data.json +0 -1
  120. np_services/.mypy_cache/3.9/ntpath.meta.json +0 -1
  121. np_services/.mypy_cache/3.9/numbers.data.json +0 -1
  122. np_services/.mypy_cache/3.9/numbers.meta.json +0 -1
  123. np_services/.mypy_cache/3.9/os/__init__.data.json +0 -1
  124. np_services/.mypy_cache/3.9/os/__init__.meta.json +0 -1
  125. np_services/.mypy_cache/3.9/os/path.data.json +0 -1
  126. np_services/.mypy_cache/3.9/os/path.meta.json +0 -1
  127. np_services/.mypy_cache/3.9/pathlib.data.json +0 -1
  128. np_services/.mypy_cache/3.9/pathlib.meta.json +0 -1
  129. np_services/.mypy_cache/3.9/pickle.data.json +0 -1
  130. np_services/.mypy_cache/3.9/pickle.meta.json +0 -1
  131. np_services/.mypy_cache/3.9/platform.data.json +0 -1
  132. np_services/.mypy_cache/3.9/platform.meta.json +0 -1
  133. np_services/.mypy_cache/3.9/posixpath.data.json +0 -1
  134. np_services/.mypy_cache/3.9/posixpath.meta.json +0 -1
  135. np_services/.mypy_cache/3.9/random.data.json +0 -1
  136. np_services/.mypy_cache/3.9/random.meta.json +0 -1
  137. np_services/.mypy_cache/3.9/re.data.json +0 -1
  138. np_services/.mypy_cache/3.9/re.meta.json +0 -1
  139. np_services/.mypy_cache/3.9/shutil.data.json +0 -1
  140. np_services/.mypy_cache/3.9/shutil.meta.json +0 -1
  141. np_services/.mypy_cache/3.9/socket.data.json +0 -1
  142. np_services/.mypy_cache/3.9/socket.meta.json +0 -1
  143. np_services/.mypy_cache/3.9/sre_compile.data.json +0 -1
  144. np_services/.mypy_cache/3.9/sre_compile.meta.json +0 -1
  145. np_services/.mypy_cache/3.9/sre_constants.data.json +0 -1
  146. np_services/.mypy_cache/3.9/sre_constants.meta.json +0 -1
  147. np_services/.mypy_cache/3.9/sre_parse.data.json +0 -1
  148. np_services/.mypy_cache/3.9/sre_parse.meta.json +0 -1
  149. np_services/.mypy_cache/3.9/string.data.json +0 -1
  150. np_services/.mypy_cache/3.9/string.meta.json +0 -1
  151. np_services/.mypy_cache/3.9/subprocess.data.json +0 -1
  152. np_services/.mypy_cache/3.9/subprocess.meta.json +0 -1
  153. np_services/.mypy_cache/3.9/sys.data.json +0 -1
  154. np_services/.mypy_cache/3.9/sys.meta.json +0 -1
  155. np_services/.mypy_cache/3.9/threading.data.json +0 -1
  156. np_services/.mypy_cache/3.9/threading.meta.json +0 -1
  157. np_services/.mypy_cache/3.9/time.data.json +0 -1
  158. np_services/.mypy_cache/3.9/time.meta.json +0 -1
  159. np_services/.mypy_cache/3.9/types.data.json +0 -1
  160. np_services/.mypy_cache/3.9/types.meta.json +0 -1
  161. np_services/.mypy_cache/3.9/typing.data.json +0 -1
  162. np_services/.mypy_cache/3.9/typing.meta.json +0 -1
  163. np_services/.mypy_cache/3.9/typing_extensions.data.json +0 -1
  164. np_services/.mypy_cache/3.9/typing_extensions.meta.json +0 -1
  165. np_services/.mypy_cache/3.9/warnings.data.json +0 -1
  166. np_services/.mypy_cache/3.9/warnings.meta.json +0 -1
  167. np_services/.mypy_cache/3.9/weakref.data.json +0 -1
  168. np_services/.mypy_cache/3.9/weakref.meta.json +0 -1
  169. np_services/.mypy_cache/3.9/zmq/__init__.data.json +0 -1
  170. np_services/.mypy_cache/3.9/zmq/__init__.meta.json +0 -1
  171. np_services/.mypy_cache/3.9/zmq/_typing.data.json +0 -1
  172. np_services/.mypy_cache/3.9/zmq/_typing.meta.json +0 -1
  173. np_services/.mypy_cache/3.9/zmq/backend/__init__.data.json +0 -1
  174. np_services/.mypy_cache/3.9/zmq/backend/__init__.meta.json +0 -1
  175. np_services/.mypy_cache/3.9/zmq/backend/select.data.json +0 -1
  176. np_services/.mypy_cache/3.9/zmq/backend/select.meta.json +0 -1
  177. np_services/.mypy_cache/3.9/zmq/constants.data.json +0 -1
  178. np_services/.mypy_cache/3.9/zmq/constants.meta.json +0 -1
  179. np_services/.mypy_cache/3.9/zmq/error.data.json +0 -1
  180. np_services/.mypy_cache/3.9/zmq/error.meta.json +0 -1
  181. np_services/.mypy_cache/3.9/zmq/sugar/__init__.data.json +0 -1
  182. np_services/.mypy_cache/3.9/zmq/sugar/__init__.meta.json +0 -1
  183. np_services/.mypy_cache/3.9/zmq/sugar/attrsettr.data.json +0 -1
  184. np_services/.mypy_cache/3.9/zmq/sugar/attrsettr.meta.json +0 -1
  185. np_services/.mypy_cache/3.9/zmq/sugar/context.data.json +0 -1
  186. np_services/.mypy_cache/3.9/zmq/sugar/context.meta.json +0 -1
  187. np_services/.mypy_cache/3.9/zmq/sugar/frame.data.json +0 -1
  188. np_services/.mypy_cache/3.9/zmq/sugar/frame.meta.json +0 -1
  189. np_services/.mypy_cache/3.9/zmq/sugar/poll.data.json +0 -1
  190. np_services/.mypy_cache/3.9/zmq/sugar/poll.meta.json +0 -1
  191. np_services/.mypy_cache/3.9/zmq/sugar/socket.data.json +0 -1
  192. np_services/.mypy_cache/3.9/zmq/sugar/socket.meta.json +0 -1
  193. np_services/.mypy_cache/3.9/zmq/sugar/tracker.data.json +0 -1
  194. np_services/.mypy_cache/3.9/zmq/sugar/tracker.meta.json +0 -1
  195. np_services/.mypy_cache/3.9/zmq/sugar/version.data.json +0 -1
  196. np_services/.mypy_cache/3.9/zmq/sugar/version.meta.json +0 -1
  197. np_services/.mypy_cache/3.9/zmq/utils/__init__.data.json +0 -1
  198. np_services/.mypy_cache/3.9/zmq/utils/__init__.meta.json +0 -1
  199. np_services/.mypy_cache/3.9/zmq/utils/interop.data.json +0 -1
  200. np_services/.mypy_cache/3.9/zmq/utils/interop.meta.json +0 -1
  201. np_services/.mypy_cache/3.9/zmq/utils/jsonapi.data.json +0 -1
  202. np_services/.mypy_cache/3.9/zmq/utils/jsonapi.meta.json +0 -1
  203. np_services/.mypy_cache/CACHEDIR.TAG +0 -3
  204. np_services/resources/black_desktop.ps1 +0 -66
  205. np_services/resources/grey_desktop.ps1 +0 -66
  206. np_services/resources/reset_desktop.ps1 +0 -66
  207. np_services-0.1.69.dist-info/RECORD +0 -206
np_services/open_ephys.py CHANGED
@@ -1,378 +1,377 @@
1
- import enum
2
- import json
3
- import os
4
- import pathlib
5
- import shutil
6
- import socket
7
- import sys
8
- import time
9
- from typing import Any, Optional, Sequence
10
-
11
- import np_config
12
- import np_logging
13
- import requests
14
- import npc_ephys
15
-
16
- import np_services.utils as utils
17
- from np_services.protocols import TestError
18
-
19
- # global vars -------------------------------------------------------------------------- #
20
- logger = np_logging.getLogger(__name__) # logs will show full module path
21
-
22
- __name = "OpenEphys" # Service protocol operations will see just the 'class' name
23
-
24
- exc: Optional[BaseException] = None
25
- initialized: float = 0
26
- "`time.time()` when the service was initialized."
27
-
28
- try:
29
- host: str = np_config.Rig().Acq
30
- except ValueError:
31
- logger.warning("Not connected to a rig: `OpenEphys.host` needs to be set manually.")
32
- port: str | int = 37497 # 1-800-EPHYS
33
- latest_start: float = 0
34
- "`time.time()` when the service was last started via `start()`."
35
-
36
- # for launching:
37
- rsc_app_id = "open-ephys"
38
-
39
- # device records:
40
- gb_per_hr: int | float = 250 # per drive
41
- min_rec_hr: int | float = 2
42
- pretest_duration_sec: int | float = 0.5
43
-
44
- # for resulting data:
45
- folder: str #! required
46
- "The string that will be sent to Open Ephys to name the recording: typically `0123456789_366122_20220618`"
47
- data_files: list[pathlib.Path] = []
48
- "Storage for paths collected over the experiment."
49
- data_root: Optional[pathlib.Path] = None
50
-
51
- # for validation
52
- sync_path: Optional[pathlib.Path] = None
53
-
54
- # -------------------------------------------------------------------------------------- #
55
-
56
-
57
- class State(enum.Enum):
58
- idle = "IDLE"
59
- acquire = "ACQUIRE"
60
- record = "RECORD"
61
-
62
-
63
- class Endpoint(enum.Enum):
64
- status = "status"
65
- recording = "recording"
66
- processors = "processors"
67
- message = "message"
68
-
69
-
70
- def launch() -> None:
71
- utils.start_rsc_app(host, rsc_app_id)
72
-
73
-
74
- def pretest() -> None:
75
- logger.info("OpenEphys | Starting pretest")
76
- global folder
77
- folder = "_pretest_"
78
- initialize()
79
- test()
80
- try:
81
- start()
82
- time.sleep(pretest_duration_sec)
83
- verify()
84
- finally:
85
- stop()
86
- finalize()
87
- validate()
88
- logger.info("OpenEphys | Pretest passed")
89
-
90
- def url(endpoint: Endpoint):
91
- return f"http://{host}:{port}/api/{endpoint.value}"
92
-
93
-
94
- def get_state() -> requests.Response:
95
- mode = requests.get(u := url(Endpoint.status)).json().get("mode")
96
- logger.debug("%s -> get mode: %s", u, mode)
97
- return mode
98
-
99
-
100
- def set_state(state: State) -> requests.Response:
101
- msg = {"mode": state.value}
102
- mode = requests.put(u := url(Endpoint.status), json.dumps(msg))
103
- logger.debug("%s <- set mode: %s", u, state.value)
104
- return mode
105
-
106
-
107
- def is_connected() -> bool:
108
- global exc
109
-
110
- if not utils.is_online(host):
111
- exc = TestError(
112
- f"OpenEphys | No response from {host}: may be offline or unreachable"
113
- )
114
- return False
115
-
116
- try:
117
- state = get_state()
118
- except requests.RequestException:
119
- exc = TestError(
120
- f"OpenEphys | No response from Open Ephys http server: is the software started?"
121
- )
122
- return False
123
- else:
124
- if not any(_.value == state for _ in State):
125
- exc = TestError(f"OpenEphys | Unexpected state: {state}")
126
- return False
127
-
128
- return True
129
-
130
-
131
- def initialize() -> None:
132
- logger.info("OpenEphys | Initializing")
133
- global data_files
134
- data_files = []
135
- sync_path = None
136
- global initialized
137
- initialized = time.time()
138
- global folder
139
- set_folder(folder)
140
-
141
-
142
- def get_required_disk_gb() -> int | float:
143
- """The minimum amount of free disk space required to start recording."""
144
- return gb_per_hr * min_rec_hr
145
-
146
-
147
- def is_disk_space_ok() -> bool:
148
- global exc
149
- exc = None
150
- required = get_required_disk_gb()
151
- for data_root in get_data_roots():
152
- try:
153
- free = utils.free_gb(data_root)
154
- except FileNotFoundError as e:
155
- exc = e
156
- logger.exception(f"{__name} data path not accessible: {data_root}")
157
- else:
158
- logger.info(
159
- "%s free disk space on %s: %s GB", __name, data_root.drive, free
160
- )
161
- if free < required:
162
- exc = ValueError(
163
- f"{__name} free disk space on {data_root.drive} doesn't meet minimum of {required} GB"
164
- )
165
- if exc:
166
- return False
167
- return True
168
-
169
-
170
- def test() -> None:
171
- logger.info("OpenEphys | Testing")
172
- if not is_connected():
173
- if exc:
174
- raise TestError(f"Acq computer {host} isn't responding, or OpenEphys isn't open") from exc
175
- gb = get_required_disk_gb()
176
- if not is_disk_space_ok():
177
- raise TestError(
178
- f"{__name} free disk space on one or more recording drives doesn't meet minimum of {gb} GB"
179
- ) from exc
180
- unlock_previous_recording()
181
-
182
-
183
- def is_started() -> bool:
184
- if get_state() == State.record.value:
185
- return True
186
- return False
187
-
188
-
189
- def is_ready_to_start() -> bool:
190
- if get_state() == State.acquire.value:
191
- return True
192
- return False
193
-
194
-
195
- def start() -> None:
196
- logger.info("OpenEphys | Starting recording")
197
- if is_started():
198
- logger.warning("OpenEphys is already started")
199
- return
200
- if not is_ready_to_start():
201
- set_state(State.acquire)
202
- time.sleep(0.5)
203
- global latest_start
204
- latest_start = time.time()
205
- set_state(State.record)
206
-
207
-
208
- def stop() -> None:
209
- logger.info("OpenEphys | Stopping recording")
210
- set_state(State.acquire)
211
-
212
-
213
- def finalize() -> None:
214
- logger.info("OpenEphys | Finalizing")
215
- data_files.extend(get_latest_data_dirs())
216
- unlock_previous_recording()
217
-
218
-
219
- def set_folder(
220
- name: str, prepend_text: str = "", append_text: str = ""
221
- ) -> None:
222
- """Recording folder string"""
223
- recording = requests.get(url(Endpoint.recording)).json()
224
-
225
- if name == "":
226
- name = "_"
227
- logger.warning(
228
- "OpenEphys | Recording directory cannot be empty, replaced with underscore: %s",
229
- name,
230
- )
231
- if "." in name:
232
- name.replace(".", "_")
233
- logger.warning(
234
- "OpenEphys | Recording directory cannot contain periods, replaced with underscores: %s",
235
- name,
236
- )
237
-
238
- recording["base_text"] = name
239
- recording["prepend_text"] = prepend_text
240
- recording["append_text"] = append_text
241
- logger.debug(
242
- "OpenEphys | Setting recording directory to: %s",
243
- prepend_text + name + append_text,
244
- )
245
- response = requests.put(url(Endpoint.recording), json.dumps(recording))
246
- time.sleep(0.1)
247
- if (actual := response.json().get("base_text")) != name:
248
- raise TestError(
249
- f"OpenEphys | Set folder to {name}, but software shows: {actual}"
250
- )
251
-
252
-
253
- def get_folder() -> str | None:
254
- return requests.get(url(Endpoint.recording)).json().get("base_text")
255
-
256
-
257
- def clear_open_ephys_name() -> None:
258
- set_folder("_temp_")
259
-
260
-
261
- def set_idle():
262
- "Should be called before sending any configuration to Open Ephys"
263
- if is_started():
264
- stop()
265
- time.sleep(0.5)
266
- set_state(State.idle)
267
-
268
-
269
- def unlock_previous_recording():
270
- "stop rec/acquiring | set name to _temp_ | record briefly | acquire | set name to folder"
271
- logger.debug("OpenEphys | Unlocking previous recording")
272
- set_idle()
273
- time.sleep(0.5)
274
- clear_open_ephys_name()
275
- time.sleep(0.5)
276
- start()
277
- time.sleep(0.5)
278
- stop()
279
- time.sleep(0.5)
280
- global folder
281
- set_folder(folder)
282
-
283
-
284
- def get_record_nodes() -> list[dict[str, Any]]:
285
- """Returns a list of record node info dicts, incl keys `node_id`, `parent_directory`"""
286
- return requests.get(url(Endpoint.recording)).json().get("record_nodes", None) or []
287
-
288
-
289
- def get_data_roots() -> list[pathlib.Path]:
290
- return [
291
- pathlib.Path(f"//{host}/{_['parent_directory'].replace(':','')}")
292
- for _ in get_record_nodes()
293
- ]
294
-
295
-
296
- def get_latest_data_dirs() -> list[pathlib.Path]:
297
- """Returns the path to the latest data folder, based on the latest modified time"""
298
- dirs = []
299
- for root in get_data_roots():
300
- if subfolders := [
301
- sub
302
- for sub in root.iterdir()
303
- if sub.is_dir()
304
- and not any(
305
- _ in str(sub) for _ in ["System Volume Information", "$RECYCLE.BIN"]
306
- )
307
- ]:
308
- subfolders.sort(key=lambda _: _.stat().st_ctime)
309
- dirs.append(subfolders[-1])
310
- return dirs
311
-
312
- def check_files_increasing_in_size() -> None:
313
- for data_dir in get_latest_data_dirs():
314
- for file in reversed(
315
- utils.get_files_created_between(
316
- data_dir, "*/*/*/continuous/*/sample_numbers.npy", latest_start
317
- )
318
- ):
319
- if utils.is_file_growing(file):
320
- break
321
- else:
322
- raise TestError(
323
- f"OpenEphys | Data file(s) not increasing in size in {data_dir}"
324
- )
325
-
326
- def verify() -> None:
327
- logger.debug("OpenEphys | Verifying")
328
- check_files_increasing_in_size()
329
- logger.info(
330
- "OpenEphys | Verified files are increasing in size for all Record Nodes"
331
- )
332
-
333
-
334
- def validate() -> None:
335
- logger.info(f"OpenEphys | Validating")
336
- npc_ephys.validate_ephys(
337
- root_paths=data_files,
338
- sync_path_or_dataset=sync_path,
339
- ignore_small_folders=True,
340
- )
341
- logger.info(f"OpenEphys | Validated data {'with' if sync_path else 'without'} sync")
342
-
343
- def set_ref(ext_tip="TIP"):
344
- # for port in [0, 1, 2]:
345
- # for slot in [0, 1, 2]:
346
-
347
- slot = 2 #! Test
348
- port = 1 #! Test
349
- dock = 1 # TODO may be 1 or 2 with firmware upgrade
350
- tip_ref_msg = {"text": f"NP REFERENCE {slot} {port} {dock} {ext_tip}"}
351
- # logger.info(f"sending ...
352
- # return
353
- requests.put(
354
- "http://localhost:37497/api/processors/100/config", json.dumps(tip_ref_msg)
355
- )
356
- time.sleep(3)
357
-
358
-
359
- # TODO set up everything possible from here to avoid accidentally changed settings ?
360
- # probe channels
361
- # sampling rate
362
- # tip ref
363
- # signal chain?
364
- # acq drive letters
365
-
366
- """
367
- if __name__ == "__main__":
368
- r = requests.get(Endpoint.recording)
369
- print((r.json()['current_directory_name'], r.json()['prepend_text'], r.json()['append_text']))
370
-
371
- r = EphysHTTP.set_folder(path = "mouseID_", prepend_text="sessionID", append_text="_date")
372
- print((r.json()['current_directory_name'], r.json()['prepend_text'], r.json()['append_text']))
373
-
374
- r = EphysHTTP.set_folder(path = "mouse", prepend_text="session", append_text="date")
375
- print((r.json()['current_directory_name'], r.json()['prepend_text'], r.json()['append_text']))
376
-
377
- print((r.json()['base_text'])) # fails as of 06/23 https://github.com/open-ephys/plugin-GUI/pull/514
378
- """
1
+ import enum
2
+ import json
3
+ import os
4
+ import pathlib
5
+ import shutil
6
+ import socket
7
+ import sys
8
+ import time
9
+ from typing import Any, Optional, Sequence
10
+
11
+ import np_config
12
+ import np_logging
13
+ import requests
14
+ import npc_ephys
15
+
16
+ import np_services.utils as utils
17
+ from np_services.protocols import TestError
18
+
19
+ # global vars -------------------------------------------------------------------------- #
20
+ logger = np_logging.getLogger(__name__) # logs will show full module path
21
+
22
+ __name = "OpenEphys" # Service protocol operations will see just the 'class' name
23
+
24
+ exc: Optional[BaseException] = None
25
+ initialized: float = 0
26
+ "`time.time()` when the service was initialized."
27
+
28
+ try:
29
+ host: str = np_config.Rig().Acq
30
+ except ValueError:
31
+ logger.warning("Not connected to a rig: `OpenEphys.host` needs to be set manually.")
32
+ port: str | int = 37497 # 1-800-EPHYS
33
+ latest_start: float = 0
34
+ "`time.time()` when the service was last started via `start()`."
35
+
36
+ # for launching:
37
+ rsc_app_id = "open-ephys"
38
+
39
+ # device records:
40
+ gb_per_hr: int | float = 250 # per drive
41
+ min_rec_hr: int | float = 2
42
+ pretest_duration_sec: int | float = 0.5
43
+
44
+ # for resulting data:
45
+ folder: str #! required
46
+ "The string that will be sent to Open Ephys to name the recording: typically `0123456789_366122_20220618`"
47
+ data_files: list[pathlib.Path] = []
48
+ "Storage for paths collected over the experiment."
49
+ data_root: Optional[pathlib.Path] = None
50
+
51
+ # for validation
52
+ sync_path: Optional[pathlib.Path] = None
53
+
54
+ # -------------------------------------------------------------------------------------- #
55
+
56
+
57
+ class State(enum.Enum):
58
+ idle = "IDLE"
59
+ acquire = "ACQUIRE"
60
+ record = "RECORD"
61
+
62
+
63
+ class Endpoint(enum.Enum):
64
+ status = "status"
65
+ recording = "recording"
66
+ processors = "processors"
67
+ message = "message"
68
+
69
+
70
+ def launch() -> None:
71
+ utils.start_rsc_app(host, rsc_app_id)
72
+
73
+
74
+ def pretest() -> None:
75
+ logger.info("OpenEphys | Starting pretest")
76
+ global folder
77
+ folder = "_pretest_"
78
+ initialize()
79
+ test()
80
+ try:
81
+ start()
82
+ time.sleep(pretest_duration_sec)
83
+ verify()
84
+ finally:
85
+ stop()
86
+ finalize()
87
+ validate()
88
+ logger.info("OpenEphys | Pretest passed")
89
+
90
+ def url(endpoint: Endpoint):
91
+ return f"http://{host}:{port}/api/{endpoint.value}"
92
+
93
+
94
+ def get_state() -> requests.Response:
95
+ mode = requests.get(u := url(Endpoint.status)).json().get("mode")
96
+ logger.debug("%s -> get mode: %s", u, mode)
97
+ return mode
98
+
99
+
100
+ def set_state(state: State) -> requests.Response:
101
+ msg = {"mode": state.value}
102
+ mode = requests.put(u := url(Endpoint.status), json.dumps(msg))
103
+ logger.debug("%s <- set mode: %s", u, state.value)
104
+ return mode
105
+
106
+
107
+ def is_connected() -> bool:
108
+ global exc
109
+
110
+ if not utils.is_online(host):
111
+ exc = TestError(
112
+ f"OpenEphys | No response from {host}: may be offline or unreachable"
113
+ )
114
+ return False
115
+
116
+ try:
117
+ state = get_state()
118
+ except requests.RequestException:
119
+ exc = TestError(
120
+ f"OpenEphys | No response from Open Ephys http server: is the software started?"
121
+ )
122
+ return False
123
+ else:
124
+ if not any(_.value == state for _ in State):
125
+ exc = TestError(f"OpenEphys | Unexpected state: {state}")
126
+ return False
127
+
128
+ return True
129
+
130
+
131
+ def initialize() -> None:
132
+ logger.info("OpenEphys | Initializing")
133
+ global data_files
134
+ data_files = []
135
+ global initialized
136
+ initialized = time.time()
137
+ global folder
138
+ set_folder(folder)
139
+
140
+
141
+ def get_required_disk_gb() -> int | float:
142
+ """The minimum amount of free disk space required to start recording."""
143
+ return gb_per_hr * min_rec_hr
144
+
145
+
146
+ def is_disk_space_ok() -> bool:
147
+ global exc
148
+ exc = None
149
+ required = get_required_disk_gb()
150
+ for data_root in get_data_roots():
151
+ try:
152
+ free = utils.free_gb(data_root)
153
+ except FileNotFoundError as e:
154
+ exc = e
155
+ logger.exception(f"{__name} data path not accessible: {data_root}")
156
+ else:
157
+ logger.info(
158
+ "%s free disk space on %s: %s GB", __name, data_root.drive, free
159
+ )
160
+ if free < required:
161
+ exc = ValueError(
162
+ f"{__name} free disk space on {data_root.drive} doesn't meet minimum of {required} GB"
163
+ )
164
+ if exc:
165
+ return False
166
+ return True
167
+
168
+
169
+ def test() -> None:
170
+ logger.info("OpenEphys | Testing")
171
+ if not is_connected():
172
+ if exc:
173
+ raise TestError(f"Acq computer {host} isn't responding, or OpenEphys isn't open") from exc
174
+ gb = get_required_disk_gb()
175
+ if not is_disk_space_ok():
176
+ raise TestError(
177
+ f"{__name} free disk space on one or more recording drives doesn't meet minimum of {gb} GB"
178
+ ) from exc
179
+ unlock_previous_recording()
180
+
181
+
182
+ def is_started() -> bool:
183
+ if get_state() == State.record.value:
184
+ return True
185
+ return False
186
+
187
+
188
+ def is_ready_to_start() -> bool:
189
+ if get_state() == State.acquire.value:
190
+ return True
191
+ return False
192
+
193
+
194
+ def start() -> None:
195
+ logger.info("OpenEphys | Starting recording")
196
+ if is_started():
197
+ logger.warning("OpenEphys is already started")
198
+ return
199
+ if not is_ready_to_start():
200
+ set_state(State.acquire)
201
+ time.sleep(0.5)
202
+ global latest_start
203
+ latest_start = time.time()
204
+ set_state(State.record)
205
+
206
+
207
+ def stop() -> None:
208
+ logger.info("OpenEphys | Stopping recording")
209
+ set_state(State.acquire)
210
+
211
+
212
+ def finalize() -> None:
213
+ logger.info("OpenEphys | Finalizing")
214
+ data_files.extend(get_latest_data_dirs())
215
+ unlock_previous_recording()
216
+
217
+
218
+ def set_folder(
219
+ name: str, prepend_text: str = "", append_text: str = ""
220
+ ) -> None:
221
+ """Recording folder string"""
222
+ recording = requests.get(url(Endpoint.recording)).json()
223
+
224
+ if name == "":
225
+ name = "_"
226
+ logger.warning(
227
+ "OpenEphys | Recording directory cannot be empty, replaced with underscore: %s",
228
+ name,
229
+ )
230
+ if "." in name:
231
+ name.replace(".", "_")
232
+ logger.warning(
233
+ "OpenEphys | Recording directory cannot contain periods, replaced with underscores: %s",
234
+ name,
235
+ )
236
+
237
+ recording["base_text"] = name
238
+ recording["prepend_text"] = prepend_text
239
+ recording["append_text"] = append_text
240
+ logger.debug(
241
+ "OpenEphys | Setting recording directory to: %s",
242
+ prepend_text + name + append_text,
243
+ )
244
+ response = requests.put(url(Endpoint.recording), json.dumps(recording))
245
+ time.sleep(0.1)
246
+ if (actual := response.json().get("base_text")) != name:
247
+ raise TestError(
248
+ f"OpenEphys | Set folder to {name}, but software shows: {actual}"
249
+ )
250
+
251
+
252
+ def get_folder() -> str | None:
253
+ return requests.get(url(Endpoint.recording)).json().get("base_text")
254
+
255
+
256
+ def clear_open_ephys_name() -> None:
257
+ set_folder("_temp_")
258
+
259
+
260
+ def set_idle():
261
+ "Should be called before sending any configuration to Open Ephys"
262
+ if is_started():
263
+ stop()
264
+ time.sleep(0.5)
265
+ set_state(State.idle)
266
+
267
+
268
+ def unlock_previous_recording():
269
+ "stop rec/acquiring | set name to _temp_ | record briefly | acquire | set name to folder"
270
+ logger.debug("OpenEphys | Unlocking previous recording")
271
+ set_idle()
272
+ time.sleep(0.5)
273
+ clear_open_ephys_name()
274
+ time.sleep(0.5)
275
+ start()
276
+ time.sleep(0.5)
277
+ stop()
278
+ time.sleep(0.5)
279
+ global folder
280
+ set_folder(folder)
281
+
282
+
283
+ def get_record_nodes() -> list[dict[str, Any]]:
284
+ """Returns a list of record node info dicts, incl keys `node_id`, `parent_directory`"""
285
+ return requests.get(url(Endpoint.recording)).json().get("record_nodes", None) or []
286
+
287
+
288
+ def get_data_roots() -> list[pathlib.Path]:
289
+ return [
290
+ pathlib.Path(f"//{host}/{_['parent_directory'].replace(':','')}")
291
+ for _ in get_record_nodes()
292
+ ]
293
+
294
+
295
+ def get_latest_data_dirs() -> list[pathlib.Path]:
296
+ """Returns the path to the latest data folder, based on the latest modified time"""
297
+ dirs = []
298
+ for root in get_data_roots():
299
+ if subfolders := [
300
+ sub
301
+ for sub in root.iterdir()
302
+ if sub.is_dir()
303
+ and not any(
304
+ _ in str(sub) for _ in ["System Volume Information", "$RECYCLE.BIN"]
305
+ )
306
+ ]:
307
+ subfolders.sort(key=lambda _: _.stat().st_ctime)
308
+ dirs.append(subfolders[-1])
309
+ return dirs
310
+
311
+ def check_files_increasing_in_size() -> None:
312
+ for data_dir in get_latest_data_dirs():
313
+ for file in reversed(
314
+ utils.get_files_created_between(
315
+ data_dir, "*/*/*/continuous/*/sample_numbers.npy", latest_start
316
+ )
317
+ ):
318
+ if utils.is_file_growing(file):
319
+ break
320
+ else:
321
+ raise TestError(
322
+ f"OpenEphys | Data file(s) not increasing in size in {data_dir}"
323
+ )
324
+
325
+ def verify() -> None:
326
+ logger.debug("OpenEphys | Verifying")
327
+ check_files_increasing_in_size()
328
+ logger.info(
329
+ "OpenEphys | Verified files are increasing in size for all Record Nodes"
330
+ )
331
+
332
+
333
+ def validate() -> None:
334
+ logger.info(f"OpenEphys | Validating")
335
+ npc_ephys.validate_ephys(
336
+ root_paths=data_files,
337
+ sync_path_or_dataset=sync_path,
338
+ ignore_small_folders=True,
339
+ )
340
+ logger.info(f"OpenEphys | Validated data {'with' if sync_path else 'without'} sync")
341
+
342
+ def set_ref(ext_tip="TIP"):
343
+ # for port in [0, 1, 2]:
344
+ # for slot in [0, 1, 2]:
345
+
346
+ slot = 2 #! Test
347
+ port = 1 #! Test
348
+ dock = 1 # TODO may be 1 or 2 with firmware upgrade
349
+ tip_ref_msg = {"text": f"NP REFERENCE {slot} {port} {dock} {ext_tip}"}
350
+ # logger.info(f"sending ...
351
+ # return
352
+ requests.put(
353
+ "http://localhost:37497/api/processors/100/config", json.dumps(tip_ref_msg)
354
+ )
355
+ time.sleep(3)
356
+
357
+
358
+ # TODO set up everything possible from here to avoid accidentally changed settings ?
359
+ # probe channels
360
+ # sampling rate
361
+ # tip ref
362
+ # signal chain?
363
+ # acq drive letters
364
+
365
+ """
366
+ if __name__ == "__main__":
367
+ r = requests.get(Endpoint.recording)
368
+ print((r.json()['current_directory_name'], r.json()['prepend_text'], r.json()['append_text']))
369
+
370
+ r = EphysHTTP.set_folder(path = "mouseID_", prepend_text="sessionID", append_text="_date")
371
+ print((r.json()['current_directory_name'], r.json()['prepend_text'], r.json()['append_text']))
372
+
373
+ r = EphysHTTP.set_folder(path = "mouse", prepend_text="session", append_text="date")
374
+ print((r.json()['current_directory_name'], r.json()['prepend_text'], r.json()['append_text']))
375
+
376
+ print((r.json()['base_text'])) # fails as of 06/23 https://github.com/open-ephys/plugin-GUI/pull/514
377
+ """