oep-client-python 0.0.48__py3-none-any.whl → 0.0.49__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.
oep_client/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.0.48"
5
+ __version__ = "0.0.49"
oep_client/acquire.py CHANGED
@@ -1660,9 +1660,9 @@ class _Spool:
1660
1660
  self._open(p, x)
1661
1661
  data = bytes(self.pending[p - self.at:nxt - self.at])
1662
1662
  cols = _segment_columns(self.cap, data, samples, len(self.names), self.cfg)
1663
- for j, (col, f, ev) in enumerate(zip(cols, self.cur["files"], self.cur["events"])):
1663
+ for c, (col, f, ev) in enumerate(zip(cols, self.cur["files"], self.cur["events"])):
1664
1664
  if isinstance(col, bytes): # joined bit by bit: a segment need not end on a whole byte
1665
- self.cur["carry"][j] = _write_bits(f, self.cur["carry"][j], col, samples)
1665
+ self.cur["carry"][c] = _write_bits(f, self.cur["carry"][c], col, samples)
1666
1666
  else:
1667
1667
  ev[0].append(col[0] + self.cur["samples"] // col[4])
1668
1668
  ev[1].append(col[1])
@@ -82,11 +82,11 @@ BITS = 12 # the analog value's b (layout)
82
82
  BLOCK_TOP = 4096 # its own: the longest block L it makes (§1.3)
83
83
  STREAM_BUFFER_BYTES = 1 << 20 # its own: what a stream holds unsent, said in the answer (stream_buffer, §3.3)
84
84
  UNKNOWN_END = 1 << 62 # streaming format 1: a segment dropped whole whose end is not taken yet
85
- BLOCKS_A_TICK = 1 << 14 # streaming: the most samples one tick makes - Python makes far below a fast rate,
86
- # and catching up on all the time passed fell further behind every tick until the
87
- # bench answered nothing (wireskein: format 1 at 2 MHz x 4 lines; format 0 at 1 MHz
88
- # x 2 lines, stop answered after 12.8 s): it makes what it can and the stream runs
89
- # late instead
85
+ FAST_A_TICK = 1 << 17 # streaming: the most one tick makes when numpy makes them at once (a_tick)
86
+ BLOCKS_A_TICK = 1 << 14 # streaming: the most samples one tick makes sample by sample - catching up on all
87
+ # the time passed at once fell further behind every tick until the bench answered
88
+ # nothing (wireskein: format 1 at 2 MHz x 4 lines; format 0 at 1 MHz x 2 lines,
89
+ # stop answered after 12.8 s): it catches up over ticks (held_most)
90
90
 
91
91
 
92
92
  TIMEBASE_HZ = 100_000_000 # its own: format 3's delta tick (the answer's timebase, §3.8)
@@ -206,6 +206,38 @@ def logic_block(raw: list[list[int]], events: list[list[int]], ds: list[int], be
206
206
  return fallback + start + struct.pack("<I", len(ev)) + body + b"".join(ev)
207
207
 
208
208
 
209
+ def logic_block_np(np, raw: list, events: list, ds: list[int], before: list[int], L: int) -> bytes:
210
+ """logic_block with each channel's levels a numpy array: the same bytes, made at once."""
211
+ r = len((raw + events)[0])
212
+ E = len(events)
213
+ fb, ev = [], []
214
+ for e, lv in enumerate(events):
215
+ d = ds[e]
216
+ q = -(-r // d)
217
+ prev = np.concatenate([np.array([before[e]], np.uint8), lv[:-1]])
218
+ changed = np.zeros(q * d, np.int64)
219
+ changed[:r] = lv != prev
220
+ counts = changed.reshape(q, d).sum(axis=1)
221
+ last = lv[np.minimum((np.arange(q) + 1) * d, r) - 1].astype(np.int64)
222
+ t = np.nonzero(counts)[0]
223
+ rec = np.zeros(len(t), dtype=[("t", "<u2"), ("e", "u1"), ("v", "u1")])
224
+ rec["t"], rec["e"], rec["v"] = t, e, (last[t] << 7) | np.minimum(counts[t], 127)
225
+ if len(t) * 4 > L // 8:
226
+ fb.append(e)
227
+ else:
228
+ ev.append(rec.tobytes())
229
+ nb = (E + 7) // 8
230
+ fallback = sum(1 << e for e in fb).to_bytes(nb, "little")
231
+ start = sum(before[e] << e for e in range(E)).to_bytes(nb, "little")
232
+ cols = raw + [events[e] for e in fb]
233
+ body = b""
234
+ if cols:
235
+ bits = np.stack(cols, axis=1).astype(np.uint8)
236
+ body = np.packbits(bits.reshape(-1), bitorder="little").tobytes()
237
+ n_ev = sum(len(x) // 4 for x in ev)
238
+ return fallback + start + struct.pack("<I", n_ev) + body + b"".join(ev)
239
+
240
+
209
241
  def analog_part(values: list[int], kind: int, d: int, b: int) -> bytes:
210
242
  """One channel's part of a format 2 block (§1.4): packed the r values of b bits; else for every interval of d
211
243
  (the last one partial) min and max (b bits each) and / or the sum (b + ceil(log2 d) bits), in that order - bits by
@@ -230,6 +262,15 @@ def analog_part(values: list[int], kind: int, d: int, b: int) -> bytes:
230
262
  return acc.to_bytes((at + 7) // 8, "little")
231
263
 
232
264
 
265
+ class _wave:
266
+ """A logic wave: wave(i, rate) for one sample, wave.many(np, samples, rate) for an array of them (numpy)."""
267
+ def __init__(self, one, many):
268
+ self.one, self.many = one, many
269
+
270
+ def __call__(self, i, rate):
271
+ return self.one(i, rate)
272
+
273
+
233
274
  def logic_wave(spec: str):
234
275
  """A logic channel's waveform instead of the counter's bit: `counter`; `quiet[:LEVEL]` (never changes, default
235
276
  high); `square:PERIOD` (samples); `uart:BAUD[:TEXT]` - 8N1 characters of TEXT (default "OEP\\n") one after another
@@ -239,10 +280,10 @@ def logic_wave(spec: str):
239
280
  return None
240
281
  if kind == "quiet":
241
282
  level = int(args[0]) if args else 1
242
- return lambda i, rate: level
283
+ return _wave(lambda i, rate: level, lambda np, i, rate: np.full(len(i), level, np.uint8))
243
284
  if kind == "square":
244
285
  half = max(1, int(args[0]) // 2)
245
- return lambda i, rate: (i // half) & 1
286
+ return _wave(lambda i, rate: (i // half) & 1, lambda np, i, rate: ((i // half) & 1).astype(np.uint8))
246
287
  if kind == "uart":
247
288
  baud = int(args[0])
248
289
  text = (args[1] if len(args) > 1 else "OEP\n").encode()
@@ -255,7 +296,14 @@ def logic_wave(spec: str):
255
296
  if at <= 8:
256
297
  return text[char % len(text)] >> (at - 1) & 1 # LSB first
257
298
  return 1 # stop, idle
258
- return uart
299
+
300
+ def many(np, i, rate): # the same, for an array of samples
301
+ r = Fraction(rate) # the track's rate is exact: so is the bit time
302
+ bit = (i * (baud * r.denominator)) // r.numerator
303
+ char, at = np.divmod(bit, 20)
304
+ data = (np.frombuffer(text, np.uint8)[char % len(text)] >> np.clip(at - 1, 0, 7)) & 1
305
+ return np.where(at == 0, 0, np.where(at <= 8, data, 1)).astype(np.uint8)
306
+ return _wave(uart, many)
259
307
  raise ValueError(f"logic wave {spec!r}: counter, quiet[:LEVEL], square:PERIOD or uart:BAUD[:TEXT]")
260
308
 
261
309
 
@@ -702,8 +750,39 @@ class VirtualCapture:
702
750
  slot = "H" if self.width == 2 * self.channels else "I"
703
751
  return b"".join(struct.pack(f"<{self.channels}{slot}", *(self.value(k, first + i) for k in self.analog_roles))
704
752
  for i in range(n))
753
+ levels = self._levels(first, n)
754
+ if levels is not None: # numpy: the samples at once
755
+ import numpy as np
756
+ bits = np.zeros((n, self.width), np.uint8)
757
+ bits[:, :] = np.stack(levels[:self.width], axis=1)
758
+ return np.packbits(bits.reshape(-1), bitorder="little").tobytes()
705
759
  return pack_samples([sum(self.value(k, first + i) << k for k in range(self.width)) for i in range(n)], self.width)
706
760
 
761
+ def _levels(self, first: int, n: int):
762
+ """Logic channels 0 .. max(width, channels) - 1 at samples first .. first + n - 1 as numpy arrays, as value() gives them; None
763
+ where only value() can (numpy missing, the glitch filter, the external clock, a wave without its array form, a
764
+ value() a test put on the track).
765
+ Python made a sample a microsecond or more: a bench behind 1 MS/s on a busy host."""
766
+ ns = max(self.glitch_ns.values(), default=0)
767
+ if self.analog or self.clock is not None or n <= 0 or (ns and -(-ns * self.rate // 1_000_000_000) > 1) or \
768
+ "value" in vars(self): # a test's own value(): only it says
769
+ return None
770
+ try:
771
+ import numpy as np
772
+ except ImportError:
773
+ return None
774
+ i = np.arange(first, first + n, dtype=np.int64)
775
+ out = []
776
+ for k in range(max(self.width, self.channels)): # format 1: the channels, its w apart (0 or less)
777
+ wave = self.waves.get(self.logic_roles[k]) if k < len(self.logic_roles) else None
778
+ if wave is None:
779
+ out.append(((i >> k) & 1).astype(np.uint8))
780
+ elif isinstance(wave, _wave):
781
+ out.append(wave.many(np, i, self.rate))
782
+ else:
783
+ return None
784
+ return out
785
+
707
786
  def _blocks(self, first: int, n: int, contiguous: bool, segment_start: bool = True) -> bytes:
708
787
  """A format 1 segment (§1.3): blocks of L from its sample 0; start_level the sample before when it is
709
788
  contiguous in the generation, else the block's first. Format 2: _analog_blocks; format 3: _timed."""
@@ -715,10 +794,18 @@ class VirtualCapture:
715
794
  raw = [k for k in roles if self.carriages.get(k, (CARRIAGE["raw"], 1))[0] == CARRIAGE["raw"]]
716
795
  ev = [k for k in roles if k not in raw]
717
796
  ds = [self.carriages[k][1] for k in ev]
797
+ levels = self._levels(first, n) # numpy: the samples and the blocks at once
718
798
  out = bytearray()
719
799
  for b in range(0, n, self.block):
720
800
  r = min(self.block, n - b)
721
801
  at = first + b
802
+ if levels is not None:
803
+ before = [int(levels[k][b - 1]) if b else (self.value(k, at - 1) if contiguous else int(levels[k][0]))
804
+ for k in ev]
805
+ import numpy as np
806
+ out += logic_block_np(np, [levels[k][b:b + r] for k in raw], [levels[k][b:b + r] for k in ev], ds,
807
+ before, self.block)
808
+ continue
722
809
  before = [self.value(k, at - 1) if (b or contiguous) else self.value(k, at) for k in ev]
723
810
  out += logic_block([[self.value(k, at + i) for i in range(r)] for k in raw],
724
811
  [[self.value(k, at + i) for i in range(r)] for k in ev], ds, before, self.block)
@@ -1018,22 +1105,43 @@ class VirtualCapture:
1018
1105
  if len(self.segs) >= self.segments_max:
1019
1106
  self.state = STATE["paused"]
1020
1107
  break
1108
+ elif self.mode == MODE["streaming"] and due - self.produced > self.held_most():
1109
+ events += self.overflow() # behind by more than its stream holds (below)
1021
1110
  elif self.mode == MODE["streaming"] and self.format in PIECES:
1022
1111
  self._stream_blocks(due, whole=True)
1023
1112
  elif self.mode == MODE["streaming"]:
1024
- per_byte = 1 if self.analog else 8 // math.gcd(self.width, 8) # whole bytes only (§2.2)
1025
- n = min((due - self.produced) // per_byte * per_byte, BLOCKS_A_TICK)
1026
- if n > 0:
1027
- self.data += self._pack(self.produced, n)
1028
- self.produced += n
1029
- taken = self.produced - self.capture_begin
1030
- first = self.first_segment_samples or self.samples
1031
- complete = 0 if taken < first else 1 + (taken - first) // self.samples
1032
- self.serial_done = complete & 0xFFFFFFFF
1033
- done_samples = 0 if not complete else first + (complete - 1) * self.samples
1034
- self.stream_committed_bytes = self.bytes_of(done_samples)
1113
+ self._stream_samples(due)
1035
1114
  return events
1036
1115
 
1116
+ # Streaming follows the clock as a probe's does: sample i is the one taken at t0 + i / rate, and the samples the
1117
+ # clock has taken that the bench has not made yet are as a probe's samples held unsent in its stream buffer. A tick
1118
+ # makes a bounded number (the bench keeps answering), so it may be behind for a while and catch up; stop makes every
1119
+ # sample up to the stop (a probe sends all it holds); behind by more than the stream buffer holds, the track stops
1120
+ # in error as a probe whose buffer overflowed (state 6, error storage) - never a stream that silently ran late
1121
+ # (a section placed by the probe's clock past the stream's end, wireskein on a busy host).
1122
+ def held_most(self) -> int:
1123
+ """The samples its stream buffer holds (stream_buffer, §3.3), at format 0's bytes a sample."""
1124
+ return STREAM_BUFFER_BYTES // max(1, self.width) if self.analog else STREAM_BUFFER_BYTES * 8 // max(1, self.width)
1125
+
1126
+ def a_tick(self) -> int:
1127
+ """The most samples one tick makes: more when numpy makes them at once (a sample a microsecond or more in
1128
+ Python, about a hundredth of that at once)."""
1129
+ return FAST_A_TICK if self._levels(self.produced, 1) is not None else BLOCKS_A_TICK
1130
+
1131
+ def _stream_samples(self, due: int, most: int | None = None) -> None:
1132
+ """Format 0 streaming: the samples up to `due`, at most `most` (a_tick() by default), whole bytes only."""
1133
+ per_byte = 1 if self.analog else 8 // math.gcd(self.width, 8) # whole bytes only (§2.2)
1134
+ n = min((due - self.produced) // per_byte * per_byte, self.a_tick() if most is None else most)
1135
+ if n > 0:
1136
+ self.data += self._pack(self.produced, n)
1137
+ self.produced += n
1138
+ taken = self.produced - self.capture_begin
1139
+ first = self.first_segment_samples or self.samples
1140
+ complete = 0 if taken < first else 1 + (taken - first) // self.samples
1141
+ self.serial_done = complete & 0xFFFFFFFF
1142
+ done_samples = 0 if not complete else first + (complete - 1) * self.samples
1143
+ self.stream_committed_bytes = self.bytes_of(done_samples)
1144
+
1037
1145
  def _stream_geometry(self, rel: int) -> tuple[int, int, int]:
1038
1146
  """Streaming: the segment holding sample `rel` (from the retained start) -> (its number, first sample, size)."""
1039
1147
  first = self.first_segment_samples or self.samples
@@ -1046,7 +1154,7 @@ class VirtualCapture:
1046
1154
  """Format 1 streaming: the blocks the clock has taken - each whole (a block counts its events first, §1.3), at
1047
1155
  most BLOCKS_A_TICK samples a call; `whole` False at stop: the block being taken ends there, short."""
1048
1156
  if whole:
1049
- due = min(due, self.produced + BLOCKS_A_TICK)
1157
+ due = min(due, self.produced + self.a_tick())
1050
1158
  else: # stop: only the block begun, not all the time behind
1051
1159
  rel = self.produced - self.capture_begin
1052
1160
  _, at, size = self._stream_geometry(rel)
@@ -1104,6 +1212,18 @@ class VirtualCapture:
1104
1212
  index = self.first_trigger_index if self.first_trigger_index < partial else NONE
1105
1213
  segment = self._segment(partial, index, flags=FLAG["short"])
1106
1214
  events.append(bytes([EVENT["segment"]]) + segment.pack())
1215
+ if self.state == STATE["capturing"] and self.mode == MODE["streaming"] and now_ms is not None:
1216
+ due = self.kept(int((now_ms - self.started_ms) * self.rate / 1000)) # every sample up to the stop: the
1217
+ if due - self.produced > self.held_most(): # stream holds them (tick's rules, above)
1218
+ return events + self.overflow()
1219
+ while self.state == STATE["capturing"]:
1220
+ before = self.produced
1221
+ if self.format in PIECES:
1222
+ self._stream_blocks(due, whole=True)
1223
+ else:
1224
+ self._stream_samples(due, most=due)
1225
+ if self.produced == before:
1226
+ break
1107
1227
  if self.state == STATE["capturing"] and self.mode == MODE["streaming"] and self.format in PIECES:
1108
1228
  if now_ms is not None: # the taken samples, a short last block
1109
1229
  self._stream_blocks(self.kept(int((now_ms - self.started_ms) * self.rate / 1000)), whole=False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.48
3
+ Version: 0.0.49
4
4
  Summary: Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a virtual bench
5
5
  Project-URL: Homepage, https://github.com/Open-Embedded-Probe/oep-client-python
6
6
  Project-URL: Repository, https://github.com/Open-Embedded-Probe/oep-client-python
@@ -1,6 +1,6 @@
1
- oep_client/__init__.py,sha256=6oSvhMIWGGzIF7vHRn0zjXVkFRa6spDeH9YGkw-8pZU,164
1
+ oep_client/__init__.py,sha256=HsHjSnQVjNKgEHyTGCVruPWP4HisGn-GuiP1r3bk70c,164
2
2
  oep_client/__main__.py,sha256=kbXBtEmFdhRXbVqXEbWCO-_5aDa-KyqMcW2HLZb5fjc,31510
3
- oep_client/acquire.py,sha256=_-cH4JxePvym2REq7ElaBcNSuREt8DbsDwAkYM0xSw0,104034
3
+ oep_client/acquire.py,sha256=9_rNRUB6jLPMu2Ga9QWSh9bLYR99taLpZL9-qBHdt8M,104034
4
4
  oep_client/arm.py,sha256=SszSLae8zErD4T-a1uQ6s6o3Lz7pe2SR8d3Qy-wi5rY,15633
5
5
  oep_client/capture.py,sha256=fvLqs4Ltot4pyOdQiCWIzf_v1j_iduCxU4rrX6oWfg8,76245
6
6
  oep_client/capture_blocks.py,sha256=dPrrZ8K-ISF80w6UumKVst5XNt0IjUk9lxbVvFnUEqg,20836
@@ -38,7 +38,7 @@ oep_client/targets.py,sha256=ixUxbZUfYqOJjZzcaOudwV2wweVwdk0G85dfRiZzMeA,7183
38
38
  oep_client/uiapduino.py,sha256=oafP1SyKPO3cox-2RYbWUZusy4vc6NsEVC7Z-Y1sjWI,6337
39
39
  oep_client/usb_stream.py,sha256=zRGGHkvtOme6lIZ-gkrCe2xKn9hC62JGqmc7pKQDAJI,23540
40
40
  oep_client/virtual_bench.py,sha256=gc2i8UZM5P_2sg68wcDx5B4pRkYNUZ2wjzIQBpKOOuI,50262
41
- oep_client/virtual_bench_capture.py,sha256=QIazFIKYSw8KW4Mc4xjeluXBBtnk5Pry_ag7_qesAko,94933
41
+ oep_client/virtual_bench_capture.py,sha256=84MsaS2wXq2N6wcFtsFHdBaGR9bjWPXathZpcJ341Mo,101920
42
42
  oep_client/virtual_bench_mdns.py,sha256=cnJNBKg5zTduXUcH-wqN_ZBDGl7DyL-tL3CBn2jro0E,13305
43
43
  oep_client/virtual_bench_serial.py,sha256=EvbdOYeGrM6CRti2b90I0PzpmmherfoJRaAg4v7Iq9Y,13205
44
44
  oep_client/virtual_bench_serve.py,sha256=LPjvXcfVpixOyhGM1-oZpDqjJfD53eKOrhpAdoXV1ck,39015
@@ -255,8 +255,8 @@ oep_client/conformance/wire_options_sample.py,sha256=dog-yvUcu693M8b51tA5TnhAnKr
255
255
  oep_client/conformance/writer_lease.py,sha256=5wDd4XIJWFPFlRY91wHlJs5U7ANp3xPpGgU2Eki7e8g,7331
256
256
  oep_client/conformance/writer_lease_model.py,sha256=FxhDqqzdFOa_SSHSvjdQFh49Wn3YuZ98ISSlRRxrq9E,1721
257
257
  oep_client/conformance/writer_lease_sample.py,sha256=oowRwYUNAE4f6rEE2HmYlSS9dDWE0TGegkcsqVH8Nak,455
258
- oep_client_python-0.0.48.dist-info/METADATA,sha256=dmN1zicxaqz2IlW815qXtU6KXYYqPebwgAs00ikR0v0,55269
259
- oep_client_python-0.0.48.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
260
- oep_client_python-0.0.48.dist-info/entry_points.txt,sha256=XEt39Jz7oQyU9Hfg_tV6eDGofNxWYrjSfslxKP-fh70,135
261
- oep_client_python-0.0.48.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
262
- oep_client_python-0.0.48.dist-info/RECORD,,
258
+ oep_client_python-0.0.49.dist-info/METADATA,sha256=UYYYs1l9AeiJXE7eZqgk2XKm04VWVD1PLmjh5LlcYXQ,55269
259
+ oep_client_python-0.0.49.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
260
+ oep_client_python-0.0.49.dist-info/entry_points.txt,sha256=XEt39Jz7oQyU9Hfg_tV6eDGofNxWYrjSfslxKP-fh70,135
261
+ oep_client_python-0.0.49.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
262
+ oep_client_python-0.0.49.dist-info/RECORD,,