smpclient 4.2.0__tar.gz → 4.3.0__tar.gz
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.
- {smpclient-4.2.0 → smpclient-4.3.0}/PKG-INFO +7 -1
- {smpclient-4.2.0 → smpclient-4.3.0}/README.md +5 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/pyproject.toml +16 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/__init__.py +174 -29
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/generics.py +38 -5
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/transport/__init__.py +29 -6
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/transport/_udp_client.py +13 -4
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/transport/ble.py +5 -3
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/transport/serial.py +23 -20
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/transport/udp.py +6 -1
- {smpclient-4.2.0 → smpclient-4.3.0}/LICENSE +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/exceptions.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/extensions/__init__.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/extensions/intercreate.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/mcuboot.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/py.typed +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/requests/__init__.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/requests/file_management.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/requests/image_management.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/requests/os_management.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/requests/settings_management.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/requests/shell_management.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/requests/statistics_management.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/requests/user/__init__.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/requests/user/intercreate.py +0 -0
- {smpclient-4.2.0 → smpclient-4.3.0}/smpclient/requests/zephyr_management.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: smpclient
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.0
|
|
4
4
|
Summary: Simple Management Protocol (SMP) Client for remotely managing MCU firmware
|
|
5
5
|
Home-page: https://github.com/intercreate/smpclient
|
|
6
6
|
License: Apache-2.0
|
|
@@ -18,6 +18,7 @@ Requires-Dist: bleak (>=0.22.1,<0.23.0)
|
|
|
18
18
|
Requires-Dist: intelhex (>=2.3.0,<3.0.0)
|
|
19
19
|
Requires-Dist: pyserial (>=3.5,<4.0)
|
|
20
20
|
Requires-Dist: smp (>=3.1.1,<4.0.0)
|
|
21
|
+
Project-URL: Documentation, https://intercreate.github.io/smpclient
|
|
21
22
|
Project-URL: Repository, https://github.com/intercreate/smpclient
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
23
24
|
|
|
@@ -39,6 +40,11 @@ If you'd like an SMP CLI application instead of a library, then you should try
|
|
|
39
40
|
|
|
40
41
|
`smpclient` is [distributed by PyPI](https://pypi.org/project/smpclient/) and can be installed with `poetry`, `pip`, and other dependency managers.
|
|
41
42
|
|
|
43
|
+
## User Documentation
|
|
44
|
+
|
|
45
|
+
Documentation is in the source code so that it is available to your editor.
|
|
46
|
+
An online version is generated and available [here](https://intercreate.github.io/smpclient/).
|
|
47
|
+
|
|
42
48
|
## Development Quickstart
|
|
43
49
|
|
|
44
50
|
> Assumes that you've already [setup your development environment](#development-environment-setup).
|
|
@@ -16,6 +16,11 @@ If you'd like an SMP CLI application instead of a library, then you should try
|
|
|
16
16
|
|
|
17
17
|
`smpclient` is [distributed by PyPI](https://pypi.org/project/smpclient/) and can be installed with `poetry`, `pip`, and other dependency managers.
|
|
18
18
|
|
|
19
|
+
## User Documentation
|
|
20
|
+
|
|
21
|
+
Documentation is in the source code so that it is available to your editor.
|
|
22
|
+
An online version is generated and available [here](https://intercreate.github.io/smpclient/).
|
|
23
|
+
|
|
19
24
|
## Development Quickstart
|
|
20
25
|
|
|
21
26
|
> Assumes that you've already [setup your development environment](#development-environment-setup).
|
|
@@ -7,6 +7,7 @@ authors = [
|
|
|
7
7
|
"J.P. Hutchins <jp@intercreate.io",
|
|
8
8
|
]
|
|
9
9
|
repository = "https://github.com/intercreate/smpclient"
|
|
10
|
+
documentation = "https://intercreate.github.io/smpclient"
|
|
10
11
|
readme = "README.md"
|
|
11
12
|
license = "Apache-2.0"
|
|
12
13
|
packages = [{ include = "smpclient" }]
|
|
@@ -36,6 +37,14 @@ mypy-extensions = "^1.0.0"
|
|
|
36
37
|
pytest-asyncio = "^0.23.2"
|
|
37
38
|
types-pyserial = "^3.5.0.11"
|
|
38
39
|
tox = "^4.15.0"
|
|
40
|
+
pydoclint = "^0.5.8"
|
|
41
|
+
|
|
42
|
+
[tool.poetry.group.doc.dependencies]
|
|
43
|
+
mkdocstrings = { extras = ["python"], version = "^0.26.1" }
|
|
44
|
+
mike = "^2.1.3"
|
|
45
|
+
mkdocs-material = "^9.5.38"
|
|
46
|
+
griffe-inherited-docstrings = "^1.0.1"
|
|
47
|
+
griffe = "^1.3.1"
|
|
39
48
|
|
|
40
49
|
[tool.black]
|
|
41
50
|
line-length = 100
|
|
@@ -52,6 +61,13 @@ skip = [".venv", "dutfirmware", ".tox"]
|
|
|
52
61
|
disallow_untyped_defs = true
|
|
53
62
|
exclude = ['.venv', 'dutfirmware', '.tox']
|
|
54
63
|
|
|
64
|
+
[tool.pydoclint]
|
|
65
|
+
style = "google"
|
|
66
|
+
arg-type-hints-in-docstring = false
|
|
67
|
+
allow-init-docstring = true
|
|
68
|
+
check-return-types = false
|
|
69
|
+
check-yield-types = false
|
|
70
|
+
|
|
55
71
|
[tool.pytest.ini_options]
|
|
56
72
|
norecursedirs = "dutfirmware/*"
|
|
57
73
|
filterwarnings = ["ignore:The --rsyncdir:DeprecationWarning"]
|
|
@@ -1,4 +1,27 @@
|
|
|
1
|
-
"""Simple Management Protocol (SMP) Client.
|
|
1
|
+
"""Simple Management Protocol (SMP) Client.
|
|
2
|
+
|
|
3
|
+
This package implements transport layers for the Simple Management Protocol (SMP).
|
|
4
|
+
The SMP Protocol defines common management operations for MCUs like firmware
|
|
5
|
+
updates, file management, configuration, and statistics retrieval.
|
|
6
|
+
|
|
7
|
+
Additionally, SMP is extensible, allowing for custom commands to be defined to
|
|
8
|
+
meet the specific needs of the product.
|
|
9
|
+
|
|
10
|
+
### Operating Systems
|
|
11
|
+
|
|
12
|
+
| | Windows 11 | Ubuntu | macOS (Arm/x86) |
|
|
13
|
+
|---|------------|-------|-------|
|
|
14
|
+
| Serial (UART, USB, CAN, ...) | ✅ | ✅ | ✅ |
|
|
15
|
+
| Bluetooth Low Energy (BLE) | ✅ | ✅ | ✅ |
|
|
16
|
+
| UDP (Ethernet, Wi-Fi) | ✅ | ✅ | ✅ |
|
|
17
|
+
|
|
18
|
+
### Examples
|
|
19
|
+
|
|
20
|
+
Many usage examples are available on
|
|
21
|
+
[GitHub](https://github.com/intercreate/smpclient/tree/main/examples)
|
|
22
|
+
or in your local clone at `examples/`.
|
|
23
|
+
|
|
24
|
+
"""
|
|
2
25
|
|
|
3
26
|
from __future__ import annotations
|
|
4
27
|
|
|
@@ -6,7 +29,7 @@ import asyncio
|
|
|
6
29
|
import logging
|
|
7
30
|
from hashlib import sha256
|
|
8
31
|
from types import TracebackType
|
|
9
|
-
from typing import AsyncIterator, Final, Tuple, Type
|
|
32
|
+
from typing import AsyncIterator, Final, NoReturn, Tuple, Type
|
|
10
33
|
|
|
11
34
|
from pydantic import ValidationError
|
|
12
35
|
from smp import header as smpheader
|
|
@@ -28,13 +51,51 @@ logger = logging.getLogger(__name__)
|
|
|
28
51
|
|
|
29
52
|
|
|
30
53
|
class SMPClient:
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
"""Create a client to the SMP server `address`, using `transport`.
|
|
55
|
+
|
|
56
|
+
This class provides a high-level interface to an SMP server. Other than
|
|
57
|
+
the `request` method, all methods are abstractions of common SMP routines,
|
|
58
|
+
such as uploading a FW image or downloading a file.
|
|
59
|
+
|
|
60
|
+
The `request` method is used to send an SMP request to the server and return
|
|
61
|
+
the response or error.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
transport: the `SMPTransport` to use
|
|
65
|
+
address: the address of the SMP server, see `smpclient.transport` for details
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import asyncio
|
|
71
|
+
from smpclient import SMPClient
|
|
72
|
+
from smpclient.requests.os_management import EchoWrite
|
|
73
|
+
from smpclient.transport.ble import SMPBLETransport
|
|
74
|
+
|
|
75
|
+
async def main():
|
|
76
|
+
async with SMPClient(SMPBLETransport(), "00:11:22:33:44:55") as client:
|
|
77
|
+
response = await client.request(EchoWrite(d="Hello, World!"))
|
|
78
|
+
|
|
79
|
+
if success(response):
|
|
80
|
+
print(f"Response: {response=}")
|
|
81
|
+
elif error(response):
|
|
82
|
+
print(f"Error: {response=}")
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
asyncio.run(main())
|
|
86
|
+
```
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, transport: SMPTransport, address: str): # noqa: DOC301
|
|
33
90
|
self._transport: Final = transport
|
|
34
91
|
self._address: Final = address
|
|
35
92
|
|
|
36
93
|
async def connect(self, timeout_s: float = 5.0) -> None:
|
|
37
|
-
"""Connect to the SMP server.
|
|
94
|
+
"""Connect to the SMP server.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
timeout_s: the timeout for the connection attempt in seconds
|
|
98
|
+
"""
|
|
38
99
|
await self._transport.connect(self._address, timeout_s)
|
|
39
100
|
await self._initialize()
|
|
40
101
|
|
|
@@ -45,7 +106,53 @@ class SMPClient:
|
|
|
45
106
|
async def request(
|
|
46
107
|
self, request: SMPRequest[TRep, TEr1, TEr2], timeout_s: float = 120.000
|
|
47
108
|
) -> TRep | TEr1 | TEr2:
|
|
48
|
-
"""Make an `SMPRequest` to the SMP server.
|
|
109
|
+
"""Make an `SMPRequest` to the SMP server and return the Response or Error.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
request: the `SMPRequest` to send
|
|
113
|
+
timeout_s: the timeout for the request in seconds
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The typed and validated Response or Error
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
TimeoutError: if the request times out
|
|
120
|
+
SMPBadSequence: if the response sequence does not match the request sequence
|
|
121
|
+
ValidationError: if the response cannot be parsed as a Response or Error
|
|
122
|
+
|
|
123
|
+
Examples:
|
|
124
|
+
|
|
125
|
+
Usage:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
response = await client.request(EchoWrite(d="Hello, World!"))
|
|
129
|
+
if success(response):
|
|
130
|
+
print(f"Response: {response=}")
|
|
131
|
+
elif error(response):
|
|
132
|
+
print(f"Error: {response=}")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Type Safety with Generic Typing:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
response = await client.request(EchoWrite(d="Hello, World!"))
|
|
139
|
+
reveal_type(response)
|
|
140
|
+
# Revealed type is 'Union[EchoWriteResponse, EchoWriteErrorV1, EchoWriteErrorV2]'
|
|
141
|
+
if success(response):
|
|
142
|
+
reveal_type(response)
|
|
143
|
+
# Revealed type is 'EchoWriteResponse'
|
|
144
|
+
elif error(response):
|
|
145
|
+
reveal_type(response)
|
|
146
|
+
# Revealed type is 'Union[EchoWriteErrorV1, EchoWriteErrorV2]'
|
|
147
|
+
if error_v1(response):
|
|
148
|
+
reveal_type(response)
|
|
149
|
+
# Revealed type is 'EchoWriteErrorV1'
|
|
150
|
+
else:
|
|
151
|
+
reveal_type(response)
|
|
152
|
+
# Revealed type is 'EchoWriteErrorV2'
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
"""
|
|
49
156
|
|
|
50
157
|
try:
|
|
51
158
|
async with timeout(timeout_s):
|
|
@@ -91,22 +198,29 @@ class SMPClient:
|
|
|
91
198
|
) -> AsyncIterator[int]:
|
|
92
199
|
"""Iteratively upload an `image` to `slot`, yielding the offset.
|
|
93
200
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
201
|
+
Args:
|
|
202
|
+
image: the `bytes` to upload
|
|
203
|
+
slot: the slot to upload to (0 for default)
|
|
204
|
+
upgrade: `True` to mark the image as confirmed. This is unsafe and
|
|
205
|
+
can cause a boot-loop that could brick the device. This setting
|
|
206
|
+
should be left at the default `False` and the image should be
|
|
207
|
+
confirmed from within the upgraded application. Zephyr provides
|
|
208
|
+
[boot_write_img_confirmed()](https://docs.zephyrproject.org/apidoc/latest/group__mcuboot__api.html#ga95ccc9e1c7460fec16b9ce9ac8ad7a72)
|
|
209
|
+
for this purpose.
|
|
210
|
+
first_timeout_s: the timeout for the first `ImageUploadWrite` request
|
|
211
|
+
subsequent_timeout_s: the timeout for subsequent `ImageUploadWrite` requests
|
|
212
|
+
use_sha: `True` to include the SHA256 hash of the image in the first
|
|
213
|
+
packet.
|
|
214
|
+
|
|
215
|
+
Zephyr's SMP server will fail with `MGMT_ERR.EINVAL` if the
|
|
216
|
+
MTU is too small to include both the SHA256 and the first 32-bytes
|
|
217
|
+
of the image. Increase the MTU or set `use_sha=False` in this case.
|
|
218
|
+
|
|
219
|
+
Yields:
|
|
220
|
+
the offset of the image upload
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
SMPUploadError: if the upload routine fails
|
|
110
224
|
"""
|
|
111
225
|
|
|
112
226
|
response = await self.request(
|
|
@@ -131,7 +245,7 @@ class SMPClient:
|
|
|
131
245
|
raise SMPUploadError(f"No offset received: {response=}")
|
|
132
246
|
yield response.off
|
|
133
247
|
else: # pragma: no cover
|
|
134
|
-
|
|
248
|
+
_unreachable()
|
|
135
249
|
|
|
136
250
|
# send chunks until the SMP server reports that the offset is at the end of the image
|
|
137
251
|
while response.off != len(image):
|
|
@@ -155,7 +269,7 @@ class SMPClient:
|
|
|
155
269
|
raise SMPUploadError(f"No offset received: {response=}")
|
|
156
270
|
yield response.off
|
|
157
271
|
else: # pragma: no cover
|
|
158
|
-
|
|
272
|
+
_unreachable()
|
|
159
273
|
|
|
160
274
|
logger.info("Upload complete")
|
|
161
275
|
|
|
@@ -172,6 +286,19 @@ class SMPClient:
|
|
|
172
286
|
file_path: str,
|
|
173
287
|
timeout_s: float = 2.500,
|
|
174
288
|
) -> AsyncIterator[int]:
|
|
289
|
+
"""Iteratively upload a `file_data` to `file_path`, yielding the offset.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
file_data: the `bytes` to upload
|
|
293
|
+
file_path: the path to upload to
|
|
294
|
+
timeout_s: the timeout for each `FileUpload` request
|
|
295
|
+
|
|
296
|
+
Yields:
|
|
297
|
+
int: the offset of the file upload
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
SMPUploadError: if the upload routine fails
|
|
301
|
+
"""
|
|
175
302
|
response = await self.request(
|
|
176
303
|
self._maximize_file_upload_packet(
|
|
177
304
|
FileUpload(name=file_path, off=0, data=b"", len=len(file_data)),
|
|
@@ -187,7 +314,7 @@ class SMPClient:
|
|
|
187
314
|
raise SMPUploadError(f"No offset received: {response=}")
|
|
188
315
|
yield response.off
|
|
189
316
|
else: # pragma: no cover
|
|
190
|
-
|
|
317
|
+
_unreachable()
|
|
191
318
|
|
|
192
319
|
# send chunks until the SMP server reports that the offset is at the end of the image
|
|
193
320
|
while response.off != len(file_data):
|
|
@@ -202,7 +329,7 @@ class SMPClient:
|
|
|
202
329
|
elif success(response):
|
|
203
330
|
yield response.off
|
|
204
331
|
else: # pragma: no cover
|
|
205
|
-
|
|
332
|
+
_unreachable()
|
|
206
333
|
|
|
207
334
|
logger.info("Upload complete")
|
|
208
335
|
|
|
@@ -211,6 +338,18 @@ class SMPClient:
|
|
|
211
338
|
file_path: str,
|
|
212
339
|
timeout_s: float = 2.500,
|
|
213
340
|
) -> bytes:
|
|
341
|
+
"""Download a file from the SMP server.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
file_path: the path to download
|
|
345
|
+
timeout_s: the timeout for each `FileDownload` request
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
The downloaded file as `bytes`
|
|
349
|
+
|
|
350
|
+
Raises:
|
|
351
|
+
SMPUploadError: if the download routine fails
|
|
352
|
+
"""
|
|
214
353
|
response = await self.request(FileDownload(off=0, name=file_path), timeout_s=timeout_s)
|
|
215
354
|
file_length = 0
|
|
216
355
|
|
|
@@ -221,7 +360,7 @@ class SMPClient:
|
|
|
221
360
|
raise SMPUploadError(f"No length received: {response=}")
|
|
222
361
|
file_length = response.len
|
|
223
362
|
else: # pragma: no cover
|
|
224
|
-
|
|
363
|
+
_unreachable()
|
|
225
364
|
|
|
226
365
|
file_data = response.data
|
|
227
366
|
|
|
@@ -236,13 +375,15 @@ class SMPClient:
|
|
|
236
375
|
elif success(response):
|
|
237
376
|
file_data += response.data
|
|
238
377
|
else: # pragma: no cover
|
|
239
|
-
|
|
378
|
+
_unreachable()
|
|
240
379
|
|
|
241
380
|
logger.info("Download complete")
|
|
242
381
|
return file_data
|
|
243
382
|
|
|
244
383
|
@property
|
|
245
384
|
def address(self) -> str:
|
|
385
|
+
"""The SMP server address."""
|
|
386
|
+
|
|
246
387
|
return self._address
|
|
247
388
|
|
|
248
389
|
async def __aenter__(self) -> "SMPClient":
|
|
@@ -353,6 +494,10 @@ class SMPClient:
|
|
|
353
494
|
elif error(mcumgr_parameters):
|
|
354
495
|
logger.warning(f"Error reading MCUMgr parameters: {mcumgr_parameters}")
|
|
355
496
|
else:
|
|
356
|
-
|
|
497
|
+
_unreachable()
|
|
357
498
|
except asyncio.TimeoutError:
|
|
358
499
|
logger.warning("Timeout waiting for MCUMgr parameters")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _unreachable() -> NoReturn:
|
|
503
|
+
raise Exception("Unreachable")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Generics and TypeGuards for SMP Requests and Responses."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -10,8 +10,13 @@ from smp import message as smpmessage
|
|
|
10
10
|
from typing_extensions import TypeGuard
|
|
11
11
|
|
|
12
12
|
TEr1 = TypeVar("TEr1", bound=smperror.ErrorV1)
|
|
13
|
+
"""Type of SMP Error V1."""
|
|
14
|
+
|
|
13
15
|
TEr2 = TypeVar("TEr2", bound=smperror.ErrorV2)
|
|
16
|
+
"""Type of SMP Error V2."""
|
|
17
|
+
|
|
14
18
|
TRep = TypeVar("TRep", bound=Union[smpmessage.ReadResponse, smpmessage.WriteResponse])
|
|
19
|
+
"""Type of successful SMP Response (ReadResponse or WriteResponse)."""
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
class SMPRequest(Protocol[TRep, TEr1, TEr2]):
|
|
@@ -43,20 +48,48 @@ class SMPRequest(Protocol[TRep, TEr1, TEr2]):
|
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
def error_v1(response: smperror.ErrorV1 | TEr2 | TRep) -> TypeGuard[smperror.ErrorV1]:
|
|
46
|
-
"""`TypeGuard` that returns `True` if the `response` is an `ErrorV1`.
|
|
51
|
+
"""`TypeGuard` that returns `True` if the `response` is an `ErrorV1`.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
response: The response to check.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
`True` if the `response` is an `ErrorV1`.
|
|
58
|
+
"""
|
|
47
59
|
return response.RESPONSE_TYPE == smpmessage.ResponseType.ERROR_V1
|
|
48
60
|
|
|
49
61
|
|
|
50
62
|
def error_v2(response: smperror.ErrorV1 | TEr2 | TRep) -> TypeGuard[TEr2]:
|
|
51
|
-
"""`TypeGuard` that returns `True` if the `response` is an `ErrorV2`.
|
|
63
|
+
"""`TypeGuard` that returns `True` if the `response` is an `ErrorV2`.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
response: The response to check.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
`True` if the `response` is an `ErrorV2`.
|
|
70
|
+
"""
|
|
52
71
|
return response.RESPONSE_TYPE == smpmessage.ResponseType.ERROR_V2
|
|
53
72
|
|
|
54
73
|
|
|
55
74
|
def error(response: smperror.ErrorV1 | TEr2 | TRep) -> TypeGuard[smperror.ErrorV1 | TEr2]:
|
|
56
|
-
"""`TypeGuard` that returns `True` if the `response` is an `ErrorV1` or `ErrorV2`.
|
|
75
|
+
"""`TypeGuard` that returns `True` if the `response` is an `ErrorV1` or `ErrorV2`.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
response: The response to check.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
`True` if the `response` is an `ErrorV1` or `ErrorV2`.
|
|
82
|
+
"""
|
|
57
83
|
return error_v1(response) or error_v2(response)
|
|
58
84
|
|
|
59
85
|
|
|
60
86
|
def success(response: smperror.ErrorV1 | TEr2 | TRep) -> TypeGuard[TRep]:
|
|
61
|
-
"""`TypeGuard` that returns `True` if the `response` is a successful `Response`.
|
|
87
|
+
"""`TypeGuard` that returns `True` if the `response` is a successful `Response`.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
response: The response to check.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
`True` if the `response` is a successful `Response`.
|
|
94
|
+
"""
|
|
62
95
|
return response.RESPONSE_TYPE == smpmessage.ResponseType.SUCCESS
|
|
@@ -6,7 +6,7 @@ from typing import Protocol
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class SMPTransportDisconnected(Exception):
|
|
9
|
-
|
|
9
|
+
"""Raised when the SMP transport is disconnected."""
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class SMPTransport(Protocol):
|
|
@@ -14,22 +14,45 @@ class SMPTransport(Protocol):
|
|
|
14
14
|
"""The SMP server transport buffer size, in 8-bit bytes."""
|
|
15
15
|
|
|
16
16
|
async def connect(self, address: str, timeout_s: float) -> None: # pragma: no cover
|
|
17
|
-
"""Connect the `SMPTransport`.
|
|
17
|
+
"""Connect the `SMPTransport`.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
address: The SMP server address.
|
|
21
|
+
timeout_s: The connection timeout in seconds."""
|
|
18
22
|
|
|
19
23
|
async def disconnect(self) -> None: # pragma: no cover
|
|
20
24
|
"""Disconnect the `SMPTransport`."""
|
|
21
25
|
|
|
22
26
|
async def send(self, data: bytes) -> None: # pragma: no cover
|
|
23
|
-
"""Send the encoded `SMPRequest` `data`.
|
|
27
|
+
"""Send the encoded `SMPRequest` `data`.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
data: The encoded `SMPRequest`.
|
|
31
|
+
"""
|
|
24
32
|
|
|
25
33
|
async def receive(self) -> bytes: # pragma: no cover
|
|
26
|
-
"""Receive the decoded `SMPResponse
|
|
34
|
+
"""Receive the decoded `SMPResponse` data.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The `SMPResponse` bytes.
|
|
38
|
+
"""
|
|
27
39
|
|
|
28
40
|
async def send_and_receive(self, data: bytes) -> bytes: # pragma: no cover
|
|
29
|
-
"""Send the encoded `SMPRequest` `data` and receive the decoded `SMPResponse`.
|
|
41
|
+
"""Send the encoded `SMPRequest` `data` and receive the decoded `SMPResponse`.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
data: The encoded `SMPRequest`.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The `SMPResponse` bytes.
|
|
48
|
+
"""
|
|
30
49
|
|
|
31
50
|
def initialize(self, smp_server_transport_buffer_size: int) -> None: # pragma: no cover
|
|
32
|
-
"""Initialize the `SMPTransport` with the server transport buffer size.
|
|
51
|
+
"""Initialize the `SMPTransport` with the server transport buffer size.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
smp_server_transport_buffer_size: The SMP server transport buffer size, in 8-bit bytes.
|
|
55
|
+
"""
|
|
33
56
|
self._smp_server_transport_buffer_size = smp_server_transport_buffer_size
|
|
34
57
|
|
|
35
58
|
@property
|
|
@@ -22,9 +22,11 @@ class UDPClient:
|
|
|
22
22
|
async def connect(self, remote_addr: Addr, _local_addr: Addr | None = None) -> None:
|
|
23
23
|
"""Create a UDP connection to the given `Addr`.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
Args:
|
|
26
|
+
remote_addr: The remote address to connect to.
|
|
27
|
+
_local_addr: For unit tests only! The local address to connect from.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
28
30
|
|
|
29
31
|
```python
|
|
30
32
|
|
|
@@ -44,12 +46,19 @@ class UDPClient:
|
|
|
44
46
|
|
|
45
47
|
This does not block; it buffers the data and arranges for it to be sent
|
|
46
48
|
out asynchronously.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
data: The data to send.
|
|
47
52
|
"""
|
|
48
53
|
|
|
49
54
|
self._transport.sendto(data)
|
|
50
55
|
|
|
51
56
|
async def receive(self) -> bytes:
|
|
52
|
-
"""Receive data from the transport.
|
|
57
|
+
"""Receive data from the transport.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
bytes: The data received
|
|
61
|
+
"""
|
|
53
62
|
|
|
54
63
|
return await self._protocol.receive_queue.get()
|
|
55
64
|
|
|
@@ -51,21 +51,23 @@ UUID_PATTERN: Final = re.compile(
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
class SMPBLETransportException(SMPClientException):
|
|
54
|
-
|
|
54
|
+
"""Base class for SMP BLE transport exceptions."""
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
class SMPBLETransportDeviceNotFound(SMPBLETransportException):
|
|
58
|
-
|
|
58
|
+
"""Raised when a BLE device is not found."""
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
class SMPBLETransportNotSMPServer(SMPBLETransportException):
|
|
62
|
-
|
|
62
|
+
"""Raised when the SMP characteristic UUID is not found."""
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
logger = logging.getLogger(__name__)
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
class SMPBLETransport(SMPTransport):
|
|
69
|
+
"""A Bluetooth Low Energy (BLE) SMPTransport."""
|
|
70
|
+
|
|
69
71
|
def __init__(self) -> None:
|
|
70
72
|
self._buffer = bytearray()
|
|
71
73
|
self._notify_condition = asyncio.Condition()
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
"""A serial SMPTransport.
|
|
1
|
+
"""A serial SMPTransport.
|
|
2
|
+
|
|
3
|
+
In addition to UART, this transport can be used with USB CDC ACM and CAN.
|
|
4
|
+
"""
|
|
2
5
|
|
|
3
6
|
from __future__ import annotations
|
|
4
7
|
|
|
@@ -66,7 +69,7 @@ class SMPSerialTransport(SMPTransport):
|
|
|
66
69
|
self.state = SMPSerialTransport._ReadBuffer.State.SER
|
|
67
70
|
"""The state of the read buffer."""
|
|
68
71
|
|
|
69
|
-
def __init__(
|
|
72
|
+
def __init__( # noqa: DOC301
|
|
70
73
|
self,
|
|
71
74
|
max_smp_encoded_frame_size: int = 256,
|
|
72
75
|
line_length: int = 128,
|
|
@@ -85,24 +88,24 @@ class SMPSerialTransport(SMPTransport):
|
|
|
85
88
|
) -> None:
|
|
86
89
|
"""Initialize the serial transport.
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
91
|
+
Args:
|
|
92
|
+
max_smp_encoded_frame_size: The maximum size of an encoded SMP
|
|
93
|
+
frame. The SMP server needs to have a buffer large enough to
|
|
94
|
+
receive the encoded frame packets and to store the decoded frame.
|
|
95
|
+
line_length: The maximum SMP packet size.
|
|
96
|
+
line_buffers: The number of line buffers in the serial buffer.
|
|
97
|
+
baudrate: The baudrate of the serial connection. OK to ignore for
|
|
98
|
+
USB CDC ACM.
|
|
99
|
+
bytesize: The number of data bits.
|
|
100
|
+
parity: The parity setting.
|
|
101
|
+
stopbits: The number of stop bits.
|
|
102
|
+
timeout: The read timeout.
|
|
103
|
+
xonxoff: Enable software flow control.
|
|
104
|
+
rtscts: Enable hardware (RTS/CTS) flow control.
|
|
105
|
+
write_timeout: The write timeout.
|
|
106
|
+
dsrdtr: Enable hardware (DSR/DTR) flow control.
|
|
107
|
+
inter_byte_timeout: The inter-byte timeout.
|
|
108
|
+
exclusive: The exclusive access timeout.
|
|
106
109
|
|
|
107
110
|
"""
|
|
108
111
|
if max_smp_encoded_frame_size < line_length * line_buffers:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""A UDP SMPTransport."""
|
|
1
|
+
"""A UDP SMPTransport for Network connections like Wi-Fi or Ethernet."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
@@ -16,6 +16,11 @@ logger = logging.getLogger(__name__)
|
|
|
16
16
|
|
|
17
17
|
class SMPUDPTransport(SMPTransport):
|
|
18
18
|
def __init__(self, mtu: int = 1500) -> None:
|
|
19
|
+
"""Initialize the SMP UDP transport.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
mtu: The Maximum Transmission Unit (MTU) in 8-bit bytes.
|
|
23
|
+
"""
|
|
19
24
|
self._mtu = mtu
|
|
20
25
|
|
|
21
26
|
self._client: Final = UDPClient()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|