oep-client-python 0.0.1__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.
@@ -0,0 +1,209 @@
1
+ """A USB vendor bulk endpoint pair shaped like the part of a pyserial port the v1 link uses (read with a timeout,
2
+ in_waiting, write, reset_input_buffer), so length-prefixed frames run over it unchanged. pyusb is imported only here.
3
+
4
+ IN is drained on a thread: a bulk write blocks until the device takes the data, and the device stops taking data
5
+ once its answers fill its FIFO and nobody reads them (E160).
6
+
7
+ Each IN read asks for READ_SIZE bytes. A read completes when that much has arrived or a short packet ends the
8
+ transfer; if its timeout hits first, pyusb raises and the bytes already received are lost. Under a continuous stream
9
+ (12.5 MB/s, 2026-09-25) a 64 KiB read waited for more than its 50 ms and dropped whole answers, so reads stay small."""
10
+
11
+ from __future__ import annotations
12
+
13
+ import threading
14
+ import time
15
+
16
+
17
+ class UsbBulkStream:
18
+ READ_SIZE = 16384
19
+
20
+ def __init__(self, device, endpoint_in, endpoint_out, interface: int):
21
+ self.device, self.ep_in, self.ep_out, self.interface = device, endpoint_in, endpoint_out, interface
22
+ self.timeout = 0.05
23
+ self._buffer = bytearray()
24
+ self._cond = threading.Condition()
25
+ self._closed = False
26
+ self._reader = threading.Thread(target=self._drain, daemon=True)
27
+ self._reader.start()
28
+
29
+ @classmethod
30
+ def open(cls, vid: int, pid: int, serial: str | None = None) -> "UsbBulkStream":
31
+ import usb.core
32
+ import usb.util
33
+ match = (lambda d: usb.util.get_string(d, d.iSerialNumber).lower() == serial.lower()) if serial else None
34
+ dev = usb.core.find(idVendor=vid, idProduct=pid, custom_match=match)
35
+ if dev is None:
36
+ raise FileNotFoundError(f"no USB device {vid:04x}:{pid:04x}" + (f" serial {serial}" if serial else ""))
37
+ cfg = dev.get_active_configuration()
38
+ for intf in cfg:
39
+ eps = [e for e in intf if usb.util.endpoint_type(e.bmAttributes) == usb.util.ENDPOINT_TYPE_BULK]
40
+ ins = [e for e in eps if usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN]
41
+ outs = [e for e in eps if usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT]
42
+ if ins and outs:
43
+ usb.util.claim_interface(dev, intf.bInterfaceNumber)
44
+ return cls(dev, ins[0], outs[0], intf.bInterfaceNumber)
45
+ raise FileNotFoundError("no bulk IN/OUT pair on the device")
46
+
47
+ def _drain(self) -> None:
48
+ import usb.core
49
+ while not self._closed:
50
+ try:
51
+ data = self.ep_in.read(self.READ_SIZE, timeout=200)
52
+ except usb.core.USBTimeoutError:
53
+ continue
54
+ except usb.core.USBError:
55
+ if self._closed:
56
+ return
57
+ time.sleep(0.01)
58
+ continue
59
+ if len(data):
60
+ with self._cond:
61
+ self._buffer += bytes(data)
62
+ self._cond.notify_all()
63
+
64
+ @property
65
+ def in_waiting(self) -> int:
66
+ with self._cond:
67
+ return len(self._buffer)
68
+
69
+ def read(self, n: int = 1) -> bytes:
70
+ deadline = time.monotonic() + (self.timeout or 0)
71
+ with self._cond:
72
+ while not self._buffer:
73
+ left = deadline - time.monotonic()
74
+ if left <= 0:
75
+ return b""
76
+ self._cond.wait(left)
77
+ out = bytes(self._buffer[:n])
78
+ del self._buffer[:n]
79
+ return out
80
+
81
+ def write(self, data: bytes) -> int:
82
+ n = self.ep_out.write(data, timeout=2000)
83
+ if data and len(data) % self.ep_out.wMaxPacketSize == 0: # a zero-length packet ends the probe's transfer
84
+ self.ep_out.write(b"", timeout=2000)
85
+ return n
86
+
87
+ def reset_input_buffer(self) -> None:
88
+ with self._cond:
89
+ self._buffer.clear()
90
+
91
+ def close(self) -> None:
92
+ import usb.util
93
+ self._closed = True
94
+ self._reader.join(timeout=0.5)
95
+ usb.util.release_interface(self.device, self.interface)
96
+ usb.util.dispose_resources(self.device)
97
+
98
+
99
+ class UsbAsyncStream:
100
+ """The same shape as UsbBulkStream on python-libusb1 (`usb1`): IN runs as DEPTH asynchronous transfers of URB_SIZE
101
+ kept queued by an event thread, so a continuous stream near the HS ceiling (wch-protocols E116: 1 MiB x 8 reaches
102
+ ~47 MB/s over usbipd; 64 KiB x 32 carried 2 x 160 Msps here) is taken without gaps. A transfer completes when full or on a short packet; the probe ends a
103
+ transfer whose length is a whole number of packets with a zero-length packet, so small answers never wait."""
104
+
105
+ # 64 KiB x 32: a probe streaming whole-packet frames (16 KiB) completes a read every four frames, so the latency at
106
+ # a low rate stays short (1 MiB reads waited 0.8 s at 1.25 MB/s), and 2 MiB stay queued for the host's hiccups
107
+ URB_SIZE = 64 * 1024
108
+ DEPTH = 32
109
+
110
+ def __init__(self, context, handle, endpoint_in: int, endpoint_out: int, interface: int):
111
+ import usb1
112
+ self._usb1 = usb1
113
+ self.context, self.handle, self.ep_in, self.ep_out, self.interface = context, handle, endpoint_in, endpoint_out, interface
114
+ self.timeout = 0.05
115
+ self._buffer = bytearray()
116
+ self._cond = threading.Condition()
117
+ self._closed = False
118
+ self._transfers = []
119
+ for _ in range(self.DEPTH):
120
+ t = handle.getTransfer()
121
+ t.setBulk(endpoint_in, self.URB_SIZE, callback=self._done, timeout=0)
122
+ t.submit()
123
+ self._transfers.append(t)
124
+ self._events = threading.Thread(target=self._run, daemon=True)
125
+ self._events.start()
126
+
127
+ @classmethod
128
+ def open(cls, vid: int, pid: int, serial: str | None = None) -> "UsbAsyncStream":
129
+ import usb1
130
+ context = usb1.USBContext()
131
+ context.open()
132
+ for dev in context.getDeviceIterator(skip_on_error=True):
133
+ if dev.getVendorID() != vid or dev.getProductID() != pid:
134
+ continue
135
+ handle = dev.open()
136
+ if serial and (handle.getSerialNumber() or "").lower() != serial.lower():
137
+ handle.close()
138
+ continue
139
+ for setting in dev.iterSettings():
140
+ eps = [(e.getAddress(), e.getAttributes()) for e in setting]
141
+ ins = [a for a, attr in eps if attr & 3 == 2 and a & 0x80]
142
+ outs = [a for a, attr in eps if attr & 3 == 2 and not a & 0x80]
143
+ if ins and outs:
144
+ handle.claimInterface(setting.getNumber())
145
+ return cls(context, handle, ins[0], outs[0], setting.getNumber())
146
+ handle.close()
147
+ context.close()
148
+ raise FileNotFoundError(f"no USB device {vid:04x}:{pid:04x}" + (f" serial {serial}" if serial else ""))
149
+
150
+ def _done(self, transfer) -> None:
151
+ usb1 = self._usb1
152
+ status = transfer.getStatus()
153
+ if status == usb1.TRANSFER_COMPLETED:
154
+ n = transfer.getActualLength()
155
+ if n:
156
+ data = transfer.getBuffer()[:n]
157
+ with self._cond:
158
+ self._buffer += data
159
+ self._cond.notify_all()
160
+ if not self._closed and status in (usb1.TRANSFER_COMPLETED, usb1.TRANSFER_TIMED_OUT):
161
+ transfer.submit()
162
+
163
+ def _run(self) -> None:
164
+ while not self._closed:
165
+ try:
166
+ self.context.handleEventsTimeout(0.1)
167
+ except self._usb1.USBErrorInterrupted:
168
+ continue
169
+
170
+ @property
171
+ def in_waiting(self) -> int:
172
+ with self._cond:
173
+ return len(self._buffer)
174
+
175
+ def read(self, n: int = 1) -> bytes:
176
+ deadline = time.monotonic() + (self.timeout or 0)
177
+ with self._cond:
178
+ while not self._buffer:
179
+ left = deadline - time.monotonic()
180
+ if left <= 0:
181
+ return b""
182
+ self._cond.wait(left)
183
+ out = bytes(self._buffer[:n])
184
+ del self._buffer[:n]
185
+ return out
186
+
187
+ def write(self, data: bytes) -> int:
188
+ n = self.handle.bulkWrite(self.ep_out, data, timeout=2000)
189
+ if data and len(data) % 512 == 0: # end the probe's receive transfer (a probe reading large transfers needs it)
190
+ self.handle.bulkWrite(self.ep_out, b"", timeout=2000)
191
+ return n
192
+
193
+ def reset_input_buffer(self) -> None:
194
+ with self._cond:
195
+ self._buffer.clear()
196
+
197
+ def close(self) -> None:
198
+ self._closed = True
199
+ for t in self._transfers:
200
+ try:
201
+ t.cancel()
202
+ except self._usb1.USBError:
203
+ pass
204
+ self._events.join(timeout=0.5)
205
+ try:
206
+ self.handle.releaseInterface(self.interface)
207
+ finally:
208
+ self.handle.close()
209
+ self.context.close()
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.5
2
+ Name: oep-client-python
3
+ Version: 0.0.1
4
+ Summary: Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe
5
+ Project-URL: Homepage, https://github.com/Open-Embedded-Probe/oep-client-python
6
+ Project-URL: Repository, https://github.com/Open-Embedded-Probe/oep-client-python
7
+ Author: TANAKA Masayuki
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Embedded Systems
13
+ Classifier: Topic :: Software Development :: Testing
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: pyserial>=3.5
16
+ Requires-Dist: pyusb>=1.3
17
+ Provides-Extra: hid
18
+ Requires-Dist: hidapi>=0.14; extra == 'hid'
19
+ Provides-Extra: usb-async
20
+ Requires-Dist: libusb1>=3; extra == 'usb-async'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # OEP Python client
24
+
25
+ Open Embedded Probe の host 側。v1(oep-spec の `docs/oep-core.ja.md` と `docs/oep-if-*.ja.md`、固める候補の形)を話す。番号は oep-spec の
26
+ `generated/oep-v1/oep_v1_registry.py` をそのまま写した `oep_client.v1.registry` から取る。破壊的変更を前提とする
27
+ 実験段階で、互換 API は約束しない。OEP を初めて読む人は oep-spec の `docs/review-guide.ja.md`(どこに何が書いてあるか)から。
28
+
29
+ target の知識は host にある、という OEP の分担に従う。probe は線と DMI / DP・AP の転送しか知らず、CH32 の flash
30
+ コントローラ、RAM ローダー、RP2350 の boot ROM、Cortex-M の debug レジスタなどはここに置く。
31
+
32
+ ```sh
33
+ pip install oep-client-python # PyPI (import oep_client); a checkout: pip install -e <checkout>
34
+ uv run pytest # in a checkout
35
+ ```
36
+
37
+ `import oep_client.v1` だけで使える(`sys.path` に `src/` を足す使い方は不要になった)。番号の表は `tools/sync_registry.sh` で
38
+ oep-spec から写す。PyPI の `oep-client` は別のプロジェクトなので、配布名は `oep-client-python`。
39
+
40
+ リリースは GitHub Actions の Release(workflow_dispatch、version = X.Y.Z か X.Y.ZbN): `tools/prepare_release.py` が
41
+ pyproject.toml と `oep_client.__version__` を書き換え、CHANGELOG.md の Unreleased をその版にし、試験と build の後に commit と tag、
42
+ GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md の Unreleased に (EN) / (JA) で書き足しておく。
43
+
44
+ ## モジュール(`oep_client.v1`)
45
+
46
+ | モジュール | 中身 |
47
+ |---|---|
48
+ | `host` | 要求と結果、session_id とロック、`call()`(失敗なら例外)、pipeline、エラーの階層(`OepError` / `Rejected` / `Failed`) |
49
+ | `link` | transport: シリアルの口(常に COBS + CRC、`0x00 <COBS> 0x00`、フレームの外は雑音として捨てる、排他で開く)、USB vendor bulk / HID と TCP(長さつきフレーム、§5.1 の立て直し)、corr による照合と送り直し、`open_host(target)` |
50
+ | `core` | インターフェースを名前で探す(キャッシュつき)、confirm、probe の describe(ラベル、transport の一覧)、ロックの取り方(`take`)、ピンの割り当て(plan)、`Interface` の土台 |
51
+ | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`、`oep.target.riscv-dm`、リセット線の探索、GPIO 経由の attach |
52
+ | `console` | `oep.target.console`(位置つきのストリーム)と、バイト列として読む `ConsoleIO` |
53
+ | `fixture` | `oep.fixture.gpio` / `uart`(revision 1) |
54
+ | `capture` | `oep.fixture.capture`(revision 1、oep-spec の oep-if-capture)。読んだ区画は `Host.on_capture` の callback に `CaptureRecord` で渡る(記録の受け口。wireskein には依存しない) |
55
+ | `esp32_targets` | 独自インターフェース `io.github.ch32-riscv-ug.esp32.i2c-target` / `spi-target`(oep-probe-arduino の ESP32 の I2C / SPI の target) |
56
+ | `decode` | キャプチャのチャネルの復号(I2C) |
57
+ | `registry` | oep-spec の番号の表から生成したモジュール(編集しない。oep-spec から写し直す) |
58
+ | `arm` | `oep.wire.swd`、`oep.target.arm-adi`、MEM-AP、Cortex-M の停止と関数呼び出し |
59
+ | `ch32_flash` | CH32 の書き込み(RAM ローダー、ページ単位の書き直し) |
60
+ | `rp2350` | RP2350 の boot ROM 経由の flash と reboot |
61
+ | `uiapduino` | UIAPduino のブートローダへの出入り |
62
+ | `catalog` / `names` / `interfaces` / `dump` | 能力の一覧と describe の形、表示 |
63
+ | `fake` / `endpoint` / `fake_serial` / `fake_serve` | 偽の probe(下の「偽の probe」) |
64
+ | `target` | 上の主なものを 1 か所から import する入口(最初の版に合わせて書いた呼び出し側のため) |
65
+
66
+ ## 使い方の例
67
+
68
+ ```python
69
+ from oep_client.v1 import core, link, riscv, ch32_flash
70
+
71
+ hst = link.open_host("/run/board-identify/by-id/esp32-series-30eda0e31108") # pipelining つき
72
+ # a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
73
+ core.take(hst, 30000, owner="flash script") # the only way in: force; else wait out the lease, name the holder
74
+ wire = riscv.Wire(hst, "oep.wire.rvswd")
75
+ conn, _ = wire.attach(halt=True)
76
+ dm = riscv.RiscvDm(hst, conn)
77
+ dm.reset_halt()
78
+ result = ch32_flash.program(hst, dm, open("sketch.bin", "rb").read(), ch32_flash.PROFILES["x035"])
79
+ dm.reset(confirm=True)
80
+ wire.detach(conn)
81
+ hst.end()
82
+ ```
83
+
84
+ 能力の一覧は `uv run python -m oep_client.v1 dump --port <probe>`(`--fake p4-x035` でハードウェアなし)。
85
+ 実機での一通りの確認は ArduinoCore-CH32 の `tests/manual/oep_smoke/`(`oep_smoke.py`、`oep_probe_checks.py`)。
86
+
87
+ ## 偽の probe(動く spec)
88
+
89
+ `endpoint.Endpoint` は oep-spec の規範どおりに答える偽の probe で、ch32rv・この client・probe の firmware を突き合わせる
90
+ 「動く spec」として使う(spec が変わったら、probe の firmware より先にここを合わせる)。`fake` は宣言の例(profile:
91
+ `p4-x035`、`esp32-v003`、`p4-bench` = スロット 3 か所と席 2 つの架空の治具)、`fake_serial` はシリアルの口のバイトの側(COBS の
92
+ 候補、生のバイトと bind、セッション中の停止と再開)。
93
+
94
+ 外のプログラムの試験には `fake_serve` を子プロセスで使う:
95
+
96
+ ```sh
97
+ uv run python -m oep_client.v1.fake_serve --pty --profile p4-bench --slot x035 --bind last-reset \
98
+ --console 'uptime %d\r\n' --every 100
99
+ # 最初の行: PTY /dev/pts/N(--tcp 0 なら PORT n)。stdin を閉じると終わる
100
+ ```
101
+
102
+ pty がシリアルの口(host が TIOCEXCL を掛けて開く)、`--tcp PORT` は `--framing cobs`(シリアルの口)か `--framing length`
103
+ (vendor bulk / TCP の形)。故障の注入は `--drop N`(N 番目の答えを 1 回出さない。要求は実行済みなので送り直しは覚えた答えを
104
+ 受ける)、`--noise TEXT`(答えの前に雑音)、`--corrupt N`(N 番目の答えの CRC を 1 回壊す)。ほかは `--help`。
105
+
106
+ v0 の client(`oep_client.v0`)は 2026-09-26 に消した(git の履歴に残る)。v0 を話す probe はもう無い。
@@ -0,0 +1,36 @@
1
+ oep_client/__init__.py,sha256=f5QEx-7qtfSSRnMCtAflZ2y0GvfX6lXa_AVjdc4SQfg,166
2
+ oep_client/v1/__init__.py,sha256=Aghb5g5wouqnDIUFv9S4dkimnzs5YZcWx9gzgxi7GPM,90
3
+ oep_client/v1/__main__.py,sha256=CytzDl1qI-sqXPS5ZqtvV07Riz2agQ7uK-Hlwmyz8fw,1747
4
+ oep_client/v1/arm.py,sha256=JLyUhLSPcmsk73schZcaJYoOKuHzrvnU8kPmfrEvVuo,12331
5
+ oep_client/v1/capture.py,sha256=RZgDnWqr8Aqy49U7PqQpnKIQEL18djVuDJFydxrmv-c,17996
6
+ oep_client/v1/catalog.py,sha256=EqAQnSgHFxGbCO2GQ_Ee-uheJKbBBUUpL1-0qHfNMXU,6574
7
+ oep_client/v1/ch32_flash.py,sha256=Fc-6jEj15cQtMSKmfC5dmDetMXJ8hAdAuJ9d9odwrsA,12651
8
+ oep_client/v1/cobs.py,sha256=hqddQI_X7rsBvBvcP_MBE3bXPyB13-Azjqp_9JfdQ1g,2288
9
+ oep_client/v1/console.py,sha256=YYcuc9Qxe3kihla8wynYbDaLVr7wBLEvswrM-cMK3Tk,6859
10
+ oep_client/v1/core.py,sha256=z0-mplQVLOmPyXj1lHc-U7HYl3Gd6o-b5YJhpQTvqKU,8789
11
+ oep_client/v1/decode.py,sha256=M-bRARrA2qGZLoJn0BdXeTb4WjL_2Hb2nk_g1IGXIw4,2382
12
+ oep_client/v1/dump.py,sha256=GAnmu2abuDmGmbWcbbEfC3EUuPp5Hady9vQVNcQ-724,6743
13
+ oep_client/v1/endpoint.py,sha256=ay767evyw0QgM6oXOUcl94b_wxTHtXHxsc8sAUMi6u0,72014
14
+ oep_client/v1/esp32_targets.py,sha256=b8tNH-nb2KC-XgNfWg5l3TO0cyhZOF-gJajaVxNVSGQ,4988
15
+ oep_client/v1/fake.py,sha256=UzpB-A42Ma2QqJj0l-JmwCJkMn6DPVHHN77fcemT59U,13104
16
+ oep_client/v1/fake_serial.py,sha256=6fhtrit9kWix-D83yYfUKFLHJ11pEiMDrlKNeJKvCfU,4314
17
+ oep_client/v1/fake_serve.py,sha256=B93k_fV0j0KpO8Qtyx8dQfffxn2EeRsURsdX_ScKEP0,12423
18
+ oep_client/v1/fixture.py,sha256=653hCD-Iz0Lfs8dT4Hdg3QyubbbOGs1_xjq_3nb8foU,5564
19
+ oep_client/v1/frames.py,sha256=xjgjnXmXUqNJZ5m0h91r4Cy48WoVK1mEMW4gRg-mlMI,3991
20
+ oep_client/v1/hid_stream.py,sha256=99Dsm4Io1keukuETG_UNqjBGiVYZZeb__Pv4ytaKg9k,10695
21
+ oep_client/v1/host.py,sha256=Y5oqE-pw5Ja9cG1dzdy8svIEgR4Kqhki0wuyrpP_xH8,15563
22
+ oep_client/v1/interfaces.py,sha256=P4ehrIj4j-9eg4NjaVdV15pawI2vOqucJwVklSdR-As,5472
23
+ oep_client/v1/link.py,sha256=J9VtBWfTgNoIgUQuj8SsH9hHesQM2uEN545IoxKVS8E,19406
24
+ oep_client/v1/message.py,sha256=MEM1UT6KFM7AsT56jvBLt_FJK_6SJ7O_nGAiipW0_zo,9912
25
+ oep_client/v1/names.py,sha256=ztCZFWs__aR-a5G14TnJP-Q4YfLhbyrzvOVOMJuG93k,3653
26
+ oep_client/v1/registry.py,sha256=nRM-sQiAMs1OzuSZIajdsBfdFckYo3OfwgmcLkHzxqw,9867
27
+ oep_client/v1/riscv.py,sha256=E0_C-4mBsrp9FUoZpMiqSH9e8sC8Q4EO5HRjFKE6_pc,21392
28
+ oep_client/v1/rp2350.py,sha256=7OMyj270k3AY1e3-ljicPwOjf9jOlokbT6QNkA0b0yY,4227
29
+ oep_client/v1/target.py,sha256=R8BvYfFfYNhZF0MysJkNb8b69YgBlmeGXNX8ReZIe5k,1068
30
+ oep_client/v1/uiapduino.py,sha256=IEPG3LbZPksi8g3wmAf6T0CRWSfYKW1_qrH_GBVQdxY,6090
31
+ oep_client/v1/usb_stream.py,sha256=1nvuRNLQkpp8054eG-JutBKh9y0F_8PHGDAfG0ZSbaY,8819
32
+ oep_client_python-0.0.1.dist-info/METADATA,sha256=PdTZlP4XrPcU_zZZZYofEqST_flj7AeoXHWF9iC06LQ,7312
33
+ oep_client_python-0.0.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
34
+ oep_client_python-0.0.1.dist-info/entry_points.txt,sha256=F9_WK-5S92fBuWSeCR0GXzmA5OXY6y0xvYpHseqY2R8,52
35
+ oep_client_python-0.0.1.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
36
+ oep_client_python-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ oep = oep_client.v1.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Open Embedded Probe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.