oasis-daq 0.0.0__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.
- oasis_daq/__init__.py +28 -0
- oasis_daq/__main__.py +3 -0
- oasis_daq/acquisition.py +410 -0
- oasis_daq/app.py +283 -0
- oasis_daq/context.py +47 -0
- oasis_daq/ema_export.py +180 -0
- oasis_daq/h5_export.py +406 -0
- oasis_daq/main.py +25 -0
- oasis_daq/processing.py +337 -0
- oasis_daq/project.py +353 -0
- oasis_daq/qt_utils.py +33 -0
- oasis_daq/resources/logo_with_title.svg +243 -0
- oasis_daq/theme.py +44 -0
- oasis_daq/widgets/__init__.py +5 -0
- oasis_daq/widgets/device_setup.py +498 -0
- oasis_daq/widgets/impact_testing.py +1024 -0
- oasis_daq/widgets/level_meter.py +96 -0
- oasis_daq/widgets/measurement_setup.py +253 -0
- oasis_daq-0.0.0.dist-info/METADATA +29 -0
- oasis_daq-0.0.0.dist-info/RECORD +23 -0
- oasis_daq-0.0.0.dist-info/WHEEL +5 -0
- oasis_daq-0.0.0.dist-info/entry_points.txt +2 -0
- oasis_daq-0.0.0.dist-info/top_level.txt +1 -0
oasis_daq/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Python DAQ for the Open Acquisition System for IEPE Sensors (OASIS).
|
|
2
|
+
|
|
3
|
+
Version and description come from the installed distribution metadata, which
|
|
4
|
+
setuptools builds from pyproject.toml, so what the About dialog shows is what
|
|
5
|
+
is actually installed. In a source checkout that was never installed there is
|
|
6
|
+
no metadata to read and the fallbacks below apply.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from importlib.metadata import PackageNotFoundError
|
|
10
|
+
from importlib.metadata import metadata as _metadata
|
|
11
|
+
from importlib.metadata import version as _version
|
|
12
|
+
|
|
13
|
+
__all__ = ["APP_NAME", "DESCRIPTION", "DISTRIBUTION", "__version__"]
|
|
14
|
+
|
|
15
|
+
DISTRIBUTION = "oasis-daq" # the project name in pyproject.toml
|
|
16
|
+
APP_NAME = "OASIS-DAQ" # the same name as it is shown to the user
|
|
17
|
+
|
|
18
|
+
_UNKNOWN_VERSION = "unknown"
|
|
19
|
+
_FALLBACK_DESCRIPTION = (
|
|
20
|
+
"Python DAQ for the Open Acquisition System for IEPE Sensors (OASIS)"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
__version__ = _version(DISTRIBUTION)
|
|
25
|
+
DESCRIPTION = _metadata(DISTRIBUTION)["Summary"] or _FALLBACK_DESCRIPTION
|
|
26
|
+
except PackageNotFoundError:
|
|
27
|
+
__version__ = _UNKNOWN_VERSION
|
|
28
|
+
DESCRIPTION = _FALLBACK_DESCRIPTION
|
oasis_daq/__main__.py
ADDED
oasis_daq/acquisition.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""Triggered multi-board acquisition loop for impact testing.
|
|
2
|
+
|
|
3
|
+
Multi-board handling follows oasis-gui: all boards share the CONVST line, the
|
|
4
|
+
primary board broadcasts the sampling parameters and every board stores the
|
|
5
|
+
same capture on its own SD card. Acquisition therefore has two steps here:
|
|
6
|
+
the primary board is armed and the impact is recorded (`_sample`), then the
|
|
7
|
+
capture is pulled from every board's USB MSC volume - all boards at the same
|
|
8
|
+
time, the primary included, since the volumes are independent - and the
|
|
9
|
+
boards' channels are stacked in port order (`stack_sources`).
|
|
10
|
+
|
|
11
|
+
Splitting the two steps means the sampling command is sent from here instead
|
|
12
|
+
of through ``OasisBoard.acquire()``, which always downloads the primary's own
|
|
13
|
+
capture before returning.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import tempfile
|
|
19
|
+
import threading
|
|
20
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
from oasis_api import OasisBoard
|
|
25
|
+
from PyQt6.QtCore import QThread, pyqtSignal
|
|
26
|
+
|
|
27
|
+
DEFAULT_SERIAL_BAUDRATE = 1_000_000
|
|
28
|
+
PULL_ATTEMPTS = 2 # a second pull after rebooting the boards that failed
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PullError(RuntimeError):
|
|
32
|
+
"""One or more boards did not hand over their capture; `ports` lists them."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, failures: dict[str, str]) -> None:
|
|
35
|
+
self.failures = dict(failures)
|
|
36
|
+
self.ports = list(failures)
|
|
37
|
+
super().__init__(
|
|
38
|
+
"Transfer failed: "
|
|
39
|
+
+ " | ".join(f"{port}: {error}" for port, error in self.failures.items())
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AcquisitionWorker(QThread):
|
|
44
|
+
"""Repeatedly arms the boards and emits one record per triggered impact."""
|
|
45
|
+
|
|
46
|
+
stateChanged = pyqtSignal(str, str) # state key, human readable detail
|
|
47
|
+
log = pyqtSignal(str)
|
|
48
|
+
progress = pyqtSignal(int)
|
|
49
|
+
recordReady = pyqtSignal(object) # dict: data [C, S], t [S], sample_rate
|
|
50
|
+
finished_ok = pyqtSignal()
|
|
51
|
+
failed = pyqtSignal(str)
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
ports: list[str],
|
|
56
|
+
voltage_ranges: list[list[float]], # one 8-list per board
|
|
57
|
+
sample_rate: int,
|
|
58
|
+
duration: float,
|
|
59
|
+
oversampling_exp: int,
|
|
60
|
+
trigger_level: float,
|
|
61
|
+
continuous: bool = True,
|
|
62
|
+
baudrate: int = DEFAULT_SERIAL_BAUDRATE,
|
|
63
|
+
parent=None,
|
|
64
|
+
) -> None:
|
|
65
|
+
super().__init__(parent)
|
|
66
|
+
self.ports = list(ports)
|
|
67
|
+
self.voltage_ranges = [list(ranges) for ranges in voltage_ranges]
|
|
68
|
+
self.sample_rate = int(sample_rate)
|
|
69
|
+
self.duration = float(duration)
|
|
70
|
+
self.oversampling_exp = int(oversampling_exp)
|
|
71
|
+
self.trigger_level = float(trigger_level)
|
|
72
|
+
self.continuous = bool(continuous)
|
|
73
|
+
self.baudrate = int(baudrate)
|
|
74
|
+
self._stop_requested = False
|
|
75
|
+
self._board: OasisBoard | None = None
|
|
76
|
+
self._board_lock = threading.Lock()
|
|
77
|
+
self._pull_percent: dict[str, int] = {} # per-board transfer progress
|
|
78
|
+
|
|
79
|
+
# --- control ------------------------------------------------------------
|
|
80
|
+
def request_stop(self) -> None:
|
|
81
|
+
self._stop_requested = True
|
|
82
|
+
with self._board_lock:
|
|
83
|
+
board = self._board
|
|
84
|
+
if board is not None:
|
|
85
|
+
try:
|
|
86
|
+
board.abort()
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
# --- worker -------------------------------------------------------------
|
|
91
|
+
def run(self) -> None:
|
|
92
|
+
try:
|
|
93
|
+
self._configure_secondary_boards()
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
self.failed.emit(f"Configuring additional boards failed: {exc}")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
while not self._stop_requested:
|
|
99
|
+
try:
|
|
100
|
+
payload = self._acquire_once()
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
if self._stop_requested:
|
|
103
|
+
break
|
|
104
|
+
self.failed.emit(str(exc))
|
|
105
|
+
return
|
|
106
|
+
if payload is None:
|
|
107
|
+
break
|
|
108
|
+
self.recordReady.emit(payload)
|
|
109
|
+
if not self.continuous:
|
|
110
|
+
break
|
|
111
|
+
self.finished_ok.emit()
|
|
112
|
+
|
|
113
|
+
def _configure_secondary_boards(self) -> None:
|
|
114
|
+
"""Push voltage ranges and oversampling to the non-primary boards."""
|
|
115
|
+
for port, ranges in zip(self.ports[1:], self.voltage_ranges[1:]):
|
|
116
|
+
if self._stop_requested: # arming can take seconds per board
|
|
117
|
+
return
|
|
118
|
+
self.stateChanged.emit("arming", f"Configuring board on {port}...")
|
|
119
|
+
board = OasisBoard(mode="serial", port=port, baudrate=self.baudrate)
|
|
120
|
+
try:
|
|
121
|
+
board.connect()
|
|
122
|
+
board.set_parameters(
|
|
123
|
+
t_sample=self.duration,
|
|
124
|
+
f_sample=self.sample_rate,
|
|
125
|
+
voltage_range=ranges,
|
|
126
|
+
trigger=self.trigger_level > 0,
|
|
127
|
+
v_trigg=self.trigger_level,
|
|
128
|
+
oversampling=self.oversampling_exp,
|
|
129
|
+
sync_mode=0,
|
|
130
|
+
)
|
|
131
|
+
board.set_oversampling_and_range_only()
|
|
132
|
+
finally:
|
|
133
|
+
try:
|
|
134
|
+
board.close()
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
def _acquire_once(self) -> dict | None:
|
|
139
|
+
filename = datetime.now().strftime("%Y-%m-%d-%H.%M.%S.%f")[:-3]
|
|
140
|
+
stored = self._sample(filename)
|
|
141
|
+
if stored is None:
|
|
142
|
+
return None
|
|
143
|
+
with tempfile.TemporaryDirectory(prefix="oasis_pull_") as scratch:
|
|
144
|
+
data, t = stack_sources(self._pull_all(stored, scratch))
|
|
145
|
+
return {
|
|
146
|
+
"data": data,
|
|
147
|
+
"t": t,
|
|
148
|
+
"sample_rate": float(self.sample_rate),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
def _sample(self, filename: str) -> str | None:
|
|
152
|
+
"""Arm the primary board and wait until the capture is stored.
|
|
153
|
+
|
|
154
|
+
Returns the file name the boards stored the capture under, or None
|
|
155
|
+
when the acquisition was stopped. This is ``OasisBoard.acquire()``
|
|
156
|
+
without its final step: the capture stays on the boards so that every
|
|
157
|
+
board can be pulled at the same time afterwards.
|
|
158
|
+
"""
|
|
159
|
+
board = OasisBoard(mode="serial", port=self.ports[0], baudrate=self.baudrate)
|
|
160
|
+
with self._board_lock:
|
|
161
|
+
self._board = board
|
|
162
|
+
try:
|
|
163
|
+
board.set_parameters(
|
|
164
|
+
t_sample=self.duration,
|
|
165
|
+
f_sample=self.sample_rate,
|
|
166
|
+
voltage_range=self.voltage_ranges[0],
|
|
167
|
+
trigger=self.trigger_level > 0,
|
|
168
|
+
v_trigg=self.trigger_level,
|
|
169
|
+
oversampling=self.oversampling_exp,
|
|
170
|
+
sync_mode=0,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@board.on_trigger()
|
|
174
|
+
def _on_trigger() -> None:
|
|
175
|
+
self.stateChanged.emit("triggered", "Impact detected - recording...")
|
|
176
|
+
|
|
177
|
+
board.connect()
|
|
178
|
+
board.set_oversampling_and_range_only()
|
|
179
|
+
try:
|
|
180
|
+
board.ser.reset_input_buffer()
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
if self.trigger_level > 0:
|
|
185
|
+
self.stateChanged.emit(
|
|
186
|
+
"waiting",
|
|
187
|
+
f"Armed - waiting for impact (trigger {self.trigger_level:g} V on CH1)",
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
self.stateChanged.emit("waiting", "Recording (free run)...")
|
|
191
|
+
|
|
192
|
+
trigger_level = board.trigger_level if board.trigger_mode else 0
|
|
193
|
+
command = (
|
|
194
|
+
f"OASIS.Sample({board.t_sample},{board.f_sample},"
|
|
195
|
+
f"{trigger_level},{board.sync_mode},{filename})"
|
|
196
|
+
)
|
|
197
|
+
board.ser.write((command + "\n").encode("utf-8"))
|
|
198
|
+
stored = board._wait_for_serial_sample_completion(
|
|
199
|
+
board.ser, filename, log=self.log.emit
|
|
200
|
+
)
|
|
201
|
+
return None if self._stop_requested else stored
|
|
202
|
+
finally:
|
|
203
|
+
with self._board_lock:
|
|
204
|
+
self._board = None
|
|
205
|
+
try:
|
|
206
|
+
board.close() # the capture travels over USB MSC, not over serial
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
def _pull_all(self, filename: str, output_dir: str) -> list[dict]:
|
|
211
|
+
"""Pull the capture from every board at once, in port order.
|
|
212
|
+
|
|
213
|
+
Seen on the bench: after a number of captures a board's USB MSC volume
|
|
214
|
+
no longer re-appears on the host and that board stops capturing until
|
|
215
|
+
it is rebooted. A failed transfer therefore reboots the boards that
|
|
216
|
+
failed, pushes the settings to them again and retries once; a second
|
|
217
|
+
failure is reported (that impact is lost).
|
|
218
|
+
"""
|
|
219
|
+
count = len(self.ports)
|
|
220
|
+
detail = (
|
|
221
|
+
"Transferring data..."
|
|
222
|
+
if count == 1
|
|
223
|
+
else f"Transferring data from {count} boards in parallel..."
|
|
224
|
+
)
|
|
225
|
+
for attempt in range(1, PULL_ATTEMPTS + 1):
|
|
226
|
+
self._pull_percent = {}
|
|
227
|
+
self.stateChanged.emit(
|
|
228
|
+
"transferring", detail + (" (retry)" if attempt > 1 else "")
|
|
229
|
+
)
|
|
230
|
+
self.progress.emit(0)
|
|
231
|
+
try:
|
|
232
|
+
return collect_serial_sources_parallel(
|
|
233
|
+
filename=filename,
|
|
234
|
+
serial_ports=self.ports,
|
|
235
|
+
t_sample=self.duration,
|
|
236
|
+
f_sample=self.sample_rate,
|
|
237
|
+
output_dir=output_dir,
|
|
238
|
+
baudrate=self.baudrate,
|
|
239
|
+
progress=self._pull_progress,
|
|
240
|
+
)
|
|
241
|
+
except PullError as exc:
|
|
242
|
+
if attempt >= PULL_ATTEMPTS or self._stop_requested:
|
|
243
|
+
raise
|
|
244
|
+
self.log.emit(
|
|
245
|
+
f"[GUI] {exc}; rebooting the board(s) that failed and retrying..."
|
|
246
|
+
)
|
|
247
|
+
self._reboot_boards(exc.ports)
|
|
248
|
+
raise RuntimeError("unreachable")
|
|
249
|
+
|
|
250
|
+
def _pull_progress(self, port: str, percent: int) -> None:
|
|
251
|
+
"""Mean transfer progress over all boards (called from pull threads)."""
|
|
252
|
+
self._pull_percent[port] = int(percent)
|
|
253
|
+
self.progress.emit(int(sum(self._pull_percent.values()) / len(self.ports)))
|
|
254
|
+
|
|
255
|
+
def _reboot_boards(self, ports: list[str]) -> None:
|
|
256
|
+
for port in ports:
|
|
257
|
+
if self._stop_requested:
|
|
258
|
+
return
|
|
259
|
+
self.stateChanged.emit("arming", f"Rebooting board on {port}...")
|
|
260
|
+
reboot_board(port, self.baudrate)
|
|
261
|
+
if any(port in ports for port in self.ports[1:]):
|
|
262
|
+
# a rebooted board lost its voltage ranges and oversampling
|
|
263
|
+
self._configure_secondary_boards()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# --- multi-board helpers (adapted from oasis-gui) ----------------------------
|
|
267
|
+
def collect_serial_sources_parallel(
|
|
268
|
+
filename: str,
|
|
269
|
+
serial_ports: list[str],
|
|
270
|
+
t_sample: float,
|
|
271
|
+
f_sample: float,
|
|
272
|
+
output_dir: str | None = None,
|
|
273
|
+
baudrate: int = DEFAULT_SERIAL_BAUDRATE,
|
|
274
|
+
timeout_s: float = 45.0,
|
|
275
|
+
progress=None, # callable(port, percent), called from the pull threads
|
|
276
|
+
) -> list[dict]:
|
|
277
|
+
"""Pull the stored capture from every board's USB MSC volume in parallel.
|
|
278
|
+
|
|
279
|
+
Each board copies from its own volume, so one thread per board is used and
|
|
280
|
+
the transfer takes as long as the slowest board rather than the sum of all
|
|
281
|
+
of them. Sources are returned in the order of ``serial_ports``; a board
|
|
282
|
+
that fails raises `PullError` naming every port that did not deliver.
|
|
283
|
+
"""
|
|
284
|
+
ports = []
|
|
285
|
+
seen = set()
|
|
286
|
+
for port in serial_ports:
|
|
287
|
+
if port and port not in seen:
|
|
288
|
+
seen.add(port)
|
|
289
|
+
ports.append(port)
|
|
290
|
+
if not ports:
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
if output_dir is None:
|
|
294
|
+
output_dir = tempfile.mkdtemp(prefix=f"oasis_pull_{filename}_")
|
|
295
|
+
index_map = {port: index for index, port in enumerate(ports)}
|
|
296
|
+
effective_timeout_s = max(float(timeout_s), float(t_sample) * 2.0 + 10.0)
|
|
297
|
+
|
|
298
|
+
def pull_one(port: str) -> dict:
|
|
299
|
+
board = OasisBoard(mode="serial", port=port, baudrate=baudrate)
|
|
300
|
+
result = board.collect_distributed_serial_files(
|
|
301
|
+
filename=filename,
|
|
302
|
+
serial_ports=[port],
|
|
303
|
+
t_sample=t_sample,
|
|
304
|
+
f_sample=f_sample,
|
|
305
|
+
output_dir=output_dir,
|
|
306
|
+
baudrate=baudrate,
|
|
307
|
+
timeout=effective_timeout_s,
|
|
308
|
+
print_log=lambda *_: None,
|
|
309
|
+
download_progress=progress,
|
|
310
|
+
)
|
|
311
|
+
if not result:
|
|
312
|
+
raise RuntimeError("No file collected.")
|
|
313
|
+
return result[0]
|
|
314
|
+
|
|
315
|
+
sources: list[dict] = []
|
|
316
|
+
failures: dict[str, str] = {}
|
|
317
|
+
with ThreadPoolExecutor(max_workers=len(ports)) as pool:
|
|
318
|
+
futures = {pool.submit(pull_one, port): port for port in ports}
|
|
319
|
+
for future in as_completed(futures):
|
|
320
|
+
port = futures[future]
|
|
321
|
+
try:
|
|
322
|
+
sources.append(future.result())
|
|
323
|
+
except Exception as exc:
|
|
324
|
+
failures[port] = str(exc)
|
|
325
|
+
if failures:
|
|
326
|
+
raise PullError({port: failures[port] for port in ports if port in failures})
|
|
327
|
+
sources.sort(key=lambda entry: index_map.get(entry.get("port", ""), 9999))
|
|
328
|
+
return sources
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def stack_sources(sources: list[dict]) -> tuple[np.ndarray, np.ndarray]:
|
|
332
|
+
"""Decode pulled captures and stack the boards' channels in port order.
|
|
333
|
+
|
|
334
|
+
Returns the data [channels, samples] in volts and the time axis of the
|
|
335
|
+
primary board (negative before the trigger). Captures that differ in
|
|
336
|
+
length by a sample are truncated to the shortest one.
|
|
337
|
+
"""
|
|
338
|
+
if not sources:
|
|
339
|
+
raise RuntimeError("No capture was collected.")
|
|
340
|
+
|
|
341
|
+
blocks = []
|
|
342
|
+
times = []
|
|
343
|
+
for source in sources:
|
|
344
|
+
offline = OasisBoard(mode="offline")
|
|
345
|
+
offline.load_from_files(
|
|
346
|
+
meta_path=source["meta_path"],
|
|
347
|
+
raw_path=source.get("raw_path"),
|
|
348
|
+
pretrig_raw_path=source.get("pretrig_raw_path"),
|
|
349
|
+
print_log=lambda *_: None,
|
|
350
|
+
)
|
|
351
|
+
blocks.append(np.asarray(offline.OASISData, dtype=float))
|
|
352
|
+
times.append(np.asarray(offline.t, dtype=float))
|
|
353
|
+
|
|
354
|
+
target = int(min(block.shape[1] for block in blocks))
|
|
355
|
+
return np.vstack([block[:, :target] for block in blocks]), times[0][:target]
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def reboot_board(port: str, baudrate: int = DEFAULT_SERIAL_BAUDRATE) -> None:
|
|
359
|
+
"""``OASIS.Reboot()`` over serial; returns once the port re-enumerates.
|
|
360
|
+
The USB MSC volume follows a few seconds later (oasis-api waits for it
|
|
361
|
+
when the capture is pulled)."""
|
|
362
|
+
OasisBoard._reboot_serial_port_for_msc_refresh(port, baudrate=baudrate)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def list_serial_ports() -> list[str]:
|
|
366
|
+
from serial.tools import list_ports
|
|
367
|
+
|
|
368
|
+
return [info.device for info in list_ports.comports()]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def parse_device_info(raw: str) -> dict | None:
|
|
372
|
+
"""Parse the ``OASIS.RawInfo()`` response (same layout as oasis-gui):
|
|
373
|
+
``hw;fw;adc_bits;teds;wss;device_name;architecture``."""
|
|
374
|
+
cleaned = str(raw).replace("\r", "").replace("\n", "")
|
|
375
|
+
fields = [entry.strip() for entry in cleaned.split(";") if entry.strip()]
|
|
376
|
+
if not fields:
|
|
377
|
+
return None
|
|
378
|
+
if ")" in fields[0]:
|
|
379
|
+
fields[0] = fields[0].rsplit(")", 1)[-1].strip()
|
|
380
|
+
if len(fields) < 7:
|
|
381
|
+
return None
|
|
382
|
+
try:
|
|
383
|
+
return {
|
|
384
|
+
"hw_version": fields[0],
|
|
385
|
+
"fw_version": fields[1],
|
|
386
|
+
"adc_bits": int(fields[2]),
|
|
387
|
+
"teds": int(fields[3]),
|
|
388
|
+
"wss": int(fields[4]),
|
|
389
|
+
"device_name": fields[5],
|
|
390
|
+
"architecture": fields[6],
|
|
391
|
+
}
|
|
392
|
+
except ValueError:
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def read_device_info(port: str, baudrate: int = DEFAULT_SERIAL_BAUDRATE) -> dict:
|
|
397
|
+
"""Connect to a board, read its stored device info, and disconnect."""
|
|
398
|
+
board = OasisBoard(mode="serial", port=port, baudrate=baudrate)
|
|
399
|
+
board.connect()
|
|
400
|
+
try:
|
|
401
|
+
raw = board.device_raw_info()
|
|
402
|
+
finally:
|
|
403
|
+
try:
|
|
404
|
+
board.close()
|
|
405
|
+
except Exception:
|
|
406
|
+
pass
|
|
407
|
+
fields = parse_device_info(raw)
|
|
408
|
+
if fields is None:
|
|
409
|
+
raise RuntimeError(f"Unexpected device info response: {raw!r}")
|
|
410
|
+
return fields
|