smpclient 4.3.0__tar.gz → 4.4.2__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.3.0 → smpclient-4.4.2}/PKG-INFO +4 -3
- {smpclient-4.3.0 → smpclient-4.4.2}/README.md +1 -1
- {smpclient-4.3.0 → smpclient-4.4.2}/pyproject.toml +3 -1
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/__init__.py +24 -21
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/generics.py +10 -10
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/mcuboot.py +8 -2
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/transport/udp.py +6 -1
- {smpclient-4.3.0 → smpclient-4.4.2}/LICENSE +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/exceptions.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/extensions/__init__.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/extensions/intercreate.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/py.typed +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/requests/__init__.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/requests/file_management.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/requests/image_management.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/requests/os_management.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/requests/settings_management.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/requests/shell_management.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/requests/statistics_management.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/requests/user/__init__.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/requests/user/intercreate.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/requests/zephyr_management.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/transport/__init__.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/transport/_udp_client.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/transport/ble.py +0 -0
- {smpclient-4.3.0 → smpclient-4.4.2}/smpclient/transport/serial.py +0 -0
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: smpclient
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.2
|
|
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
|
|
7
7
|
Author: J.P. Hutchins
|
|
8
8
|
Author-email: jphutchins@gmail.com
|
|
9
|
-
Requires-Python: >=3.8.1,<3.
|
|
9
|
+
Requires-Python: >=3.8.1,<3.14
|
|
10
10
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.9
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
17
|
Requires-Dist: async-timeout (>=4.0.3,<5.0.0) ; python_version < "3.11"
|
|
17
18
|
Requires-Dist: bleak (>=0.22.1,<0.23.0)
|
|
18
19
|
Requires-Dist: intelhex (>=2.3.0,<3.0.0)
|
|
@@ -73,7 +74,7 @@ An online version is generated and available [here](https://intercreate.github.i
|
|
|
73
74
|
|
|
74
75
|
### Install Dependencies
|
|
75
76
|
|
|
76
|
-
- poetry: https://python-poetry.org/docs/#installation
|
|
77
|
+
- poetry==1.8.5: https://python-poetry.org/docs/#installation
|
|
77
78
|
|
|
78
79
|
### Create the venv
|
|
79
80
|
|
|
@@ -49,7 +49,7 @@ An online version is generated and available [here](https://intercreate.github.i
|
|
|
49
49
|
|
|
50
50
|
### Install Dependencies
|
|
51
51
|
|
|
52
|
-
- poetry: https://python-poetry.org/docs/#installation
|
|
52
|
+
- poetry==1.8.5: https://python-poetry.org/docs/#installation
|
|
53
53
|
|
|
54
54
|
### Create the venv
|
|
55
55
|
|
|
@@ -19,7 +19,7 @@ mcuimg = "smpclient.mcuboot:mcuimg"
|
|
|
19
19
|
source = "git-tag"
|
|
20
20
|
|
|
21
21
|
[tool.poetry.dependencies]
|
|
22
|
-
python = ">=3.8.1, <3.
|
|
22
|
+
python = ">=3.8.1, <3.14"
|
|
23
23
|
pyserial = "^3.5"
|
|
24
24
|
smp = "^3.1.1"
|
|
25
25
|
intelhex = "^2.3.0"
|
|
@@ -45,6 +45,7 @@ mike = "^2.1.3"
|
|
|
45
45
|
mkdocs-material = "^9.5.38"
|
|
46
46
|
griffe-inherited-docstrings = "^1.0.1"
|
|
47
47
|
griffe = "^1.3.1"
|
|
48
|
+
smp = "^3.1.1"
|
|
48
49
|
|
|
49
50
|
[tool.black]
|
|
50
51
|
line-length = 100
|
|
@@ -82,6 +83,7 @@ legacy_tox_ini = """
|
|
|
82
83
|
py310
|
|
83
84
|
py311
|
|
84
85
|
py312
|
|
86
|
+
py313
|
|
85
87
|
|
|
86
88
|
[testenv]
|
|
87
89
|
allowlist_externals =
|
|
@@ -29,11 +29,12 @@ import asyncio
|
|
|
29
29
|
import logging
|
|
30
30
|
from hashlib import sha256
|
|
31
31
|
from types import TracebackType
|
|
32
|
-
from typing import AsyncIterator, Final,
|
|
32
|
+
from typing import AsyncIterator, Final, Tuple, Type
|
|
33
33
|
|
|
34
34
|
from pydantic import ValidationError
|
|
35
35
|
from smp import header as smpheader
|
|
36
36
|
from smp import message as smpmsg
|
|
37
|
+
from typing_extensions import assert_never
|
|
37
38
|
|
|
38
39
|
from smpclient.exceptions import SMPBadSequence, SMPUploadError
|
|
39
40
|
from smpclient.generics import SMPRequest, TEr1, TEr2, TRep, error, success
|
|
@@ -130,9 +131,11 @@ class SMPClient:
|
|
|
130
131
|
print(f"Response: {response=}")
|
|
131
132
|
elif error(response):
|
|
132
133
|
print(f"Error: {response=}")
|
|
134
|
+
else:
|
|
135
|
+
assert_never(response)
|
|
133
136
|
```
|
|
134
137
|
|
|
135
|
-
Type Safety with Generic Typing:
|
|
138
|
+
Type Safety and Exhaustiveness with Generic Typing:
|
|
136
139
|
|
|
137
140
|
```python
|
|
138
141
|
response = await client.request(EchoWrite(d="Hello, World!"))
|
|
@@ -147,9 +150,13 @@ class SMPClient:
|
|
|
147
150
|
if error_v1(response):
|
|
148
151
|
reveal_type(response)
|
|
149
152
|
# Revealed type is 'EchoWriteErrorV1'
|
|
150
|
-
|
|
153
|
+
elif error_v2(response):
|
|
151
154
|
reveal_type(response)
|
|
152
155
|
# Revealed type is 'EchoWriteErrorV2'
|
|
156
|
+
else:
|
|
157
|
+
assert_never(response)
|
|
158
|
+
else:
|
|
159
|
+
assert_never(response)
|
|
153
160
|
```
|
|
154
161
|
|
|
155
162
|
"""
|
|
@@ -244,8 +251,8 @@ class SMPClient:
|
|
|
244
251
|
if response.off is None:
|
|
245
252
|
raise SMPUploadError(f"No offset received: {response=}")
|
|
246
253
|
yield response.off
|
|
247
|
-
else:
|
|
248
|
-
|
|
254
|
+
else:
|
|
255
|
+
assert_never(response) # pragma: no cover
|
|
249
256
|
|
|
250
257
|
# send chunks until the SMP server reports that the offset is at the end of the image
|
|
251
258
|
while response.off != len(image):
|
|
@@ -268,8 +275,8 @@ class SMPClient:
|
|
|
268
275
|
if response.off is None:
|
|
269
276
|
raise SMPUploadError(f"No offset received: {response=}")
|
|
270
277
|
yield response.off
|
|
271
|
-
else:
|
|
272
|
-
|
|
278
|
+
else:
|
|
279
|
+
assert_never(response) # pragma: no cover
|
|
273
280
|
|
|
274
281
|
logger.info("Upload complete")
|
|
275
282
|
|
|
@@ -313,8 +320,8 @@ class SMPClient:
|
|
|
313
320
|
if response.off is None:
|
|
314
321
|
raise SMPUploadError(f"No offset received: {response=}")
|
|
315
322
|
yield response.off
|
|
316
|
-
else:
|
|
317
|
-
|
|
323
|
+
else:
|
|
324
|
+
assert_never(response) # pragma: no cover
|
|
318
325
|
|
|
319
326
|
# send chunks until the SMP server reports that the offset is at the end of the image
|
|
320
327
|
while response.off != len(file_data):
|
|
@@ -328,8 +335,8 @@ class SMPClient:
|
|
|
328
335
|
raise SMPUploadError(response)
|
|
329
336
|
elif success(response):
|
|
330
337
|
yield response.off
|
|
331
|
-
else:
|
|
332
|
-
|
|
338
|
+
else:
|
|
339
|
+
assert_never(response) # pragma: no cover
|
|
333
340
|
|
|
334
341
|
logger.info("Upload complete")
|
|
335
342
|
|
|
@@ -359,8 +366,8 @@ class SMPClient:
|
|
|
359
366
|
if response.len is None:
|
|
360
367
|
raise SMPUploadError(f"No length received: {response=}")
|
|
361
368
|
file_length = response.len
|
|
362
|
-
else:
|
|
363
|
-
|
|
369
|
+
else:
|
|
370
|
+
assert_never(response) # pragma: no cover
|
|
364
371
|
|
|
365
372
|
file_data = response.data
|
|
366
373
|
|
|
@@ -374,8 +381,8 @@ class SMPClient:
|
|
|
374
381
|
raise SMPUploadError(response)
|
|
375
382
|
elif success(response):
|
|
376
383
|
file_data += response.data
|
|
377
|
-
else:
|
|
378
|
-
|
|
384
|
+
else:
|
|
385
|
+
assert_never(response) # pragma: no cover
|
|
379
386
|
|
|
380
387
|
logger.info("Download complete")
|
|
381
388
|
return file_data
|
|
@@ -407,7 +414,7 @@ class SMPClient:
|
|
|
407
414
|
# If the integer is less than 24, then the size is encoded in the same
|
|
408
415
|
# byte as the value.
|
|
409
416
|
# https://datatracker.ietf.org/doc/html/rfc8949#name-core-deterministic-encoding
|
|
410
|
-
return 0 if integer < 24 else 1 if integer
|
|
417
|
+
return 0 if integer < 24 else 1 if integer <= 0xFF else 2 if integer <= 0xFFFF else 4
|
|
411
418
|
|
|
412
419
|
def _get_max_cbor_and_data_size(self, request: smpmsg.WriteRequest) -> Tuple[int, int]:
|
|
413
420
|
"""Given an `ImageUploadWrite`, return the maximum CBOR size and data size."""
|
|
@@ -494,10 +501,6 @@ class SMPClient:
|
|
|
494
501
|
elif error(mcumgr_parameters):
|
|
495
502
|
logger.warning(f"Error reading MCUMgr parameters: {mcumgr_parameters}")
|
|
496
503
|
else:
|
|
497
|
-
|
|
504
|
+
assert_never(mcumgr_parameters)
|
|
498
505
|
except asyncio.TimeoutError:
|
|
499
506
|
logger.warning("Timeout waiting for MCUMgr parameters")
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
def _unreachable() -> NoReturn:
|
|
503
|
-
raise Exception("Unreachable")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Generics and
|
|
1
|
+
"""Generics and Type Narrowing for SMP Requests and Responses."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ from typing import Protocol, Type, TypeVar, Union
|
|
|
7
7
|
from smp import error as smperror
|
|
8
8
|
from smp import header as smphdr
|
|
9
9
|
from smp import message as smpmessage
|
|
10
|
-
from typing_extensions import
|
|
10
|
+
from typing_extensions import TypeIs
|
|
11
11
|
|
|
12
12
|
TEr1 = TypeVar("TEr1", bound=smperror.ErrorV1)
|
|
13
13
|
"""Type of SMP Error V1."""
|
|
@@ -47,8 +47,8 @@ class SMPRequest(Protocol[TRep, TEr1, TEr2]):
|
|
|
47
47
|
...
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def error_v1(response: smperror.ErrorV1 | TEr2 | TRep) ->
|
|
51
|
-
"""`
|
|
50
|
+
def error_v1(response: smperror.ErrorV1 | TEr2 | TRep) -> TypeIs[smperror.ErrorV1]:
|
|
51
|
+
"""`TypeIs` that returns `True` if the `response` is an `ErrorV1`.
|
|
52
52
|
|
|
53
53
|
Args:
|
|
54
54
|
response: The response to check.
|
|
@@ -59,8 +59,8 @@ def error_v1(response: smperror.ErrorV1 | TEr2 | TRep) -> TypeGuard[smperror.Err
|
|
|
59
59
|
return response.RESPONSE_TYPE == smpmessage.ResponseType.ERROR_V1
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
def error_v2(response: smperror.ErrorV1 | TEr2 | TRep) ->
|
|
63
|
-
"""`
|
|
62
|
+
def error_v2(response: smperror.ErrorV1 | TEr2 | TRep) -> TypeIs[TEr2]:
|
|
63
|
+
"""`TypeIs` that returns `True` if the `response` is an `ErrorV2`.
|
|
64
64
|
|
|
65
65
|
Args:
|
|
66
66
|
response: The response to check.
|
|
@@ -71,8 +71,8 @@ def error_v2(response: smperror.ErrorV1 | TEr2 | TRep) -> TypeGuard[TEr2]:
|
|
|
71
71
|
return response.RESPONSE_TYPE == smpmessage.ResponseType.ERROR_V2
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
def error(response: smperror.ErrorV1 | TEr2 | TRep) ->
|
|
75
|
-
"""`
|
|
74
|
+
def error(response: smperror.ErrorV1 | TEr2 | TRep) -> TypeIs[smperror.ErrorV1 | TEr2]:
|
|
75
|
+
"""`TypeIs` that returns `True` if the `response` is an `ErrorV1` or `ErrorV2`.
|
|
76
76
|
|
|
77
77
|
Args:
|
|
78
78
|
response: The response to check.
|
|
@@ -83,8 +83,8 @@ def error(response: smperror.ErrorV1 | TEr2 | TRep) -> TypeGuard[smperror.ErrorV
|
|
|
83
83
|
return error_v1(response) or error_v2(response)
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def success(response: smperror.ErrorV1 | TEr2 | TRep) ->
|
|
87
|
-
"""`
|
|
86
|
+
def success(response: smperror.ErrorV1 | TEr2 | TRep) -> TypeIs[TRep]:
|
|
87
|
+
"""`TypeIs` that returns `True` if the `response` is a successful `Response`.
|
|
88
88
|
|
|
89
89
|
Args:
|
|
90
90
|
response: The response to check.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tools for inspecting firmware images.
|
|
1
|
+
"""Tools for inspecting MCUBoot compatible firmware images.
|
|
2
2
|
|
|
3
3
|
Specification: https://docs.mcuboot.com/design.html
|
|
4
4
|
"""
|
|
@@ -101,6 +101,7 @@ class ImageVersion:
|
|
|
101
101
|
|
|
102
102
|
@staticmethod
|
|
103
103
|
def loads(data: bytes) -> 'ImageVersion':
|
|
104
|
+
"""Load an `ImageVersion` from `bytes`."""
|
|
104
105
|
return ImageVersion(*IMAGE_VERSION_STRUCT.unpack(data))
|
|
105
106
|
|
|
106
107
|
def __str__(self) -> str:
|
|
@@ -148,10 +149,12 @@ class ImageHeader:
|
|
|
148
149
|
|
|
149
150
|
@staticmethod
|
|
150
151
|
def load_from(file: BytesIO | BufferedReader) -> 'ImageHeader':
|
|
152
|
+
"""Load an `ImageHeader` from an open file."""
|
|
151
153
|
return ImageHeader.loads(file.read(IMAGE_HEADER_STRUCT.size))
|
|
152
154
|
|
|
153
155
|
@staticmethod
|
|
154
156
|
def load_file(path: str) -> 'ImageHeader':
|
|
157
|
+
"""Load an `ImageHeader` the file at `path`."""
|
|
155
158
|
with open(path, 'rb') as f:
|
|
156
159
|
return ImageHeader.load_from(f)
|
|
157
160
|
|
|
@@ -178,6 +181,7 @@ class ImageTLVInfo:
|
|
|
178
181
|
|
|
179
182
|
@staticmethod
|
|
180
183
|
def load_from(file: BytesIO | BufferedReader) -> 'ImageTLVInfo':
|
|
184
|
+
"""Load an `ImageTLVInfo` from a file."""
|
|
181
185
|
return ImageTLVInfo.loads(file.read(IMAGE_TLV_INFO_STRUCT.size))
|
|
182
186
|
|
|
183
187
|
|
|
@@ -191,6 +195,7 @@ class ImageTLV:
|
|
|
191
195
|
|
|
192
196
|
@staticmethod
|
|
193
197
|
def load_from(file: BytesIO | BufferedReader) -> 'ImageTLV':
|
|
198
|
+
"""Load an `ImageTLV` from a file."""
|
|
194
199
|
return ImageTLV(*IMAGE_TLV_STRUCT.unpack_from(file.read(IMAGE_TLV_STRUCT.size)))
|
|
195
200
|
|
|
196
201
|
|
|
@@ -225,6 +230,7 @@ class ImageInfo:
|
|
|
225
230
|
|
|
226
231
|
@staticmethod
|
|
227
232
|
def load_file(path: str) -> 'ImageInfo':
|
|
233
|
+
"""Load MCUBoot `ImageInfo` from the .bin or .hex file at `path`."""
|
|
228
234
|
file_path = pathlib.Path(path)
|
|
229
235
|
if file_path.suffix not in {".bin", ".hex"}:
|
|
230
236
|
raise MCUBootImageError(
|
|
@@ -279,7 +285,7 @@ def mcuimg() -> int:
|
|
|
279
285
|
prog="mcuimg",
|
|
280
286
|
description=(
|
|
281
287
|
"Inspect an MCUBoot compatible firmware update image."
|
|
282
|
-
"\nCopyright (C) 2023 Intercreate, Inc. | github.com/intercreate/smpclient"
|
|
288
|
+
"\nCopyright (C) 2023-2024 Intercreate, Inc. | github.com/intercreate/smpclient"
|
|
283
289
|
),
|
|
284
290
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
285
291
|
)
|
|
@@ -99,4 +99,9 @@ class SMPUDPTransport(SMPTransport):
|
|
|
99
99
|
@override
|
|
100
100
|
@property
|
|
101
101
|
def mtu(self) -> int:
|
|
102
|
-
return self.
|
|
102
|
+
return self._mtu
|
|
103
|
+
|
|
104
|
+
@override
|
|
105
|
+
@property
|
|
106
|
+
def max_unencoded_size(self) -> int:
|
|
107
|
+
return self._mtu
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|