oep-client-python 0.0.4__py3-none-any.whl → 0.0.5__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.4"
5
+ __version__ = "0.0.5"
oep_client/core.py CHANGED
@@ -109,7 +109,47 @@ def plan_apply(hst: h.Host, assignments: list[tuple[int, int, int]]) -> None:
109
109
  all interfaces accept their roles or nothing changes. The plan is the
110
110
  session's resource: kept over an explicit end, released at a lease lapse or a force takeover (oep-core §9)."""
111
111
  tlv = b"".join(bytes([TAG_ROLE_ASSIGNMENT, 5]) + struct.pack("<HBH", fn, role, ch) for fn, role, ch in assignments)
112
- hst.call(m.CORE_FN, OP_PLAN_APPLY, tlv)
112
+ try:
113
+ hst.call(m.CORE_FN, OP_PLAN_APPLY, tlv)
114
+ except h.Rejected as e:
115
+ if e.result.detail != m.UNAVAILABLE:
116
+ raise
117
+ why = _pin_holders(hst, assignments)
118
+ if why:
119
+ raise PinsTaken(e.result, why) from None
120
+ raise
121
+
122
+
123
+ class PinsTaken(h.Rejected):
124
+ """plan_apply refused (unavailable) and the probe's settings say why: a pin kept by another fn's saved plan or by a
125
+ slot (oep.probe.config). `holders`: (channel, what holds it)."""
126
+
127
+ def __init__(self, result, holders: list[tuple[int, str]]):
128
+ super().__init__(result)
129
+ self.holders = holders
130
+
131
+ def __str__(self) -> str:
132
+ return "pins taken: " + "; ".join(f"channel {ch} by {who}" for ch, who in self.holders) + \
133
+ " (oep config show; oep config remove <probe> plan <fn> frees a saved plan)"
134
+
135
+
136
+ def _pin_holders(hst: h.Host, assignments: list[tuple[int, int, int]]) -> list[tuple[int, str]]:
137
+ """What the settings keep on the channels asked for (best effort: an empty list when there is no oep.probe.config
138
+ or it cannot be read)."""
139
+ from . import config
140
+ try:
141
+ items = config.ProbeConfig(hst).items()
142
+ except (LookupError, h.OepError):
143
+ return []
144
+ fns = {fn for fn, _, _ in assignments}
145
+ out = []
146
+ for fn, role, ch in assignments:
147
+ for it in items:
148
+ if isinstance(it, config.Plan) and it.channel == ch and it.fn not in fns:
149
+ out.append((ch, f"the saved plan of fn {it.fn} (role {it.role})"))
150
+ elif isinstance(it, config.Slot) and ch in it.pins:
151
+ out.append((ch, f"slot {it.slot} {it.name}"))
152
+ return out
113
153
 
114
154
 
115
155
  def plan_release(hst: h.Host, fns: list[int] | tuple[int, ...] = ()) -> None:
oep_client/endpoint.py CHANGED
@@ -923,7 +923,7 @@ class Endpoint:
923
923
  pos = s.end
924
924
  elif frm == 3:
925
925
  hits = [mk for mk in s.marks if arg == 0 or mk[2] == arg]
926
- pos = hits[-1][1] if hits else s.base
926
+ pos = hits[-1][1] if hits else s.end # no such mark: from now (common §1.2)
927
927
  else:
928
928
  raise Reject(m.UNSUPPORTED)
929
929
  flags = 0
oep_client/usb_stream.py CHANGED
@@ -10,6 +10,7 @@ transfer; if its timeout hits first, pyusb raises and the bytes already received
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import atexit
13
14
  import threading
14
15
  import time
15
16
 
@@ -114,15 +115,21 @@ class UsbAsyncStream:
114
115
  self.timeout = 0.05
115
116
  self._buffer = bytearray()
116
117
  self._cond = threading.Condition()
117
- self._closed = False
118
+ self._closed = False # no more resubmits (close() started)
119
+ self._stop = False # the event thread ends (every transfer is back)
118
120
  self._transfers = []
121
+ self._active = 0 # transfers submitted and not yet back for good
119
122
  for _ in range(self.DEPTH):
120
123
  t = handle.getTransfer()
121
124
  t.setBulk(endpoint_in, self.URB_SIZE, callback=self._done, timeout=0)
122
125
  t.submit()
123
126
  self._transfers.append(t)
127
+ self._active += 1
124
128
  self._events = threading.Thread(target=self._run, daemon=True)
125
129
  self._events.start()
130
+ # Closed at exit too: left to usb1's own finalizers, the context went while the event thread was inside
131
+ # handleEventsTimeout with transfers out - the process hung at exit or libusb aborted (usbi_mutex_destroy).
132
+ atexit.register(self.close)
126
133
 
127
134
  @classmethod
128
135
  def open(cls, vid: int, pid: int, serial: str | None = None) -> "UsbAsyncStream":
@@ -158,14 +165,23 @@ class UsbAsyncStream:
158
165
  self._buffer += data
159
166
  self._cond.notify_all()
160
167
  if not self._closed and status in (usb1.TRANSFER_COMPLETED, usb1.TRANSFER_TIMED_OUT):
161
- transfer.submit()
168
+ try:
169
+ transfer.submit()
170
+ return
171
+ except usb1.USBError:
172
+ pass
173
+ with self._cond:
174
+ self._active -= 1 # back for good (cancelled, failed, or closing)
175
+ self._cond.notify_all()
162
176
 
163
177
  def _run(self) -> None:
164
- while not self._closed:
178
+ while not self._stop:
165
179
  try:
166
180
  self.context.handleEventsTimeout(0.1)
167
181
  except self._usb1.USBErrorInterrupted:
168
182
  continue
183
+ except self._usb1.USBError:
184
+ break
169
185
 
170
186
  @property
171
187
  def in_waiting(self) -> int:
@@ -195,15 +211,27 @@ class UsbAsyncStream:
195
211
  self._buffer.clear()
196
212
 
197
213
  def close(self) -> None:
214
+ """Cancel the IN transfers and let the event thread take them back before the handle and the context go
215
+ (closing with transfers still out hung the process or aborted libusb). Safe to call twice (atexit)."""
216
+ if self._closed:
217
+ return
198
218
  self._closed = True
219
+ atexit.unregister(self.close)
199
220
  for t in self._transfers:
200
221
  try:
201
222
  t.cancel()
202
223
  except self._usb1.USBError:
203
224
  pass
204
- self._events.join(timeout=0.5)
225
+ deadline = time.monotonic() + 1.0
226
+ with self._cond:
227
+ while self._active > 0 and self._events.is_alive() and time.monotonic() < deadline:
228
+ self._cond.wait(0.05)
229
+ self._stop = True
230
+ self._events.join(timeout=1.0)
205
231
  try:
206
232
  self.handle.releaseInterface(self.interface)
233
+ except self._usb1.USBError:
234
+ pass
207
235
  finally:
208
236
  self.handle.close()
209
237
  self.context.close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe
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
@@ -43,7 +43,7 @@ uv run pytest # in a checkout
43
43
  `oep-client` is another project, so the distribution is named `oep-client-python`.
44
44
 
45
45
  Releases: run the GitHub Actions workflow Release (workflow_dispatch, version X.Y.Z or X.Y.ZbN). `tools/prepare_release.py`
46
- sets the version in pyproject.toml and `oep_client.__version__` and turns CHANGELOG.md's Unreleased into that version; after
46
+ sets the version in pyproject.toml, uv.lock and `oep_client.__version__` and turns CHANGELOG.md's Unreleased into that version; after
47
47
  the tests and the build it commits, tags, makes the GitHub Release and publishes to PyPI (Trusted Publishing). Record changes
48
48
  under Unreleased in CHANGELOG.md, (EN) and (JA).
49
49
 
@@ -1,4 +1,4 @@
1
- oep_client/__init__.py,sha256=F5if_qpFrTQuYzjKnNr2nZ1QRiCVH8b_c43b3GQbvmQ,163
1
+ oep_client/__init__.py,sha256=arvOlIBZyy_QgLJjeCKna1018c1f7GeNCDiwrNIscWc,163
2
2
  oep_client/__main__.py,sha256=Ta7D7jhtg5IhFWuI3KTikbDNYazh9QKCH3FtMMnDMjg,14143
3
3
  oep_client/arm.py,sha256=JLyUhLSPcmsk73schZcaJYoOKuHzrvnU8kPmfrEvVuo,12331
4
4
  oep_client/capture.py,sha256=RZgDnWqr8Aqy49U7PqQpnKIQEL18djVuDJFydxrmv-c,17996
@@ -7,10 +7,10 @@ oep_client/ch32_flash.py,sha256=Fc-6jEj15cQtMSKmfC5dmDetMXJ8hAdAuJ9d9odwrsA,1265
7
7
  oep_client/cobs.py,sha256=hqddQI_X7rsBvBvcP_MBE3bXPyB13-Azjqp_9JfdQ1g,2288
8
8
  oep_client/config.py,sha256=JhEmQKU-4KjSAoIovgeEwueKEiMLPUIiGVhZcR5ZlVc,9271
9
9
  oep_client/console.py,sha256=YYcuc9Qxe3kihla8wynYbDaLVr7wBLEvswrM-cMK3Tk,6859
10
- oep_client/core.py,sha256=z0-mplQVLOmPyXj1lHc-U7HYl3Gd6o-b5YJhpQTvqKU,8789
10
+ oep_client/core.py,sha256=Cyyaknjipkg6tS76dZe5GMqjmOoQjlfeP0WFtMHkJNs,10403
11
11
  oep_client/decode.py,sha256=M-bRARrA2qGZLoJn0BdXeTb4WjL_2Hb2nk_g1IGXIw4,2382
12
12
  oep_client/dump.py,sha256=GAnmu2abuDmGmbWcbbEfC3EUuPp5Hady9vQVNcQ-724,6743
13
- oep_client/endpoint.py,sha256=70eFSDQuu-UffiuooF74e9kS33dhE4DyJeC7yllRR7Y,72248
13
+ oep_client/endpoint.py,sha256=K6FWTojqKqKIQEPpuOQLmfbaDWLvspXed-ipzFNuKHY,72303
14
14
  oep_client/esp32_targets.py,sha256=b8tNH-nb2KC-XgNfWg5l3TO0cyhZOF-gJajaVxNVSGQ,4988
15
15
  oep_client/fake.py,sha256=UzpB-A42Ma2QqJj0l-JmwCJkMn6DPVHHN77fcemT59U,13104
16
16
  oep_client/fake_serial.py,sha256=uKl0UNcifhdeykQtm8gnRRhqV7TT4vPwBFuBhbf-9BI,4504
@@ -28,9 +28,9 @@ oep_client/riscv.py,sha256=E0_C-4mBsrp9FUoZpMiqSH9e8sC8Q4EO5HRjFKE6_pc,21392
28
28
  oep_client/rp2350.py,sha256=7OMyj270k3AY1e3-ljicPwOjf9jOlokbT6QNkA0b0yY,4227
29
29
  oep_client/target.py,sha256=R8BvYfFfYNhZF0MysJkNb8b69YgBlmeGXNX8ReZIe5k,1068
30
30
  oep_client/uiapduino.py,sha256=IEPG3LbZPksi8g3wmAf6T0CRWSfYKW1_qrH_GBVQdxY,6090
31
- oep_client/usb_stream.py,sha256=1nvuRNLQkpp8054eG-JutBKh9y0F_8PHGDAfG0ZSbaY,8819
32
- oep_client_python-0.0.4.dist-info/METADATA,sha256=7HtJZthSGhDIG5Ce8dvLIvdwtQpN5AYve_5YhSk8l5M,7359
33
- oep_client_python-0.0.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
34
- oep_client_python-0.0.4.dist-info/entry_points.txt,sha256=h6_8QX89Ww1ogALEGG5g4kspT3IOV6O51lwKqf5BWco,49
35
- oep_client_python-0.0.4.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
36
- oep_client_python-0.0.4.dist-info/RECORD,,
31
+ oep_client/usb_stream.py,sha256=VJMCxsOlQCk005FcS92pqTh33fRSFBizoL__L0VbUMA,10242
32
+ oep_client_python-0.0.5.dist-info/METADATA,sha256=42SpXvaLTywLE9L3yPq1mRj4UNTamkB7Vb0dbZegO24,7368
33
+ oep_client_python-0.0.5.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
34
+ oep_client_python-0.0.5.dist-info/entry_points.txt,sha256=h6_8QX89Ww1ogALEGG5g4kspT3IOV6O51lwKqf5BWco,49
35
+ oep_client_python-0.0.5.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
36
+ oep_client_python-0.0.5.dist-info/RECORD,,