openm454 0.1.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.
- openm454/__init__.py +19 -0
- openm454/__main__.py +4 -0
- openm454/acquire.py +437 -0
- openm454/awg.py +97 -0
- openm454/awg_modes.py +969 -0
- openm454/capture.py +173 -0
- openm454/cli.py +883 -0
- openm454/decode.py +737 -0
- openm454/device.py +162 -0
- openm454/errors.py +29 -0
- openm454/firmware.py +100 -0
- openm454/fixture.py +34 -0
- openm454/host.py +98 -0
- openm454/lock.py +75 -0
- openm454/logger.py +972 -0
- openm454/mcp_server.py +807 -0
- openm454/measure.py +237 -0
- openm454/mso.py +733 -0
- openm454/mso_adapter.py +132 -0
- openm454/packet.py +659 -0
- openm454/paths.py +22 -0
- openm454/plot.py +112 -0
- openm454/protosim.py +172 -0
- openm454/regs.py +30 -0
- openm454/scope.py +798 -0
- openm454/selftest.py +221 -0
- openm454/transport.py +345 -0
- openm454/units.py +82 -0
- openm454-0.1.0.dist-info/METADATA +253 -0
- openm454-0.1.0.dist-info/RECORD +34 -0
- openm454-0.1.0.dist-info/WHEEL +5 -0
- openm454-0.1.0.dist-info/entry_points.txt +3 -0
- openm454-0.1.0.dist-info/licenses/LICENSE +674 -0
- openm454-0.1.0.dist-info/top_level.txt +1 -0
openm454/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""openm454: driver and CLI for the DreamSourceLab DSCope M454 (USB 2A0E:0080, "v3" protocol).
|
|
2
|
+
|
|
3
|
+
Quick use:
|
|
4
|
+
from openm454 import Scope
|
|
5
|
+
with Scope() as s:
|
|
6
|
+
cap = s.capture(channels="A1,D0", timebase=500e-6)
|
|
7
|
+
"""
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
from .errors import DscopeError # noqa: F401
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
if name == "Scope":
|
|
14
|
+
from .scope import Scope
|
|
15
|
+
return Scope
|
|
16
|
+
if name == "load_capture":
|
|
17
|
+
from .capture import load_capture
|
|
18
|
+
return load_capture
|
|
19
|
+
raise AttributeError(name)
|
openm454/__main__.py
ADDED
openm454/acquire.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""Acquisition engine: run one register-programmed capture and collect the raw stream.
|
|
2
|
+
|
|
3
|
+
Mirrors DSWave 1.2.1 (UsbDevice_TransferThread 0x33f950, UsbBsp3x Setup/Start/StopAcquire and the UsbTransfer
|
|
4
|
+
callbacks; RVAs refer to DSWave.exe 1.2.1):
|
|
5
|
+
|
|
6
|
+
1. >= 100 ms after the previous run and after the last register write: setup program
|
|
7
|
+
(W 0x01=0, settle, MSO writes ..., W 0x00 MODE, W 0xF9=0)
|
|
8
|
+
2. submit EP 0x86 reads: one 4096-byte head transfer + N data transfers (no timeout); wait 50 ms
|
|
9
|
+
3. start: W SYNC=sync, W ACTION=0, W CTRL=1; settle. From the moment W CTRL=1 is sent until the stop handshake is
|
|
10
|
+
done every register access sits inside an EP 0x84 handshake (Transport.running / handshake / handshake_writes)
|
|
11
|
+
4. collect: data transfers are resubmitted from their callback for the WHOLE run, also after the record is
|
|
12
|
+
complete, because the device keeps streaming zero padding until it is stopped. While the head has not
|
|
13
|
+
arrived the progress register is polled like DSWave (handshake + R 0x02, every ~15 ms from 100 ms on)
|
|
14
|
+
5. normal stop: handshake{W CTRL=0, W ACTION=8}; stop resubmitting; cancel every transfer and wait until all
|
|
15
|
+
are back; W CTRL=0, 1 ms, W ACTION=4
|
|
16
|
+
error stop (failed handshake or control transfer, bad head, transfer error, zero-length transfer,
|
|
17
|
+
resubmit failure): nothing more is sent to the device; cancel, wait, mark the session failed and raise
|
|
18
|
+
DEVICE_ERROR. As in DSWave's transfer thread, a completed record or an abort is handled before an error that
|
|
19
|
+
arrived with it: such an error gets the normal stop handshake and drain, but not the final writes.
|
|
20
|
+
|
|
21
|
+
Ctrl+C (SIGINT) during a run only requests the normal stop; KeyboardInterrupt is raised after the device has
|
|
22
|
+
been stopped. Exceptions inside the libusb callback become an error stop instead of being lost. Exceptions from the
|
|
23
|
+
caller's on_chunk / stop_flag are not device failures: the run is stopped normally and the exception re-raised.
|
|
24
|
+
|
|
25
|
+
DSWave's "1 ms" sleeps last ~15 ms on the Windows timer tick (recorded 14-31 ms), so the settle times below use
|
|
26
|
+
15 ms to stay inside the recorded envelope.
|
|
27
|
+
"""
|
|
28
|
+
import collections
|
|
29
|
+
import signal
|
|
30
|
+
import struct
|
|
31
|
+
import threading
|
|
32
|
+
import time
|
|
33
|
+
|
|
34
|
+
import usb1
|
|
35
|
+
|
|
36
|
+
from . import regs
|
|
37
|
+
from .errors import DscopeError
|
|
38
|
+
from .transport import HINT_REOPEN
|
|
39
|
+
|
|
40
|
+
HEAD_BYTES = 4096
|
|
41
|
+
HEAD_MAGIC = b'\x55\x55\x55\x55'
|
|
42
|
+
EP_DATA = 0x86
|
|
43
|
+
GAP_BETWEEN_RUNS = 0.100 # FUN_14033f270: 100 ms before every run
|
|
44
|
+
SETUP_SETTLE = 0.015 # SetupAcquire: "1 ms" after W CTRL=0 (recorded 14-31 ms)
|
|
45
|
+
SUBMIT_TO_START = 0.050 # TransferThread: 50 ms between submitting and the start writes
|
|
46
|
+
START_SETTLE = 0.015 # StartAcquire: "1 ms" after W CTRL=1 (first EP 0x84 read >= 14.7 ms later)
|
|
47
|
+
FINAL_GAP = 0.001 # ForceAcquire: W CTRL=0, 1 ms, W ACTION=4 (recorded 1.6-29 ms)
|
|
48
|
+
POLL_AFTER = 0.100 # progress polling starts 100 ms after the start writes
|
|
49
|
+
POLL_EVERY = 0.015
|
|
50
|
+
DRAIN_TIMEOUT = 2.0 # cancelled transfers must be back within this time
|
|
51
|
+
MAX_EVENTS_PER_PASS = 32 # events handled between two checks of abort / stop_flag / deadlines
|
|
52
|
+
|
|
53
|
+
_STATUS = {usb1.TRANSFER_COMPLETED: 'COMPLETED', usb1.TRANSFER_ERROR: 'ERROR', usb1.TRANSFER_TIMED_OUT: 'TIMED_OUT',
|
|
54
|
+
usb1.TRANSFER_CANCELLED: 'CANCELLED', usb1.TRANSFER_STALL: 'STALL', usb1.TRANSFER_NO_DEVICE: 'NO_DEVICE',
|
|
55
|
+
usb1.TRANSFER_OVERFLOW: 'OVERFLOW'}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RawCapture:
|
|
59
|
+
def __init__(self):
|
|
60
|
+
self.head = None
|
|
61
|
+
self.data = bytearray()
|
|
62
|
+
self.n_data = 0 # data bytes received (also counted when keep_data=False leaves data empty)
|
|
63
|
+
self.frames = [] # repeat mode: one entry per data transfer (frame data + 4 KiB tail)
|
|
64
|
+
self.t_start = None
|
|
65
|
+
self.t_head = None
|
|
66
|
+
self.t_done = None
|
|
67
|
+
self.timed_out = False
|
|
68
|
+
self.aborted = False # ended early: abort() / shutdown() / SIGINT / stop_flag
|
|
69
|
+
self.stopped = False # ... by the caller's stop_flag (a user stop request)
|
|
70
|
+
self.ep84 = [] # every EP 0x84 handshake byte, in order (None = timeout)
|
|
71
|
+
self.handshakes = [] # (ms after start, 'poll'|'stop', [bytes])
|
|
72
|
+
self.progress = [] # raw R 0x02 values from the progress polls
|
|
73
|
+
self.stop = None # 'normal' | 'error' | None (never started)
|
|
74
|
+
self.error = None
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def head_words(self):
|
|
78
|
+
return struct.unpack('<8I', self.head[:32]) if self.head else None
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def triggered(self):
|
|
82
|
+
w = self.head_words
|
|
83
|
+
return None if w is None else not (w[2] & 0x80000000)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Engine:
|
|
87
|
+
def __init__(self, device):
|
|
88
|
+
self.dev = device
|
|
89
|
+
self.t = device.t
|
|
90
|
+
self.sync = 0
|
|
91
|
+
self.active = False
|
|
92
|
+
self._t_end = time.time()
|
|
93
|
+
self._abort = threading.Event()
|
|
94
|
+
self._idle = threading.Event()
|
|
95
|
+
self._idle.set()
|
|
96
|
+
self._state = threading.Lock()
|
|
97
|
+
self._shutdown = False
|
|
98
|
+
self._stale = [] # transfers that never came back after a cancel (kept alive, never freed)
|
|
99
|
+
self._sigint = False
|
|
100
|
+
|
|
101
|
+
# ------------------------------------------------------------------ cross-thread control
|
|
102
|
+
def abort(self):
|
|
103
|
+
"""Ask a running capture to stop (thread-safe). The run loop performs the normal stop handshake."""
|
|
104
|
+
self._abort.set()
|
|
105
|
+
|
|
106
|
+
def shutdown(self):
|
|
107
|
+
"""Sticky abort (watchdog): stop the current run and refuse every later one."""
|
|
108
|
+
with self._state:
|
|
109
|
+
self._shutdown = True
|
|
110
|
+
self._abort.set()
|
|
111
|
+
|
|
112
|
+
def wait_idle(self, timeout):
|
|
113
|
+
return self._idle.wait(timeout)
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------ transfers
|
|
116
|
+
def _submit(self, n):
|
|
117
|
+
tr = self.t.h.getTransfer()
|
|
118
|
+
tr.setBulk(EP_DATA, n, callback=self._cb, timeout=0)
|
|
119
|
+
self._trs.append(tr)
|
|
120
|
+
tr.submit()
|
|
121
|
+
return tr
|
|
122
|
+
|
|
123
|
+
def _cb(self, tr):
|
|
124
|
+
"""Runs inside libusb event handling (handleEventsTimeout and every synchronous transfer, e.g. the EP 0x84
|
|
125
|
+
handshake reads). Data transfers are resubmitted here, like UsbTransfer_DataCallback, so the reads stay
|
|
126
|
+
queued during handshakes. An exception must not escape: ctypes would swallow it and the transfer would be
|
|
127
|
+
lost silently, so it becomes an error event."""
|
|
128
|
+
try:
|
|
129
|
+
if self._stopping or self.t.failed:
|
|
130
|
+
return # DataCallback: stopping or device in error -> free, no resubmit
|
|
131
|
+
st = tr.getStatus()
|
|
132
|
+
n = tr.getActualLength()
|
|
133
|
+
if tr is self._head_tr:
|
|
134
|
+
if st != usb1.TRANSFER_COMPLETED:
|
|
135
|
+
self._events.append(('error', 'head transfer ended with status %s' % _STATUS.get(st, st)))
|
|
136
|
+
return
|
|
137
|
+
buf = bytes(memoryview(tr.getBuffer())[:n])
|
|
138
|
+
if len(buf) != HEAD_BYTES or buf[:4] != HEAD_MAGIC:
|
|
139
|
+
self._events.append(('error', 'bad head packet (%d bytes, check id %s)' % (len(buf), buf[:4].hex())))
|
|
140
|
+
else:
|
|
141
|
+
self._events.append(('head', buf))
|
|
142
|
+
return
|
|
143
|
+
if st != usb1.TRANSFER_COMPLETED:
|
|
144
|
+
self._events.append(('error', 'data transfer ended with status %s' % _STATUS.get(st, st)))
|
|
145
|
+
return
|
|
146
|
+
if n == 0:
|
|
147
|
+
self._events.append(('error', 'zero-length data transfer'))
|
|
148
|
+
return
|
|
149
|
+
if not self._full:
|
|
150
|
+
self._events.append(('data', bytes(memoryview(tr.getBuffer())[:n])))
|
|
151
|
+
try:
|
|
152
|
+
tr.submit()
|
|
153
|
+
except usb1.USBError as e:
|
|
154
|
+
self._events.append(('error', 'data transfer resubmit failed: %s' % e))
|
|
155
|
+
except BaseException as e: # noqa: BLE001 - see docstring
|
|
156
|
+
self._events.append(('error', 'exception in the transfer callback: %r' % (e,)))
|
|
157
|
+
|
|
158
|
+
def _cancel_and_wait(self):
|
|
159
|
+
for tr in self._trs:
|
|
160
|
+
try:
|
|
161
|
+
if tr.isSubmitted():
|
|
162
|
+
tr.cancel()
|
|
163
|
+
except usb1.USBError:
|
|
164
|
+
pass
|
|
165
|
+
end = time.time() + DRAIN_TIMEOUT
|
|
166
|
+
while any(tr.isSubmitted() for tr in self._trs):
|
|
167
|
+
if time.time() > end:
|
|
168
|
+
stale = [tr for tr in self._trs if tr.isSubmitted()]
|
|
169
|
+
self._stale.extend(stale)
|
|
170
|
+
self.t.stale = True
|
|
171
|
+
self.t.stale_transfers.extend(stale)
|
|
172
|
+
return False
|
|
173
|
+
self.t.ctx.handleEventsTimeout(0.02)
|
|
174
|
+
self._events.clear()
|
|
175
|
+
for tr in self._trs:
|
|
176
|
+
try:
|
|
177
|
+
tr.close()
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
# ------------------------------------------------------------------ run
|
|
183
|
+
def run(self, setup_writes, pkt_bytes, need_bytes, n_transfers=16, trigger_timeout=5.0,
|
|
184
|
+
collect_timeout=10.0, repeat_frames=0, on_chunk=None, stop_flag=None, poll=True, settle=0.0,
|
|
185
|
+
keep_data=True):
|
|
186
|
+
"""Execute one capture. need_bytes = data bytes (after the head) to collect.
|
|
187
|
+
repeat_frames > 0: repeat mode; after the head every data transfer is one frame (data + 4 KiB tail);
|
|
188
|
+
collect that many frames. on_chunk(bytes) is called for the head and for every data chunk as it arrives
|
|
189
|
+
(live streaming). stop_flag(): optional callable; the capture stops when it returns True.
|
|
190
|
+
settle: extra seconds between the setup writes and the start (front end recovering from overdrive).
|
|
191
|
+
collect_timeout=None: no deadline after the head (loop mode streams until stop_flag ends it).
|
|
192
|
+
keep_data=False: data chunks only go to on_chunk (cap.data stays empty, cap.n_data counts them), so an
|
|
193
|
+
endless stream does not grow in memory.
|
|
194
|
+
Returns a RawCapture; cap.head is None when nothing arrived before trigger_timeout. Raises DEVICE_ERROR
|
|
195
|
+
after an error stop (the session is then unusable until the device is reopened)."""
|
|
196
|
+
t = self.t
|
|
197
|
+
with self._state:
|
|
198
|
+
if self._shutdown:
|
|
199
|
+
raise DscopeError('TIMEOUT_WATCHDOG', 'the session is shutting down (watchdog)')
|
|
200
|
+
if self.active:
|
|
201
|
+
raise DscopeError('BUSY', 'a capture is already running on this engine')
|
|
202
|
+
self._abort.clear()
|
|
203
|
+
self._idle.clear()
|
|
204
|
+
self.active = True
|
|
205
|
+
self._sigint = False
|
|
206
|
+
restore = None
|
|
207
|
+
try:
|
|
208
|
+
if threading.current_thread() is threading.main_thread():
|
|
209
|
+
def on_sigint(signum, frame):
|
|
210
|
+
self._sigint = True
|
|
211
|
+
self._abort.set()
|
|
212
|
+
try:
|
|
213
|
+
restore = (signal.signal(signal.SIGINT, on_sigint),)
|
|
214
|
+
except (ValueError, OSError):
|
|
215
|
+
restore = None
|
|
216
|
+
if t.failed:
|
|
217
|
+
raise DscopeError('DEVICE_ERROR', 'device failed earlier in this session (%s)' % t.failed, HINT_REOPEN)
|
|
218
|
+
gap = max(self._t_end, t.t_last_write) + GAP_BETWEEN_RUNS - time.time()
|
|
219
|
+
if gap > 0:
|
|
220
|
+
time.sleep(gap)
|
|
221
|
+
cap = self._run(setup_writes, pkt_bytes, need_bytes, n_transfers, trigger_timeout, collect_timeout,
|
|
222
|
+
repeat_frames, on_chunk, stop_flag, poll, settle, keep_data)
|
|
223
|
+
finally:
|
|
224
|
+
self._t_end = time.time()
|
|
225
|
+
self.active = False
|
|
226
|
+
self._idle.set()
|
|
227
|
+
if restore is not None: # last: a SIGINT after this may raise right here
|
|
228
|
+
try:
|
|
229
|
+
signal.signal(signal.SIGINT, restore[0] if restore[0] is not None else signal.default_int_handler)
|
|
230
|
+
except (ValueError, OSError):
|
|
231
|
+
pass
|
|
232
|
+
if self._sigint:
|
|
233
|
+
raise KeyboardInterrupt
|
|
234
|
+
return cap
|
|
235
|
+
|
|
236
|
+
def _run(self, setup_writes, pkt_bytes, need_bytes, n_transfers, trigger_timeout, collect_timeout,
|
|
237
|
+
repeat_frames, on_chunk, stop_flag, poll, settle=0.0, keep_data=True):
|
|
238
|
+
t = self.t
|
|
239
|
+
cap = RawCapture()
|
|
240
|
+
self._events = collections.deque()
|
|
241
|
+
self._stopping = False
|
|
242
|
+
self._full = False
|
|
243
|
+
self._late_error = None
|
|
244
|
+
self._user_exc = None # exception from on_chunk / stop_flag (caller code)
|
|
245
|
+
self._trs = []
|
|
246
|
+
self._head_tr = None
|
|
247
|
+
|
|
248
|
+
# the device is idle here: setup writes are direct control transfers
|
|
249
|
+
for i, (addr, val) in enumerate(setup_writes):
|
|
250
|
+
t.write(addr, val)
|
|
251
|
+
if i == 0 and addr == regs.CTRL:
|
|
252
|
+
time.sleep(SETUP_SETTLE)
|
|
253
|
+
if settle > 0:
|
|
254
|
+
self._abort.wait(settle) # idle device: nothing is queued yet
|
|
255
|
+
if self._abort.is_set():
|
|
256
|
+
cap.aborted = True # stopped before the start: nothing to stop
|
|
257
|
+
return cap
|
|
258
|
+
try:
|
|
259
|
+
self._head_tr = self._submit(HEAD_BYTES)
|
|
260
|
+
for _ in range(n_transfers):
|
|
261
|
+
self._submit(pkt_bytes)
|
|
262
|
+
except usb1.USBError as e:
|
|
263
|
+
self._stopping = True # nothing was started: just take the reads back
|
|
264
|
+
self._cancel_and_wait()
|
|
265
|
+
raise DscopeError('USB_ERROR', 'cannot queue EP 0x86 reads: %s' % e, HINT_REOPEN)
|
|
266
|
+
time.sleep(SUBMIT_TO_START)
|
|
267
|
+
|
|
268
|
+
error = None
|
|
269
|
+
try:
|
|
270
|
+
try:
|
|
271
|
+
with t.guarded('run'):
|
|
272
|
+
t.write(regs.SYNC, self.sync)
|
|
273
|
+
t.write(regs.ACTION, 0x0000)
|
|
274
|
+
t.write(regs.CTRL, 0x0001) # Transport.running is set as this is sent
|
|
275
|
+
time.sleep(START_SETTLE)
|
|
276
|
+
cap.t_start = time.time()
|
|
277
|
+
error = self._collect(cap, need_bytes, trigger_timeout, collect_timeout, repeat_frames, on_chunk,
|
|
278
|
+
stop_flag, poll, keep_data)
|
|
279
|
+
except DscopeError as e: # failed start write or progress-poll handshake
|
|
280
|
+
error = 'start write failed: %s' % e.message if not cap.t_start else e.message
|
|
281
|
+
except BaseException as exc: # a bug in the engine loop: stop like a user stop, then re-raise
|
|
282
|
+
err = self._finish(cap, None)
|
|
283
|
+
if err:
|
|
284
|
+
raise DscopeError('DEVICE_ERROR', err, HINT_REOPEN) from exc
|
|
285
|
+
raise
|
|
286
|
+
error = self._finish(cap, error)
|
|
287
|
+
finally:
|
|
288
|
+
if not self._stopping: # something escaped before the reads were taken back
|
|
289
|
+
t.fail(t.failed or 'interrupted while stopping')
|
|
290
|
+
self._stopping = True
|
|
291
|
+
t.running = False
|
|
292
|
+
self._cancel_and_wait()
|
|
293
|
+
cap.t_done = time.time()
|
|
294
|
+
if error:
|
|
295
|
+
raise DscopeError('DEVICE_ERROR', error, HINT_REOPEN) from self._user_exc
|
|
296
|
+
if self._user_exc is not None: # caller code failed: the device was stopped normally first
|
|
297
|
+
raise self._user_exc
|
|
298
|
+
return cap
|
|
299
|
+
|
|
300
|
+
def _finish(self, cap, error):
|
|
301
|
+
"""Normal stop when possible, error stop otherwise. Returns the error that remains (None = clean)."""
|
|
302
|
+
if error is None and self.t.running and not self.t.failed:
|
|
303
|
+
return self._stop_normal(cap)
|
|
304
|
+
self._stop_error(cap, error or self.t.failed or 'run ended in an unknown state')
|
|
305
|
+
return cap.error
|
|
306
|
+
|
|
307
|
+
def _collect(self, cap, need_bytes, trigger_timeout, collect_timeout, repeat_frames, on_chunk, stop_flag,
|
|
308
|
+
poll, keep_data=True):
|
|
309
|
+
"""Pump events until the record is complete, the caller stops, or a deadline passes. Returns None, or an
|
|
310
|
+
error reason that requires the error stop."""
|
|
311
|
+
t = self.t
|
|
312
|
+
ctx = t.ctx
|
|
313
|
+
deadline_head = cap.t_start + trigger_timeout
|
|
314
|
+
deadline = None
|
|
315
|
+
last_poll = 0.0
|
|
316
|
+
polling = poll and not repeat_frames
|
|
317
|
+
while True:
|
|
318
|
+
ctx.handleEventsTimeout(0.005)
|
|
319
|
+
budget = MAX_EVENTS_PER_PASS # bounded, so stop requests and deadlines are always checked
|
|
320
|
+
while self._events and budget > 0:
|
|
321
|
+
budget -= 1
|
|
322
|
+
kind, buf = self._events.popleft()
|
|
323
|
+
if kind == 'error':
|
|
324
|
+
if self._full or self._abort.is_set():
|
|
325
|
+
# TransferThread checks 'frame over' / abort before error: stop normally first
|
|
326
|
+
self._late_error = self._late_error or buf
|
|
327
|
+
continue
|
|
328
|
+
return buf
|
|
329
|
+
if kind == 'head':
|
|
330
|
+
cap.head = buf
|
|
331
|
+
cap.t_head = time.time()
|
|
332
|
+
deadline = cap.t_head + collect_timeout if collect_timeout is not None else None
|
|
333
|
+
elif self._full:
|
|
334
|
+
continue # zero padding after the record: discarded
|
|
335
|
+
elif repeat_frames:
|
|
336
|
+
cap.frames.append(buf)
|
|
337
|
+
else:
|
|
338
|
+
cap.n_data += len(buf)
|
|
339
|
+
if keep_data:
|
|
340
|
+
cap.data += buf
|
|
341
|
+
if on_chunk is not None:
|
|
342
|
+
try:
|
|
343
|
+
on_chunk(buf)
|
|
344
|
+
except BaseException as e: # caller code: normal stop, re-raised by _run
|
|
345
|
+
self._user_exc = e
|
|
346
|
+
return None
|
|
347
|
+
if self._enough(cap, need_bytes, repeat_frames):
|
|
348
|
+
self._full = True
|
|
349
|
+
ctx.handleEventsTimeout(0) # keep resubmitting while a backlog is processed
|
|
350
|
+
if self._full:
|
|
351
|
+
return None
|
|
352
|
+
if self._abort.is_set():
|
|
353
|
+
cap.aborted = True
|
|
354
|
+
return None
|
|
355
|
+
if stop_flag is not None:
|
|
356
|
+
try:
|
|
357
|
+
stop = stop_flag()
|
|
358
|
+
except BaseException as e: # caller code: normal stop, re-raised by _run
|
|
359
|
+
self._user_exc = e
|
|
360
|
+
return None
|
|
361
|
+
if stop:
|
|
362
|
+
cap.aborted = cap.stopped = True
|
|
363
|
+
return None
|
|
364
|
+
now = time.time()
|
|
365
|
+
if cap.head is None:
|
|
366
|
+
if now > deadline_head:
|
|
367
|
+
cap.timed_out = True
|
|
368
|
+
return None
|
|
369
|
+
if polling and now - cap.t_start >= POLL_AFTER and now - last_poll >= POLL_EVERY:
|
|
370
|
+
last_poll = now
|
|
371
|
+
rec = []
|
|
372
|
+
try:
|
|
373
|
+
with t.handshake(rec):
|
|
374
|
+
v = t.read16(regs.ACTION)
|
|
375
|
+
finally:
|
|
376
|
+
cap.ep84 += rec
|
|
377
|
+
cap.handshakes.append((round((now - cap.t_start) * 1000, 1), 'poll', rec))
|
|
378
|
+
cap.progress.append(v)
|
|
379
|
+
elif deadline is not None and now > deadline:
|
|
380
|
+
cap.timed_out = True
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
@staticmethod
|
|
384
|
+
def _enough(cap, need_bytes, repeat_frames):
|
|
385
|
+
if repeat_frames:
|
|
386
|
+
return len(cap.frames) >= repeat_frames
|
|
387
|
+
return cap.head is not None and need_bytes is not None and cap.n_data >= need_bytes
|
|
388
|
+
|
|
389
|
+
# ------------------------------------------------------------------ stop
|
|
390
|
+
def _stop_normal(self, cap):
|
|
391
|
+
"""DSWave StopAcquire + Drain + ForceAcquire. Returns None, or the reason the run ended in the error state."""
|
|
392
|
+
t = self.t
|
|
393
|
+
rec = []
|
|
394
|
+
t_rel = round((time.time() - cap.t_start) * 1000, 1) if cap.t_start else None
|
|
395
|
+
try:
|
|
396
|
+
t.handshake_writes([(regs.CTRL, 0x0000), (regs.ACTION, 0x0008)], rec)
|
|
397
|
+
except DscopeError as e:
|
|
398
|
+
cap.ep84 += rec
|
|
399
|
+
cap.handshakes.append((t_rel, 'stop', rec))
|
|
400
|
+
self._stop_error(cap, 'stop handshake failed: %s' % e.message)
|
|
401
|
+
return cap.error
|
|
402
|
+
cap.ep84 += rec
|
|
403
|
+
cap.handshakes.append((t_rel, 'stop', rec))
|
|
404
|
+
if self._late_error is None: # data-path errors that arrived during the handshake
|
|
405
|
+
self._late_error = next((b for k, b in self._events if k == 'error'), None)
|
|
406
|
+
self._stopping = True
|
|
407
|
+
t.running = False
|
|
408
|
+
if not self._cancel_and_wait():
|
|
409
|
+
why = 'EP 0x86 reads did not come back within %.0f s of the cancel' % DRAIN_TIMEOUT
|
|
410
|
+
t.fail(why)
|
|
411
|
+
cap.stop, cap.error = 'error', why
|
|
412
|
+
return why
|
|
413
|
+
if self._late_error: # DSWave: error stop after the normal one -> ForceAcquire refused
|
|
414
|
+
t.fail(self._late_error)
|
|
415
|
+
cap.stop, cap.error = 'error', self._late_error
|
|
416
|
+
return cap.error
|
|
417
|
+
try:
|
|
418
|
+
t.write(regs.CTRL, 0x0000)
|
|
419
|
+
time.sleep(FINAL_GAP)
|
|
420
|
+
t.write(regs.ACTION, 0x0004)
|
|
421
|
+
t.hold(GAP_BETWEEN_RUNS) # writes right after this can be dropped (run without trigger)
|
|
422
|
+
except DscopeError as e:
|
|
423
|
+
why = 'final stop write failed: %s' % e.message
|
|
424
|
+
t.fail(why)
|
|
425
|
+
cap.stop, cap.error = 'error', why
|
|
426
|
+
return why
|
|
427
|
+
cap.stop = 'normal'
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
def _stop_error(self, cap, why):
|
|
431
|
+
"""DSWave error stop: send nothing; take the reads back; the session stays failed."""
|
|
432
|
+
t = self.t
|
|
433
|
+
t.fail(why)
|
|
434
|
+
self._stopping = True
|
|
435
|
+
t.running = False
|
|
436
|
+
self._cancel_and_wait()
|
|
437
|
+
cap.stop, cap.error = 'error', why
|
openm454/awg.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Built-in arbitrary waveform generator (MCX output, 0..3 V, sine/square up to 1 MHz, triangle up to 400 kHz).
|
|
2
|
+
|
|
3
|
+
Register access is indirect: W 0x08 = AWG register address, then W 0x07 = value (DSWave's log labels these
|
|
4
|
+
the other way round). Formulas fitted to DSWave 1.2.1's own register programs (tests/fixtures/scenarios/corpus3.json) and
|
|
5
|
+
verified by loopback on A0:
|
|
6
|
+
FCW (4/5) = floor(f_Hz * 40) (32-bit, lo/hi)
|
|
7
|
+
RPAR/FPAR = int(3846153.2 / f_Hz + 0.5) (14/15, 16/17; edge parameter, same for both)
|
|
8
|
+
PHASE (6/7) = phase_deg / 360 * 4e9
|
|
9
|
+
HIGH/LOW (8/9) = trunc(uV / 6e6 * 0.91525 * 1023) (~156.05 codes per volt)
|
|
10
|
+
DUTY (10/11) = duty_fraction * 4e9
|
|
11
|
+
RSLOPE/FSLOPE (12/13) = floor(68.75 / duty), floor(68.75 / (1 - duty))
|
|
12
|
+
WAVE (0): 0 sine, 1 triangle, 2 square, 3 noise; MODE (1): 0 basic
|
|
13
|
+
SWITCH (2) = 1 ... 0 brackets mode/wave/frequency updates; ENABLE (3): 2 = armed, 3 = output running
|
|
14
|
+
Stopped (ENABLE 2), the output holds the LOW level; awg_off also writes LOW = 0 so "off" means 0 V.
|
|
15
|
+
Noise (measured 2026-10-31): Gaussian-like around (low+high)/2 (+30..160 mV), sigma ~2.8 % of (high-low), peaks
|
|
16
|
+
about +-0.2 x (high-low), flat to ~2 MHz; the frequency setting has no effect.
|
|
17
|
+
"""
|
|
18
|
+
import math
|
|
19
|
+
|
|
20
|
+
from .errors import DscopeError
|
|
21
|
+
|
|
22
|
+
A_WAVE, A_MODE, A_SWITCH, A_ENABLE = 0, 1, 2, 3
|
|
23
|
+
A_FCW0, A_FCW1, A_PH0, A_PH1, A_HIGH, A_LOW = 4, 5, 6, 7, 8, 9
|
|
24
|
+
A_DUTY0, A_DUTY1, A_RSLOPE, A_FSLOPE, A_RPAR0, A_RPAR1, A_FPAR0, A_FPAR1 = 10, 11, 12, 13, 14, 15, 16, 17
|
|
25
|
+
REG_DATA, REG_ADDR = 0x07, 0x08
|
|
26
|
+
WAVES = {'sine': 0, 'triangle': 1, 'square': 2, 'noise': 3}
|
|
27
|
+
F_MAX = {'sine': 1e6, 'square': 1e6, 'triangle': 400e3, 'noise': 1e6}
|
|
28
|
+
V_MAX = 3.0
|
|
29
|
+
MIN_PULSE_S = 380e-9
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def program(wave='sine', freq=1000.0, high=3.0, low=0.0, duty=50.0, phase=0.0):
|
|
33
|
+
"""Return the ordered list of (awg_addr, value) writes that configure (but do not start) the AWG."""
|
|
34
|
+
if wave not in WAVES:
|
|
35
|
+
raise DscopeError('INVALID_ARG', 'wave must be one of %s' % ', '.join(WAVES))
|
|
36
|
+
if not 0.1 <= freq <= F_MAX[wave]:
|
|
37
|
+
raise DscopeError('INVALID_ARG', '%s frequency must be 0.1 Hz .. %g Hz' % (wave, F_MAX[wave]))
|
|
38
|
+
if not 0.0 <= low < high <= V_MAX + 1e-9:
|
|
39
|
+
raise DscopeError('INVALID_ARG', 'levels must satisfy 0 <= low < high <= 3.0 V')
|
|
40
|
+
if not 0.1 <= duty <= 99.9:
|
|
41
|
+
raise DscopeError('INVALID_ARG', 'duty must be 0.1 .. 99.9 %')
|
|
42
|
+
if wave == 'square':
|
|
43
|
+
period = 1.0 / freq
|
|
44
|
+
if min(duty, 100 - duty) / 100 * period < MIN_PULSE_S * (1 - 1e-9): # 380 ns itself is allowed
|
|
45
|
+
raise DscopeError('INVALID_ARG', 'pulse narrower than 380 ns at this frequency/duty')
|
|
46
|
+
# DSWave's exact conversions (openm454/awg_modes.py replays its AWG handler write for write)
|
|
47
|
+
from .awg_modes import edge_param, level_code, micro, slopes
|
|
48
|
+
fcw = int((micro(freq) / 1e6) * 40.0)
|
|
49
|
+
rpar = edge_param(freq)
|
|
50
|
+
ph = int(((micro(phase % 360) // 1000000) / 360.0) * 4e9) & 0xFFFFFFFF # whole degrees, truncated
|
|
51
|
+
dw, rs, fs = slopes(duty)
|
|
52
|
+
dw &= 0xFFFFFFFF
|
|
53
|
+
hi, lo = level_code(high), level_code(low)
|
|
54
|
+
return [
|
|
55
|
+
(A_ENABLE, 0x0002),
|
|
56
|
+
(A_SWITCH, 1), (A_MODE, 0), (A_SWITCH, 0),
|
|
57
|
+
(A_SWITCH, 1), (A_WAVE, WAVES[wave]), (A_SWITCH, 0),
|
|
58
|
+
(A_SWITCH, 1), (A_FCW0, fcw & 0xFFFF), (A_FCW1, fcw >> 16), (A_RPAR0, rpar & 0xFFFF), (A_RPAR1, rpar >> 16),
|
|
59
|
+
(A_FPAR0, rpar & 0xFFFF), (A_FPAR1, rpar >> 16), (A_SWITCH, 0),
|
|
60
|
+
(A_PH0, ph & 0xFFFF), (A_PH1, ph >> 16),
|
|
61
|
+
(A_HIGH, hi), (A_LOW, lo),
|
|
62
|
+
(A_DUTY0, dw & 0xFFFF), (A_DUTY1, dw >> 16), (A_RSLOPE, rs & 0xFFFF), (A_FSLOPE, fs & 0xFFFF),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
START = [(A_SWITCH, 1), (A_ENABLE, 0x0003), (A_SWITCH, 0)]
|
|
67
|
+
STOP = [(A_ENABLE, 0x0002)]
|
|
68
|
+
#: after STOP the output holds the programmed LOW level (measured 2026-10-31); LOW = 0 parks it at 0 V. The next
|
|
69
|
+
#: program() rewrites LOW.
|
|
70
|
+
PARK = [(A_LOW, 0)]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def dc(level):
|
|
74
|
+
"""Writes for a steady DC output: stopped, the generator holds LOW (not a DSWave feature; measured)."""
|
|
75
|
+
if not 0.0 <= level <= V_MAX + 1e-9:
|
|
76
|
+
raise DscopeError('INVALID_ARG', 'DC level must be 0 .. 3.0 V')
|
|
77
|
+
from .awg_modes import level_code
|
|
78
|
+
return STOP + [(A_LOW, level_code(level))]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def apply(transport, writes):
|
|
82
|
+
for addr, val in writes:
|
|
83
|
+
transport.write(REG_ADDR, addr)
|
|
84
|
+
transport.write(REG_DATA, val)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
CODES_PER_V = 1023 * 0.91525 / 6.0 # HIGH/LOW code per volt (DSWave: trunc(uV / 6e6 * 0.91525 * 1023))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def describe(wave, freq, high, low, duty, phase):
|
|
91
|
+
from .awg_modes import level_code
|
|
92
|
+
fcw = int(math.floor(freq * 40))
|
|
93
|
+
if wave == 'noise':
|
|
94
|
+
return {'wave': wave, 'high_v': high, 'low_v': low, 'note': 'Gaussian-like noise around (low+high)/2 with sigma '
|
|
95
|
+
'about 2.8 % of (high-low), peaks about +-0.2 x (high-low), flat to ~2 MHz; frequency is ignored'}
|
|
96
|
+
return {'wave': wave, 'freq_hz': freq, 'freq_actual_hz': fcw / 40.0, 'high_v': round(level_code(high) / CODES_PER_V, 4),
|
|
97
|
+
'low_v': round(level_code(low) / CODES_PER_V, 4), 'duty_pct': duty, 'phase_deg': phase}
|