pyxcp 0.23.8__cp313-cp313-macosx_11_0_arm64.whl → 0.25.7__cp313-cp313-macosx_11_0_arm64.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.
- pyxcp/__init__.py +1 -1
- pyxcp/cmdline.py +14 -29
- pyxcp/config/__init__.py +1257 -1258
- pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
- pyxcp/cpp_ext/bin.hpp +7 -6
- pyxcp/cpp_ext/cpp_ext.cpython-310-darwin.so +0 -0
- pyxcp/cpp_ext/cpp_ext.cpython-311-darwin.so +0 -0
- pyxcp/cpp_ext/cpp_ext.cpython-312-darwin.so +0 -0
- pyxcp/cpp_ext/cpp_ext.cpython-313-darwin.so +0 -0
- pyxcp/cpp_ext/daqlist.hpp +241 -73
- pyxcp/cpp_ext/extension_wrapper.cpp +123 -15
- pyxcp/cpp_ext/framing.hpp +360 -0
- pyxcp/cpp_ext/helper.hpp +280 -280
- pyxcp/cpp_ext/mcobject.hpp +248 -246
- pyxcp/cpp_ext/sxi_framing.hpp +332 -0
- pyxcp/daq_stim/__init__.py +145 -67
- pyxcp/daq_stim/optimize/binpacking.py +2 -2
- pyxcp/daq_stim/scheduler.cpp +8 -8
- pyxcp/errormatrix.py +2 -2
- pyxcp/examples/run_daq.py +5 -4
- pyxcp/examples/xcp_policy.py +6 -6
- pyxcp/examples/xcp_read_benchmark.py +2 -2
- pyxcp/examples/xcp_skel.py +1 -2
- pyxcp/examples/xcp_unlock.py +10 -12
- pyxcp/examples/xcp_user_supplied_driver.py +1 -2
- pyxcp/examples/xcphello.py +2 -15
- pyxcp/examples/xcphello_recorder.py +2 -2
- pyxcp/master/__init__.py +1 -0
- pyxcp/master/errorhandler.py +134 -4
- pyxcp/master/master.py +823 -252
- pyxcp/recorder/.idea/.gitignore +8 -0
- pyxcp/recorder/.idea/misc.xml +4 -0
- pyxcp/recorder/.idea/modules.xml +8 -0
- pyxcp/recorder/.idea/recorder.iml +6 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +7 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/index.pb +7 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/index.pb +7 -0
- pyxcp/recorder/.idea/vcs.xml +10 -0
- pyxcp/recorder/__init__.py +96 -98
- pyxcp/recorder/converter/__init__.py +4 -10
- pyxcp/recorder/reader.hpp +138 -139
- pyxcp/recorder/reco.py +1 -0
- pyxcp/recorder/rekorder.cpython-310-darwin.so +0 -0
- pyxcp/recorder/rekorder.cpython-311-darwin.so +0 -0
- pyxcp/recorder/rekorder.cpython-312-darwin.so +0 -0
- pyxcp/recorder/rekorder.cpython-313-darwin.so +0 -0
- pyxcp/recorder/rekorder.hpp +274 -274
- pyxcp/recorder/unfolder.hpp +1354 -1319
- pyxcp/recorder/wrap.cpp +184 -183
- pyxcp/recorder/writer.hpp +302 -302
- pyxcp/scripts/xcp_daq_recorder.py +54 -0
- pyxcp/scripts/xcp_fetch_a2l.py +2 -2
- pyxcp/scripts/xcp_id_scanner.py +1 -2
- pyxcp/scripts/xcp_info.py +66 -51
- pyxcp/scripts/xcp_profile.py +1 -2
- pyxcp/tests/test_daq.py +1 -1
- pyxcp/tests/test_framing.py +262 -0
- pyxcp/tests/test_master.py +210 -100
- pyxcp/tests/test_transport.py +138 -42
- pyxcp/timing.py +1 -1
- pyxcp/transport/__init__.py +8 -5
- pyxcp/transport/base.py +70 -180
- pyxcp/transport/can.py +58 -7
- pyxcp/transport/eth.py +32 -15
- pyxcp/transport/hdf5_policy.py +167 -0
- pyxcp/transport/sxi.py +126 -52
- pyxcp/transport/transport_ext.cpython-310-darwin.so +0 -0
- pyxcp/transport/transport_ext.cpython-311-darwin.so +0 -0
- pyxcp/transport/transport_ext.cpython-312-darwin.so +0 -0
- pyxcp/transport/transport_ext.cpython-313-darwin.so +0 -0
- pyxcp/transport/transport_ext.hpp +214 -0
- pyxcp/transport/transport_wrapper.cpp +249 -0
- pyxcp/transport/usb_transport.py +47 -31
- pyxcp/types.py +0 -13
- pyxcp/{utils.py → utils/__init__.py} +1 -2
- pyxcp/utils/cli.py +78 -0
- {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info}/METADATA +4 -2
- pyxcp-0.25.7.dist-info/RECORD +158 -0
- {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info}/WHEEL +1 -1
- pyxcp/examples/conf_sxi.json +0 -9
- pyxcp/examples/conf_sxi.toml +0 -7
- pyxcp-0.23.8.dist-info/RECORD +0 -135
- {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info}/entry_points.txt +0 -0
- {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info/licenses}/LICENSE +0 -0
pyxcp/master/master.py
CHANGED
|
@@ -6,12 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
.. [1] XCP Specification, Part 2 - Protocol Layer Specification
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
import functools
|
|
10
11
|
import logging
|
|
11
12
|
import struct
|
|
12
13
|
import traceback
|
|
13
14
|
import warnings
|
|
14
|
-
from
|
|
15
|
+
from contextlib import suppress
|
|
16
|
+
from typing import Any, Callable, Collection, TypeVar
|
|
17
|
+
|
|
18
|
+
from pyxcp.daq_stim.stim import DaqEventInfo, Stim
|
|
15
19
|
|
|
16
20
|
from pyxcp import checksum, types
|
|
17
21
|
from pyxcp.constants import (
|
|
@@ -24,7 +28,6 @@ from pyxcp.constants import (
|
|
|
24
28
|
makeWordPacker,
|
|
25
29
|
makeWordUnpacker,
|
|
26
30
|
)
|
|
27
|
-
from pyxcp.daq_stim.stim import DaqEventInfo, Stim
|
|
28
31
|
from pyxcp.master.errorhandler import (
|
|
29
32
|
SystemExit,
|
|
30
33
|
disable_error_handling,
|
|
@@ -35,6 +38,10 @@ from pyxcp.master.errorhandler import (
|
|
|
35
38
|
from pyxcp.transport.base import create_transport
|
|
36
39
|
from pyxcp.utils import decode_bytes, delay, short_sleep
|
|
37
40
|
|
|
41
|
+
# Type variables for better type hinting
|
|
42
|
+
T = TypeVar("T")
|
|
43
|
+
R = TypeVar("R")
|
|
44
|
+
|
|
38
45
|
|
|
39
46
|
def broadcasted(func: Callable):
|
|
40
47
|
""""""
|
|
@@ -42,107 +49,215 @@ def broadcasted(func: Callable):
|
|
|
42
49
|
|
|
43
50
|
|
|
44
51
|
class SlaveProperties(dict):
|
|
45
|
-
"""Container class for fixed parameters, like byte-order, maxCTO, ...
|
|
52
|
+
"""Container class for fixed parameters, like byte-order, maxCTO, ...
|
|
53
|
+
|
|
54
|
+
This class extends dict to provide attribute-style access to dictionary items.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, *args: Any, **kws: Any) -> None:
|
|
58
|
+
"""Initialize a new SlaveProperties instance.
|
|
46
59
|
|
|
47
|
-
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
*args : Any
|
|
63
|
+
Positional arguments passed to dict.__init__
|
|
64
|
+
**kws : Any
|
|
65
|
+
Keyword arguments passed to dict.__init__
|
|
66
|
+
"""
|
|
48
67
|
super().__init__(*args, **kws)
|
|
49
68
|
|
|
50
|
-
def __getattr__(self, name):
|
|
69
|
+
def __getattr__(self, name: str) -> Any:
|
|
70
|
+
"""Get an attribute by name.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
name : str
|
|
75
|
+
The name of the attribute to get
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
Any
|
|
80
|
+
The value of the attribute
|
|
81
|
+
"""
|
|
51
82
|
return self[name]
|
|
52
83
|
|
|
53
|
-
def __setattr__(self, name, value):
|
|
84
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
85
|
+
"""Set an attribute by name.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
name : str
|
|
90
|
+
The name of the attribute to set
|
|
91
|
+
value : Any
|
|
92
|
+
The value to set
|
|
93
|
+
"""
|
|
54
94
|
self[name] = value
|
|
55
95
|
|
|
56
|
-
def __getstate__(self):
|
|
96
|
+
def __getstate__(self) -> dict:
|
|
97
|
+
"""Get the state of the object for pickling.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
dict
|
|
102
|
+
The state of the object
|
|
103
|
+
"""
|
|
57
104
|
return self
|
|
58
105
|
|
|
59
|
-
def __setstate__(self, state):
|
|
60
|
-
|
|
106
|
+
def __setstate__(self, state: dict) -> None:
|
|
107
|
+
"""Set the state of the object from unpickling.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
state : dict
|
|
112
|
+
The state to set
|
|
113
|
+
"""
|
|
114
|
+
self.update(state) # Use update instead of direct assignment
|
|
61
115
|
|
|
62
116
|
|
|
63
117
|
class Master:
|
|
64
118
|
"""Common part of lowlevel XCP API.
|
|
65
119
|
|
|
120
|
+
This class provides methods for interacting with an XCP slave device.
|
|
121
|
+
It handles the communication protocol and provides a high-level API
|
|
122
|
+
for sending commands and receiving responses.
|
|
123
|
+
|
|
66
124
|
Parameters
|
|
67
125
|
----------
|
|
68
|
-
transport_name : str
|
|
126
|
+
transport_name : str | None
|
|
69
127
|
XCP transport layer name ['can', 'eth', 'sxi']
|
|
70
|
-
config:
|
|
128
|
+
config : Any
|
|
129
|
+
Configuration object containing transport and general settings
|
|
130
|
+
policy : Any, optional
|
|
131
|
+
Policy object for handling frames, by default None
|
|
132
|
+
transport_layer_interface : Any, optional
|
|
133
|
+
Custom transport layer interface, by default None
|
|
71
134
|
"""
|
|
72
135
|
|
|
73
|
-
def __init__(self, transport_name:
|
|
136
|
+
def __init__(self, transport_name: str | None, config: Any, policy: Any = None, transport_layer_interface: Any = None) -> None:
|
|
137
|
+
"""Initialize a new Master instance.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
transport_name : str | None
|
|
142
|
+
XCP transport layer name ['can', 'eth', 'sxi']
|
|
143
|
+
config : Any
|
|
144
|
+
Configuration object containing transport and general settings
|
|
145
|
+
policy : Any, optional
|
|
146
|
+
Policy object for handling frames, by default None
|
|
147
|
+
transport_layer_interface : Any, optional
|
|
148
|
+
Custom transport layer interface, by default None
|
|
149
|
+
|
|
150
|
+
Raises
|
|
151
|
+
------
|
|
152
|
+
ValueError
|
|
153
|
+
If transport_name is None
|
|
154
|
+
"""
|
|
74
155
|
if transport_name is None:
|
|
75
156
|
raise ValueError("No transport-layer selected") # Never reached -- to keep type-checkers happy.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
self.
|
|
79
|
-
self.
|
|
157
|
+
|
|
158
|
+
# Initialize basic properties
|
|
159
|
+
self.ctr: int = 0
|
|
160
|
+
self.succeeded: bool = True
|
|
161
|
+
self.config: Any = config.general
|
|
162
|
+
self.logger: logging.Logger = logging.getLogger("PyXCP")
|
|
163
|
+
|
|
164
|
+
# Configure error handling
|
|
80
165
|
disable_error_handling(self.config.disable_error_handling)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self.
|
|
84
|
-
|
|
166
|
+
|
|
167
|
+
# Set up transport layer
|
|
168
|
+
self.transport_name: str = transport_name.lower()
|
|
169
|
+
transport_config: Any = config.transport
|
|
170
|
+
self.transport: BaseTransport = create_transport(transport_name, transport_config, policy, transport_layer_interface)
|
|
171
|
+
|
|
172
|
+
# Set up STIM (stimulation) support
|
|
173
|
+
self.stim: Stim = Stim(self.config.stim_support)
|
|
85
174
|
self.stim.clear()
|
|
86
175
|
self.stim.set_policy_feeder(self.transport.policy.feed)
|
|
87
176
|
self.stim.set_frame_sender(self.transport.block_request)
|
|
88
177
|
|
|
89
178
|
# In some cases the transport-layer needs to communicate with us.
|
|
90
179
|
self.transport.parent = self
|
|
91
|
-
self.service = None
|
|
180
|
+
self.service: Any = None
|
|
92
181
|
|
|
93
|
-
# Policies may issue XCP commands on
|
|
182
|
+
# Policies may issue XCP commands on their own.
|
|
94
183
|
self.transport.policy.xcp_master = self
|
|
95
184
|
|
|
96
185
|
# (D)Word (un-)packers are byte-order dependent
|
|
97
186
|
# -- byte-order is returned by CONNECT_Resp (COMM_MODE_BASIC)
|
|
98
|
-
self.BYTE_pack = None
|
|
99
|
-
self.BYTE_unpack = None
|
|
100
|
-
self.WORD_pack = None
|
|
101
|
-
self.WORD_unpack = None
|
|
102
|
-
self.DWORD_pack = None
|
|
103
|
-
self.DWORD_unpack = None
|
|
104
|
-
self.DLONG_pack = None
|
|
105
|
-
self.DLONG_unpack = None
|
|
106
|
-
self.AG_pack = None
|
|
107
|
-
self.AG_unpack = None
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
self.
|
|
111
|
-
self.
|
|
112
|
-
self.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
self.
|
|
116
|
-
self.
|
|
187
|
+
self.BYTE_pack: Callable[[int], bytes] | None = None
|
|
188
|
+
self.BYTE_unpack: Callable[[bytes], tuple[int]] | None = None
|
|
189
|
+
self.WORD_pack: Callable[[int], bytes] | None = None
|
|
190
|
+
self.WORD_unpack: Callable[[bytes], tuple[int]] | None = None
|
|
191
|
+
self.DWORD_pack: Callable[[int], bytes] | None = None
|
|
192
|
+
self.DWORD_unpack: Callable[[bytes], tuple[int]] | None = None
|
|
193
|
+
self.DLONG_pack: Callable[[int], bytes] | None = None
|
|
194
|
+
self.DLONG_unpack: Callable[[bytes], tuple[int]] | None = None
|
|
195
|
+
self.AG_pack: Callable[[int], bytes] | None = None
|
|
196
|
+
self.AG_unpack: Callable[[bytes], tuple[int]] | None = None
|
|
197
|
+
|
|
198
|
+
# Initialize state variables
|
|
199
|
+
self.mta: types.MtaType = types.MtaType(None, None)
|
|
200
|
+
self.currentDaqPtr: Any = None
|
|
201
|
+
self.currentProtectionStatus: dict[str, bool] | None = None
|
|
202
|
+
|
|
203
|
+
# Configuration for seed and key
|
|
204
|
+
self.seed_n_key_dll: str | None = self.config.seed_n_key_dll
|
|
205
|
+
self.seed_n_key_function: Callable | None = self.config.seed_n_key_function
|
|
206
|
+
self.seed_n_key_dll_same_bit_width: bool = self.config.seed_n_key_dll_same_bit_width
|
|
207
|
+
self.disconnect_response_optional: bool = self.config.disconnect_response_optional
|
|
208
|
+
|
|
209
|
+
# Initialize slave properties
|
|
210
|
+
self.slaveProperties: SlaveProperties = SlaveProperties()
|
|
117
211
|
self.slaveProperties.pgmProcessor = SlaveProperties()
|
|
118
212
|
self.slaveProperties.transport_layer = self.transport_name.upper()
|
|
119
213
|
|
|
120
214
|
def __enter__(self):
|
|
121
|
-
"""Context manager entry part.
|
|
215
|
+
"""Context manager entry part.
|
|
216
|
+
|
|
217
|
+
This method is called when entering a context manager block.
|
|
218
|
+
It connects to the XCP slave and returns the Master instance.
|
|
219
|
+
|
|
220
|
+
Returns
|
|
221
|
+
-------
|
|
222
|
+
Master
|
|
223
|
+
The Master instance
|
|
224
|
+
"""
|
|
122
225
|
self.transport.connect()
|
|
123
226
|
return self
|
|
124
227
|
|
|
125
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
126
|
-
"""Context manager exit part.
|
|
127
|
-
|
|
128
|
-
|
|
228
|
+
def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb) -> None:
|
|
229
|
+
"""Context manager exit part.
|
|
230
|
+
|
|
231
|
+
This method is called when exiting a context manager block.
|
|
232
|
+
It closes the connection to the XCP slave and logs any exceptions.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
exc_type : type[BaseException] | None
|
|
237
|
+
The type of the exception that was raised, or None if no exception was raised
|
|
238
|
+
exc_val : BaseException | None
|
|
239
|
+
The exception instance that was raised, or None if no exception was raised
|
|
240
|
+
exc_tb : traceback.TracebackType | None
|
|
241
|
+
The traceback of the exception that was raised, or None if no exception was raised
|
|
242
|
+
"""
|
|
243
|
+
# Close the connection to the XCP slave
|
|
129
244
|
self.close()
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
245
|
+
|
|
246
|
+
# Handle any exceptions that were raised
|
|
247
|
+
if exc_type is not None:
|
|
133
248
|
self.succeeded = False
|
|
134
|
-
# print("=" * 79)
|
|
135
|
-
# print("Exception while in Context-Manager:\n")
|
|
136
249
|
self.logger.error("".join(traceback.format_exception(exc_type, exc_val, exc_tb)))
|
|
137
|
-
# print("=" * 79)
|
|
138
|
-
# return True
|
|
139
250
|
|
|
140
|
-
def _setService(self, service):
|
|
141
|
-
"""
|
|
251
|
+
def _setService(self, service: Any) -> None:
|
|
252
|
+
"""Record the currently processed service.
|
|
253
|
+
|
|
254
|
+
This method is called by the transport layer to record the
|
|
255
|
+
currently processed service.
|
|
142
256
|
|
|
143
257
|
Parameters
|
|
144
258
|
----------
|
|
145
|
-
service:
|
|
259
|
+
service : Any
|
|
260
|
+
The service being processed, typically a `pyxcp.types.Command`
|
|
146
261
|
|
|
147
262
|
Note
|
|
148
263
|
----
|
|
@@ -150,66 +265,122 @@ class Master:
|
|
|
150
265
|
"""
|
|
151
266
|
self.service = service
|
|
152
267
|
|
|
153
|
-
def close(self):
|
|
154
|
-
"""
|
|
268
|
+
def close(self) -> None:
|
|
269
|
+
"""Close the transport layer connection.
|
|
270
|
+
|
|
271
|
+
This method finalizes the policy and closes the transport layer connection.
|
|
272
|
+
It should be called when the Master instance is no longer needed.
|
|
273
|
+
"""
|
|
155
274
|
self.transport.policy.finalize()
|
|
156
275
|
self.transport.close()
|
|
157
276
|
|
|
158
277
|
# Mandatory Commands.
|
|
159
278
|
@wrapped
|
|
160
|
-
def connect(self, mode=0x00):
|
|
279
|
+
def connect(self, mode: int = 0x00) -> types.ConnectResponse:
|
|
161
280
|
"""Build up connection to an XCP slave.
|
|
162
281
|
|
|
163
282
|
Before the actual XCP traffic starts a connection is required.
|
|
283
|
+
This method sends a CONNECT command to the slave and processes
|
|
284
|
+
the response to set up various properties of the slave.
|
|
164
285
|
|
|
165
286
|
Parameters
|
|
166
287
|
----------
|
|
167
|
-
mode : int
|
|
168
|
-
|
|
288
|
+
mode : int, optional
|
|
289
|
+
Connection mode, by default 0x00 (normal mode)
|
|
169
290
|
|
|
170
291
|
Returns
|
|
171
292
|
-------
|
|
172
|
-
|
|
173
|
-
|
|
293
|
+
types.ConnectResponse
|
|
294
|
+
Response object containing fundamental client properties
|
|
174
295
|
|
|
175
296
|
Note
|
|
176
297
|
----
|
|
177
298
|
Every XCP slave supports at most one connection,
|
|
178
299
|
more attempts to connect are silently ignored.
|
|
179
|
-
|
|
180
300
|
"""
|
|
301
|
+
# Send CONNECT command to the slave
|
|
181
302
|
response = self.transport.request(types.Command.CONNECT, mode & 0xFF)
|
|
182
303
|
|
|
183
|
-
# First get byte-order
|
|
184
|
-
|
|
185
|
-
|
|
304
|
+
# First get byte-order from partial response
|
|
305
|
+
result_partial = types.ConnectResponsePartial.parse(response)
|
|
306
|
+
byte_order = result_partial.commModeBasic.byteOrder
|
|
307
|
+
|
|
308
|
+
# Parse the full response with the correct byte order
|
|
309
|
+
result = types.ConnectResponse.parse(response, byteOrder=byte_order)
|
|
310
|
+
|
|
311
|
+
# Set up byte order dependent properties
|
|
312
|
+
self._setup_slave_properties(result, byte_order)
|
|
313
|
+
|
|
314
|
+
# Set up byte order dependent packers and unpackers
|
|
315
|
+
self._setup_packers_and_unpackers(byte_order)
|
|
316
|
+
|
|
317
|
+
# Set up address granularity dependent properties
|
|
318
|
+
self._setup_address_granularity()
|
|
186
319
|
|
|
187
|
-
result
|
|
188
|
-
|
|
189
|
-
|
|
320
|
+
return result
|
|
321
|
+
|
|
322
|
+
def _setup_slave_properties(self, result: types.ConnectResponse, byte_order: types.ByteOrder) -> None:
|
|
323
|
+
"""Set up slave properties based on the connect response.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
result : types.ConnectResponse
|
|
328
|
+
The parsed connect response
|
|
329
|
+
byte_order : types.ByteOrder
|
|
330
|
+
The byte order reported by the slave
|
|
331
|
+
"""
|
|
332
|
+
# Set basic properties
|
|
333
|
+
self.slaveProperties.byteOrder = byte_order
|
|
190
334
|
self.slaveProperties.maxCto = result.maxCto
|
|
191
335
|
self.slaveProperties.maxDto = result.maxDto
|
|
336
|
+
|
|
337
|
+
# Set resource support flags
|
|
192
338
|
self.slaveProperties.supportsPgm = result.resource.pgm
|
|
193
339
|
self.slaveProperties.supportsStim = result.resource.stim
|
|
194
340
|
self.slaveProperties.supportsDaq = result.resource.daq
|
|
195
341
|
self.slaveProperties.supportsCalpag = result.resource.calpag
|
|
342
|
+
|
|
343
|
+
# Set communication mode properties
|
|
196
344
|
self.slaveProperties.slaveBlockMode = result.commModeBasic.slaveBlockMode
|
|
197
345
|
self.slaveProperties.addressGranularity = result.commModeBasic.addressGranularity
|
|
346
|
+
self.slaveProperties.optionalCommMode = result.commModeBasic.optional
|
|
347
|
+
|
|
348
|
+
# Set version information
|
|
198
349
|
self.slaveProperties.protocolLayerVersion = result.protocolLayerVersion
|
|
199
350
|
self.slaveProperties.transportLayerVersion = result.transportLayerVersion
|
|
200
|
-
|
|
351
|
+
|
|
352
|
+
# Calculate derived properties
|
|
201
353
|
self.slaveProperties.maxWriteDaqMultipleElements = (
|
|
202
354
|
0 if self.slaveProperties.maxCto < 10 else int((self.slaveProperties.maxCto - 2) // 8)
|
|
203
355
|
)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
self.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
356
|
+
|
|
357
|
+
# Initialize bytesPerElement (will be set in _setup_address_granularity)
|
|
358
|
+
self.slaveProperties.bytesPerElement = None
|
|
359
|
+
|
|
360
|
+
def _setup_packers_and_unpackers(self, byte_order: types.ByteOrder) -> None:
|
|
361
|
+
"""Set up byte order dependent packers and unpackers.
|
|
362
|
+
|
|
363
|
+
Parameters
|
|
364
|
+
----------
|
|
365
|
+
byte_order : types.ByteOrder
|
|
366
|
+
The byte order reported by the slave
|
|
367
|
+
"""
|
|
368
|
+
# Determine byte order prefix for struct format strings
|
|
369
|
+
byte_order_prefix = "<" if byte_order == types.ByteOrder.INTEL else ">"
|
|
370
|
+
|
|
371
|
+
# Create packers and unpackers for different data types
|
|
372
|
+
self.BYTE_pack = makeBytePacker(byte_order_prefix)
|
|
373
|
+
self.BYTE_unpack = makeByteUnpacker(byte_order_prefix)
|
|
374
|
+
self.WORD_pack = makeWordPacker(byte_order_prefix)
|
|
375
|
+
self.WORD_unpack = makeWordUnpacker(byte_order_prefix)
|
|
376
|
+
self.DWORD_pack = makeDWordPacker(byte_order_prefix)
|
|
377
|
+
self.DWORD_unpack = makeDWordUnpacker(byte_order_prefix)
|
|
378
|
+
self.DLONG_pack = makeDLongPacker(byte_order_prefix)
|
|
379
|
+
self.DLONG_unpack = makeDLongUnpacker(byte_order_prefix)
|
|
380
|
+
|
|
381
|
+
def _setup_address_granularity(self) -> None:
|
|
382
|
+
"""Set up address granularity dependent properties and packers/unpackers."""
|
|
383
|
+
# Set up address granularity dependent packers and unpackers
|
|
213
384
|
if self.slaveProperties.addressGranularity == types.AddressGranularity.BYTE:
|
|
214
385
|
self.AG_pack = struct.Struct("<B").pack
|
|
215
386
|
self.AG_unpack = struct.Struct("<B").unpack
|
|
@@ -223,15 +394,23 @@ class Master:
|
|
|
223
394
|
self.AG_unpack = self.DWORD_unpack
|
|
224
395
|
self.slaveProperties.bytesPerElement = 4
|
|
225
396
|
# self.connected = True
|
|
226
|
-
|
|
397
|
+
status = self.getStatus()
|
|
398
|
+
if status.sessionStatus.daqRunning:
|
|
399
|
+
# TODO: resume
|
|
400
|
+
self.startStopSynch(0x00)
|
|
227
401
|
|
|
228
402
|
@wrapped
|
|
229
|
-
def disconnect(self):
|
|
230
|
-
"""
|
|
403
|
+
def disconnect(self) -> bytes:
|
|
404
|
+
"""Release the connection to the XCP slave.
|
|
231
405
|
|
|
232
|
-
|
|
233
|
-
|
|
406
|
+
This method sends a DISCONNECT command to the slave, which releases
|
|
407
|
+
the connection. Thereafter, no further communication with the slave
|
|
408
|
+
is possible (besides `connect`).
|
|
234
409
|
|
|
410
|
+
Returns
|
|
411
|
+
-------
|
|
412
|
+
bytes
|
|
413
|
+
The raw response from the slave, typically empty
|
|
235
414
|
|
|
236
415
|
Note
|
|
237
416
|
-----
|
|
@@ -241,58 +420,106 @@ class Master:
|
|
|
241
420
|
- `"DISCONNECT_RESPONSE_OPTIONAL": true` (JSON)
|
|
242
421
|
to your configuration file.
|
|
243
422
|
"""
|
|
423
|
+
# Send DISCONNECT command to the slave
|
|
244
424
|
if self.disconnect_response_optional:
|
|
245
425
|
response = self.transport.request_optional_response(types.Command.DISCONNECT)
|
|
246
426
|
else:
|
|
247
427
|
response = self.transport.request(types.Command.DISCONNECT)
|
|
248
|
-
|
|
428
|
+
|
|
249
429
|
return response
|
|
250
430
|
|
|
251
431
|
@wrapped
|
|
252
|
-
def getStatus(self):
|
|
432
|
+
def getStatus(self) -> types.GetStatusResponse:
|
|
253
433
|
"""Get current status information of the slave device.
|
|
254
434
|
|
|
435
|
+
This method sends a GET_STATUS command to the slave and processes
|
|
436
|
+
the response to get information about the current status of the slave.
|
|
255
437
|
This includes the status of the resource protection, pending store
|
|
256
438
|
requests and the general status of data acquisition and stimulation.
|
|
257
439
|
|
|
258
440
|
Returns
|
|
259
441
|
-------
|
|
260
|
-
|
|
442
|
+
types.GetStatusResponse
|
|
443
|
+
Response object containing status information
|
|
261
444
|
"""
|
|
445
|
+
# Send GET_STATUS command to the slave
|
|
262
446
|
response = self.transport.request(types.Command.GET_STATUS)
|
|
447
|
+
|
|
448
|
+
# Parse the response with the correct byte order
|
|
263
449
|
result = types.GetStatusResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
|
|
450
|
+
|
|
451
|
+
# Update the current protection status
|
|
264
452
|
self._setProtectionStatus(result.resourceProtectionStatus)
|
|
453
|
+
|
|
265
454
|
return result
|
|
266
455
|
|
|
267
456
|
@wrapped
|
|
268
|
-
def synch(self):
|
|
269
|
-
"""Synchronize command execution after timeout conditions.
|
|
457
|
+
def synch(self) -> bytes:
|
|
458
|
+
"""Synchronize command execution after timeout conditions.
|
|
459
|
+
|
|
460
|
+
This method sends a SYNCH command to the slave, which synchronizes
|
|
461
|
+
command execution after timeout conditions. This is useful when
|
|
462
|
+
the slave has timed out and needs to be resynchronized.
|
|
463
|
+
|
|
464
|
+
Returns
|
|
465
|
+
-------
|
|
466
|
+
bytes
|
|
467
|
+
The raw response from the slave
|
|
468
|
+
"""
|
|
469
|
+
# Send SYNCH command to the slave
|
|
270
470
|
response = self.transport.request(types.Command.SYNCH)
|
|
271
471
|
return response
|
|
272
472
|
|
|
273
473
|
@wrapped
|
|
274
|
-
def getCommModeInfo(self):
|
|
474
|
+
def getCommModeInfo(self) -> types.GetCommModeInfoResponse:
|
|
275
475
|
"""Get optional information on different Communication Modes supported
|
|
276
476
|
by the slave.
|
|
277
477
|
|
|
478
|
+
This method sends a GET_COMM_MODE_INFO command to the slave and processes
|
|
479
|
+
the response to get information about the communication modes supported
|
|
480
|
+
by the slave.
|
|
481
|
+
|
|
278
482
|
Returns
|
|
279
483
|
-------
|
|
280
|
-
|
|
484
|
+
types.GetCommModeInfoResponse
|
|
485
|
+
Response object containing communication mode information
|
|
281
486
|
"""
|
|
487
|
+
# Send GET_COMM_MODE_INFO command to the slave
|
|
282
488
|
response = self.transport.request(types.Command.GET_COMM_MODE_INFO)
|
|
489
|
+
|
|
490
|
+
# Parse the response with the correct byte order
|
|
283
491
|
result = types.GetCommModeInfoResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
|
|
492
|
+
|
|
493
|
+
# Update slave properties with communication mode information
|
|
494
|
+
self._update_comm_mode_properties(result)
|
|
495
|
+
|
|
496
|
+
return result
|
|
497
|
+
|
|
498
|
+
def _update_comm_mode_properties(self, result: types.GetCommModeInfoResponse) -> None:
|
|
499
|
+
"""Update slave properties with communication mode information.
|
|
500
|
+
|
|
501
|
+
Parameters
|
|
502
|
+
----------
|
|
503
|
+
result : types.GetCommModeInfoResponse
|
|
504
|
+
The parsed GET_COMM_MODE_INFO response
|
|
505
|
+
"""
|
|
506
|
+
# Set optional communication mode properties
|
|
284
507
|
self.slaveProperties.interleavedMode = result.commModeOptional.interleavedMode
|
|
285
508
|
self.slaveProperties.masterBlockMode = result.commModeOptional.masterBlockMode
|
|
509
|
+
|
|
510
|
+
# Set basic communication properties
|
|
286
511
|
self.slaveProperties.maxBs = result.maxBs
|
|
287
512
|
self.slaveProperties.minSt = result.minSt
|
|
288
513
|
self.slaveProperties.queueSize = result.queueSize
|
|
289
514
|
self.slaveProperties.xcpDriverVersionNumber = result.xcpDriverVersionNumber
|
|
290
|
-
return result
|
|
291
515
|
|
|
292
516
|
@wrapped
|
|
293
|
-
def getId(self, mode: int):
|
|
294
|
-
"""
|
|
295
|
-
|
|
517
|
+
def getId(self, mode: int) -> types.GetIDResponse:
|
|
518
|
+
"""Get identification information from the slave device.
|
|
519
|
+
|
|
520
|
+
This command is used for automatic session configuration and for
|
|
521
|
+
slave device identification. It sends a GET_ID command to the slave
|
|
522
|
+
and processes the response to get identification information.
|
|
296
523
|
|
|
297
524
|
Parameters
|
|
298
525
|
----------
|
|
@@ -307,16 +534,23 @@ class Master:
|
|
|
307
534
|
|
|
308
535
|
Returns
|
|
309
536
|
-------
|
|
310
|
-
|
|
537
|
+
types.GetIDResponse
|
|
538
|
+
Response object containing identification information
|
|
311
539
|
"""
|
|
540
|
+
# Send GET_ID command to the slave
|
|
312
541
|
response = self.transport.request(types.Command.GET_ID, mode)
|
|
313
542
|
result = types.GetIDResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
|
|
314
543
|
result.length = self.DWORD_unpack(response[3:7])[0]
|
|
544
|
+
|
|
315
545
|
return result
|
|
316
546
|
|
|
317
547
|
@wrapped
|
|
318
|
-
def setRequest(self, mode: int, session_configuration_id: int):
|
|
319
|
-
"""Request to save to non-volatile memory.
|
|
548
|
+
def setRequest(self, mode: int, session_configuration_id: int) -> bytes:
|
|
549
|
+
"""Request to save data to non-volatile memory.
|
|
550
|
+
|
|
551
|
+
This method sends a SET_REQUEST command to the slave, which requests
|
|
552
|
+
the slave to save data to non-volatile memory. The data to be saved
|
|
553
|
+
is specified by the mode parameter.
|
|
320
554
|
|
|
321
555
|
Parameters
|
|
322
556
|
----------
|
|
@@ -325,42 +559,68 @@ class Master:
|
|
|
325
559
|
- 2 Request to store DAQ list, no resume
|
|
326
560
|
- 4 Request to store DAQ list, resume enabled
|
|
327
561
|
- 8 Request to clear DAQ configuration
|
|
328
|
-
|
|
562
|
+
session_configuration_id : int
|
|
563
|
+
Identifier for the session configuration
|
|
329
564
|
|
|
565
|
+
Returns
|
|
566
|
+
-------
|
|
567
|
+
bytes
|
|
568
|
+
The raw response from the slave
|
|
330
569
|
"""
|
|
570
|
+
# Send SET_REQUEST command to the slave
|
|
571
|
+
# Split the session_configuration_id into high and low bytes
|
|
331
572
|
return self.transport.request(
|
|
332
573
|
types.Command.SET_REQUEST,
|
|
333
574
|
mode,
|
|
334
|
-
session_configuration_id >> 8,
|
|
335
|
-
session_configuration_id & 0xFF,
|
|
575
|
+
session_configuration_id >> 8, # High byte
|
|
576
|
+
session_configuration_id & 0xFF, # Low byte
|
|
336
577
|
)
|
|
337
578
|
|
|
338
579
|
@wrapped
|
|
339
|
-
def getSeed(self, first: int, resource: int):
|
|
580
|
+
def getSeed(self, first: int, resource: int) -> types.GetSeedResponse:
|
|
340
581
|
"""Get seed from slave for unlocking a protected resource.
|
|
341
582
|
|
|
583
|
+
This method sends a GET_SEED command to the slave, which returns a seed
|
|
584
|
+
that can be used to generate a key for unlocking a protected resource.
|
|
585
|
+
The seed is used as input to a key generation algorithm, and the resulting
|
|
586
|
+
key is sent back to the slave using the unlock method.
|
|
587
|
+
|
|
342
588
|
Parameters
|
|
343
589
|
----------
|
|
344
590
|
first : int
|
|
345
591
|
- 0 - first part of seed
|
|
346
592
|
- 1 - remaining part
|
|
347
593
|
resource : int
|
|
348
|
-
- Mode
|
|
594
|
+
- Mode == 0 - Resource to unlock
|
|
349
595
|
- Mode == 1 - Don't care
|
|
350
596
|
|
|
351
597
|
Returns
|
|
352
598
|
-------
|
|
353
|
-
|
|
599
|
+
types.GetSeedResponse
|
|
600
|
+
Response object containing the seed
|
|
601
|
+
|
|
602
|
+
Note
|
|
603
|
+
----
|
|
604
|
+
For CAN transport, the seed may be split across multiple frames if it's
|
|
605
|
+
longer than the maximum DLC. In this case, the first byte of the response
|
|
606
|
+
indicates the remaining seed size, and the master must call getSeed
|
|
607
|
+
multiple times until the complete seed is received.
|
|
354
608
|
"""
|
|
609
|
+
# Send GET_SEED command to the slave
|
|
610
|
+
response = self.transport.request(types.Command.GET_SEED, first, resource)
|
|
611
|
+
|
|
612
|
+
# Handle CAN-specific seed format
|
|
355
613
|
if self.transport_name == "can":
|
|
356
|
-
#
|
|
357
|
-
#
|
|
358
|
-
# followed by the
|
|
359
|
-
# the master must call getSeed several times until the complete seed is received
|
|
360
|
-
response = self.transport.request(types.Command.GET_SEED, first, resource)
|
|
614
|
+
# For CAN it might happen that the seed is longer than the max DLC
|
|
615
|
+
# In this case the first byte will be the current remaining seed size
|
|
616
|
+
# followed by the seed bytes that can fit in the current frame
|
|
361
617
|
size, seed = response[0], response[1:]
|
|
618
|
+
|
|
619
|
+
# Truncate seed if necessary
|
|
362
620
|
if size < len(seed):
|
|
363
621
|
seed = seed[:size]
|
|
622
|
+
|
|
623
|
+
# Create and populate response object
|
|
364
624
|
reply = types.GetSeedResponse.parse(
|
|
365
625
|
types.GetSeedResponse.build({"length": size, "seed": bytes(size)}),
|
|
366
626
|
byteOrder=self.slaveProperties.byteOrder,
|
|
@@ -368,245 +628,386 @@ class Master:
|
|
|
368
628
|
reply.seed = seed
|
|
369
629
|
return reply
|
|
370
630
|
else:
|
|
371
|
-
|
|
631
|
+
# For other transports, parse the response directly
|
|
372
632
|
return types.GetSeedResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
|
|
373
633
|
|
|
374
634
|
@wrapped
|
|
375
|
-
def unlock(self, length: int, key: bytes):
|
|
635
|
+
def unlock(self, length: int, key: bytes) -> types.ResourceType:
|
|
376
636
|
"""Send key to slave for unlocking a protected resource.
|
|
377
637
|
|
|
638
|
+
This method sends an UNLOCK command to the slave, which attempts to
|
|
639
|
+
unlock a protected resource using the provided key. The key is generated
|
|
640
|
+
from the seed obtained using the getSeed method.
|
|
641
|
+
|
|
378
642
|
Parameters
|
|
379
643
|
----------
|
|
380
644
|
length : int
|
|
381
|
-
|
|
645
|
+
Indicates the (remaining) number of key bytes
|
|
382
646
|
key : bytes
|
|
647
|
+
The key bytes to send to the slave
|
|
383
648
|
|
|
384
649
|
Returns
|
|
385
650
|
-------
|
|
386
|
-
|
|
651
|
+
types.ResourceType
|
|
652
|
+
Response object containing the resource protection status
|
|
387
653
|
|
|
388
654
|
Note
|
|
389
655
|
----
|
|
390
656
|
The master has to use :meth:`unlock` in a defined sequence together
|
|
391
|
-
with :meth:`getSeed`. The master only
|
|
657
|
+
with :meth:`getSeed`. The master can only send an :meth:`unlock` sequence
|
|
392
658
|
if previously there was a :meth:`getSeed` sequence. The master has
|
|
393
659
|
to send the first `unlocking` after a :meth:`getSeed` sequence with
|
|
394
660
|
a Length containing the total length of the key.
|
|
395
661
|
"""
|
|
662
|
+
# Send UNLOCK command to the slave
|
|
396
663
|
response = self.transport.request(types.Command.UNLOCK, length, *key)
|
|
664
|
+
|
|
665
|
+
# Parse the response with the correct byte order
|
|
397
666
|
result = types.ResourceType.parse(response, byteOrder=self.slaveProperties.byteOrder)
|
|
667
|
+
|
|
668
|
+
# Update the current protection status
|
|
398
669
|
self._setProtectionStatus(result)
|
|
670
|
+
|
|
399
671
|
return result
|
|
400
672
|
|
|
401
673
|
@wrapped
|
|
402
|
-
def setMta(self, address: int, address_ext: int = 0x00):
|
|
674
|
+
def setMta(self, address: int, address_ext: int = 0x00) -> bytes:
|
|
403
675
|
"""Set Memory Transfer Address in slave.
|
|
404
676
|
|
|
677
|
+
This method sends a SET_MTA command to the slave, which sets the
|
|
678
|
+
Memory Transfer Address (MTA) to the specified address. The MTA is
|
|
679
|
+
used by various commands that transfer data between the master and
|
|
680
|
+
the slave.
|
|
681
|
+
|
|
405
682
|
Parameters
|
|
406
683
|
----------
|
|
407
684
|
address : int
|
|
408
|
-
|
|
685
|
+
The memory address to set
|
|
686
|
+
address_ext : int, optional
|
|
687
|
+
The address extension, by default 0x00
|
|
688
|
+
|
|
689
|
+
Returns
|
|
690
|
+
-------
|
|
691
|
+
bytes
|
|
692
|
+
The raw response from the slave
|
|
409
693
|
|
|
410
694
|
Note
|
|
411
695
|
----
|
|
412
696
|
The MTA is used by :meth:`buildChecksum`, :meth:`upload`, :meth:`download`, :meth:`downloadNext`,
|
|
413
697
|
:meth:`downloadMax`, :meth:`modifyBits`, :meth:`programClear`, :meth:`program`, :meth:`programNext`
|
|
414
698
|
and :meth:`programMax`.
|
|
415
|
-
|
|
416
699
|
"""
|
|
417
|
-
|
|
700
|
+
# Keep track of MTA (needed for error-handling)
|
|
701
|
+
self.mta = types.MtaType(address, address_ext)
|
|
702
|
+
|
|
703
|
+
# Pack the address into bytes
|
|
418
704
|
addr = self.DWORD_pack(address)
|
|
705
|
+
|
|
706
|
+
# Send SET_MTA command to the slave
|
|
419
707
|
return self.transport.request(types.Command.SET_MTA, 0, 0, address_ext, *addr)
|
|
420
708
|
|
|
421
709
|
@wrapped
|
|
422
|
-
def upload(self, length: int):
|
|
710
|
+
def upload(self, length: int) -> bytes:
|
|
423
711
|
"""Transfer data from slave to master.
|
|
424
712
|
|
|
713
|
+
This method sends an UPLOAD command to the slave, which transfers
|
|
714
|
+
data from the slave to the master. The data is read from the memory
|
|
715
|
+
address specified by the MTA, which must be set before calling this
|
|
716
|
+
method.
|
|
717
|
+
|
|
425
718
|
Parameters
|
|
426
719
|
----------
|
|
427
720
|
length : int
|
|
428
|
-
Number of elements (address granularity)
|
|
429
|
-
|
|
430
|
-
Note
|
|
431
|
-
----
|
|
432
|
-
Adress is set via :meth:`setMta` (Some services like :meth:`getID` also set the MTA).
|
|
721
|
+
Number of elements (address granularity) to upload
|
|
433
722
|
|
|
434
723
|
Returns
|
|
435
724
|
-------
|
|
436
725
|
bytes
|
|
726
|
+
The uploaded data
|
|
727
|
+
|
|
728
|
+
Note
|
|
729
|
+
----
|
|
730
|
+
Address is set via :meth:`setMta` (Some services like :meth:`getID` also set the MTA).
|
|
437
731
|
"""
|
|
732
|
+
# Calculate the number of bytes to upload
|
|
438
733
|
byte_count = length * self.slaveProperties.bytesPerElement
|
|
734
|
+
|
|
735
|
+
# Send UPLOAD command to the slave
|
|
439
736
|
response = self.transport.request(types.Command.UPLOAD, length)
|
|
737
|
+
|
|
738
|
+
# Handle block mode for large uploads
|
|
440
739
|
if byte_count > (self.slaveProperties.maxCto - 1):
|
|
740
|
+
# Receive the remaining bytes in block mode
|
|
441
741
|
block_response = self.transport.block_receive(length_required=(byte_count - len(response)))
|
|
442
742
|
response += block_response
|
|
743
|
+
# Handle CAN-specific upload format
|
|
443
744
|
elif self.transport_name == "can":
|
|
444
|
-
#
|
|
445
|
-
#
|
|
446
|
-
#
|
|
447
|
-
|
|
448
|
-
while
|
|
745
|
+
# Larger sizes will send in multiple CAN messages
|
|
746
|
+
# Each valid message will start with 0xFF followed by the upload bytes
|
|
747
|
+
# The last message might be padded to the required DLC
|
|
748
|
+
remaining_bytes = byte_count - len(response) # NOTE: Due to padding the result may negative!
|
|
749
|
+
while remaining_bytes > 0:
|
|
449
750
|
if len(self.transport.resQueue):
|
|
450
751
|
data = self.transport.resQueue.popleft()
|
|
451
|
-
response += data[1 :
|
|
452
|
-
|
|
752
|
+
response += data[1 : remaining_bytes + 1]
|
|
753
|
+
remaining_bytes = byte_count - len(response)
|
|
453
754
|
else:
|
|
454
755
|
short_sleep()
|
|
455
756
|
return response
|
|
456
757
|
|
|
457
758
|
@wrapped
|
|
458
|
-
def shortUpload(self, length: int, address: int, address_ext: int = 0x00):
|
|
459
|
-
"""Transfer data from slave to master.
|
|
460
|
-
|
|
759
|
+
def shortUpload(self, length: int, address: int, address_ext: int = 0x00) -> bytes:
|
|
760
|
+
"""Transfer data from slave to master with address information.
|
|
761
|
+
|
|
762
|
+
This method sends a SHORT_UPLOAD command to the slave, which transfers
|
|
763
|
+
data from the slave to the master. Unlike the :meth:`upload` method,
|
|
764
|
+
this method includes the address information in the command, so it
|
|
765
|
+
doesn't require setting the MTA first.
|
|
461
766
|
|
|
462
767
|
Parameters
|
|
463
768
|
----------
|
|
464
769
|
length : int
|
|
465
|
-
Number of elements (address granularity)
|
|
770
|
+
Number of elements (address granularity) to upload
|
|
466
771
|
address : int
|
|
467
|
-
|
|
772
|
+
The memory address to read from
|
|
773
|
+
address_ext : int, optional
|
|
774
|
+
The address extension, by default 0x00
|
|
468
775
|
|
|
469
776
|
Returns
|
|
470
777
|
-------
|
|
471
778
|
bytes
|
|
779
|
+
The uploaded data
|
|
472
780
|
"""
|
|
781
|
+
# Pack the address into bytes
|
|
473
782
|
addr = self.DWORD_pack(address)
|
|
783
|
+
|
|
784
|
+
# Calculate the number of bytes to upload
|
|
474
785
|
byte_count = length * self.slaveProperties.bytesPerElement
|
|
475
786
|
max_byte_count = self.slaveProperties.maxCto - 1
|
|
787
|
+
|
|
788
|
+
# Check if the requested byte count exceeds the maximum
|
|
476
789
|
if byte_count > max_byte_count:
|
|
477
790
|
self.logger.warn(f"SHORT_UPLOAD: {byte_count} bytes exceeds the maximum value of {max_byte_count}.")
|
|
791
|
+
|
|
792
|
+
# Send SHORT_UPLOAD command to the slave
|
|
478
793
|
response = self.transport.request(types.Command.SHORT_UPLOAD, length, 0, address_ext, *addr)
|
|
794
|
+
|
|
795
|
+
# Return only the requested number of bytes
|
|
479
796
|
return response[:byte_count]
|
|
480
797
|
|
|
481
798
|
@wrapped
|
|
482
|
-
def buildChecksum(self, blocksize: int):
|
|
799
|
+
def buildChecksum(self, blocksize: int) -> types.BuildChecksumResponse:
|
|
483
800
|
"""Build checksum over memory range.
|
|
484
801
|
|
|
802
|
+
This method sends a BUILD_CHECKSUM command to the slave, which calculates
|
|
803
|
+
a checksum over a memory range. The memory range starts at the address
|
|
804
|
+
specified by the MTA and has a size of `blocksize` elements.
|
|
805
|
+
|
|
485
806
|
Parameters
|
|
486
807
|
----------
|
|
487
808
|
blocksize : int
|
|
809
|
+
The number of elements (address granularity) to include in the checksum
|
|
488
810
|
|
|
489
811
|
Returns
|
|
490
812
|
-------
|
|
491
|
-
|
|
813
|
+
types.BuildChecksumResponse
|
|
814
|
+
Response object containing the checksum information
|
|
492
815
|
|
|
493
|
-
|
|
816
|
+
Note
|
|
817
|
+
----
|
|
818
|
+
Address is set via :meth:`setMta`
|
|
494
819
|
|
|
495
820
|
See Also
|
|
496
821
|
--------
|
|
497
822
|
:mod:`~pyxcp.checksum`
|
|
498
823
|
"""
|
|
824
|
+
# Pack the blocksize into bytes
|
|
499
825
|
bs = self.DWORD_pack(blocksize)
|
|
826
|
+
|
|
827
|
+
# Send BUILD_CHECKSUM command to the slave
|
|
500
828
|
response = self.transport.request(types.Command.BUILD_CHECKSUM, 0, 0, 0, *bs)
|
|
829
|
+
|
|
830
|
+
# Parse the response with the correct byte order
|
|
501
831
|
return types.BuildChecksumResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
|
|
502
832
|
|
|
503
833
|
@wrapped
|
|
504
|
-
def transportLayerCmd(self, sub_command: int, *data:
|
|
834
|
+
def transportLayerCmd(self, sub_command: int, *data: bytes) -> bytes:
|
|
505
835
|
"""Execute transfer-layer specific command.
|
|
506
836
|
|
|
837
|
+
This method sends a TRANSPORT_LAYER_CMD command to the slave, which
|
|
838
|
+
executes a transport-layer specific command. The exact behavior of
|
|
839
|
+
this command depends on the transport layer being used.
|
|
840
|
+
|
|
507
841
|
Parameters
|
|
508
842
|
----------
|
|
509
|
-
|
|
510
|
-
|
|
843
|
+
sub_command : int
|
|
844
|
+
The sub-command to execute
|
|
845
|
+
*data : bytes
|
|
846
|
+
Variable number of data bytes to send with the command
|
|
847
|
+
|
|
848
|
+
Returns
|
|
849
|
+
-------
|
|
850
|
+
bytes
|
|
851
|
+
The raw response from the slave, or None if no response is expected
|
|
511
852
|
|
|
512
853
|
Note
|
|
513
854
|
----
|
|
514
855
|
For details refer to XCP specification.
|
|
515
856
|
"""
|
|
857
|
+
# Send TRANSPORT_LAYER_CMD command to the slave
|
|
516
858
|
return self.transport.request_optional_response(types.Command.TRANSPORT_LAYER_CMD, sub_command, *data)
|
|
517
859
|
|
|
518
860
|
@wrapped
|
|
519
|
-
def userCmd(self, sub_command: int, data: bytes):
|
|
861
|
+
def userCmd(self, sub_command: int, data: bytes) -> bytes:
|
|
520
862
|
"""Execute proprietary command implemented in your XCP client.
|
|
521
863
|
|
|
864
|
+
This method sends a USER_CMD command to the slave, which executes
|
|
865
|
+
a proprietary command implemented in the XCP client. The exact behavior
|
|
866
|
+
of this command depends on the XCP client vendor.
|
|
867
|
+
|
|
522
868
|
Parameters
|
|
523
869
|
----------
|
|
524
|
-
|
|
870
|
+
sub_command : int
|
|
871
|
+
The sub-command to execute
|
|
525
872
|
data : bytes
|
|
873
|
+
The data bytes to send with the command
|
|
526
874
|
|
|
875
|
+
Returns
|
|
876
|
+
-------
|
|
877
|
+
bytes
|
|
878
|
+
The raw response from the slave
|
|
527
879
|
|
|
528
|
-
|
|
880
|
+
Note
|
|
881
|
+
----
|
|
882
|
+
For details refer to your XCP client vendor.
|
|
529
883
|
"""
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
return response
|
|
884
|
+
# Send USER_CMD command to the slave
|
|
885
|
+
return self.transport.request(types.Command.USER_CMD, sub_command, *data)
|
|
533
886
|
|
|
534
887
|
@wrapped
|
|
535
|
-
def getVersion(self):
|
|
536
|
-
"""Get version information.
|
|
888
|
+
def getVersion(self) -> types.GetVersionResponse:
|
|
889
|
+
"""Get version information from the slave.
|
|
537
890
|
|
|
538
|
-
This
|
|
539
|
-
|
|
540
|
-
currently in use.
|
|
891
|
+
This method sends a GET_VERSION command to the slave, which returns
|
|
892
|
+
detailed information about the implemented protocol layer version
|
|
893
|
+
of the XCP slave and the transport layer currently in use.
|
|
541
894
|
|
|
542
895
|
Returns
|
|
543
896
|
-------
|
|
544
|
-
|
|
897
|
+
types.GetVersionResponse
|
|
898
|
+
Response object containing version information
|
|
545
899
|
"""
|
|
546
|
-
|
|
900
|
+
# Send GET_VERSION command to the slave
|
|
547
901
|
response = self.transport.request(types.Command.GET_VERSION)
|
|
902
|
+
|
|
903
|
+
# Parse the response with the correct byte order
|
|
548
904
|
result = types.GetVersionResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
|
|
905
|
+
|
|
906
|
+
# Update slave properties with version information
|
|
907
|
+
self._update_version_properties(result)
|
|
908
|
+
|
|
909
|
+
return result
|
|
910
|
+
|
|
911
|
+
def _update_version_properties(self, result: types.GetVersionResponse) -> None:
|
|
912
|
+
"""Update slave properties with version information.
|
|
913
|
+
|
|
914
|
+
Parameters
|
|
915
|
+
----------
|
|
916
|
+
result : types.GetVersionResponse
|
|
917
|
+
The parsed GET_VERSION response
|
|
918
|
+
"""
|
|
919
|
+
# Set version information
|
|
549
920
|
self.slaveProperties.protocolMajor = result.protocolMajor
|
|
550
921
|
self.slaveProperties.protocolMinor = result.protocolMinor
|
|
551
922
|
self.slaveProperties.transportMajor = result.transportMajor
|
|
552
923
|
self.slaveProperties.transportMinor = result.transportMinor
|
|
553
|
-
return result
|
|
554
924
|
|
|
555
|
-
def fetch(self, length: int, limit_payload: int = None): # TODO: pull
|
|
556
|
-
"""Convenience function for data-transfer from slave to master
|
|
557
|
-
|
|
925
|
+
def fetch(self, length: int, limit_payload: int = None) -> bytes: # TODO: pull
|
|
926
|
+
"""Convenience function for data-transfer from slave to master.
|
|
927
|
+
|
|
928
|
+
This method transfers data from the slave to the master in chunks,
|
|
929
|
+
handling the details of breaking up large transfers into smaller
|
|
930
|
+
pieces. It's not part of the XCP Specification but provides a
|
|
931
|
+
convenient way to fetch data.
|
|
558
932
|
|
|
559
933
|
Parameters
|
|
560
934
|
----------
|
|
561
935
|
length : int
|
|
562
|
-
|
|
563
|
-
|
|
936
|
+
The number of bytes to fetch
|
|
937
|
+
limit_payload : int, optional
|
|
938
|
+
Transfer less bytes than supported by transport-layer, by default None
|
|
564
939
|
|
|
565
940
|
Returns
|
|
566
941
|
-------
|
|
567
942
|
bytes
|
|
943
|
+
The fetched data
|
|
944
|
+
|
|
945
|
+
Raises
|
|
946
|
+
------
|
|
947
|
+
ValueError
|
|
948
|
+
If limit_payload is less than 8 bytes
|
|
568
949
|
|
|
569
950
|
Note
|
|
570
951
|
----
|
|
571
|
-
|
|
952
|
+
Address is not included because of services implicitly setting
|
|
953
|
+
address information like :meth:`getID`.
|
|
572
954
|
"""
|
|
573
|
-
|
|
955
|
+
# Validate limit_payload
|
|
956
|
+
if limit_payload is not None and limit_payload < 8:
|
|
574
957
|
raise ValueError(f"Payload must be at least 8 bytes - given: {limit_payload}")
|
|
575
958
|
|
|
959
|
+
# Determine maximum payload size
|
|
576
960
|
slave_block_mode = self.slaveProperties.slaveBlockMode
|
|
577
|
-
if slave_block_mode
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
max_payload = self.slaveProperties.maxCto - 1
|
|
961
|
+
max_payload = 255 if slave_block_mode else self.slaveProperties.maxCto - 1
|
|
962
|
+
|
|
963
|
+
# Apply limit_payload if specified
|
|
581
964
|
payload = min(limit_payload, max_payload) if limit_payload else max_payload
|
|
965
|
+
|
|
966
|
+
# Calculate number of chunks and remaining bytes
|
|
582
967
|
chunk_size = payload
|
|
583
968
|
chunks = range(length // chunk_size)
|
|
584
969
|
remaining = length % chunk_size
|
|
970
|
+
|
|
971
|
+
# Fetch data in chunks
|
|
585
972
|
result = []
|
|
586
973
|
for _ in chunks:
|
|
587
974
|
data = self.upload(chunk_size)
|
|
588
975
|
result.extend(data[:chunk_size])
|
|
976
|
+
|
|
977
|
+
# Fetch remaining bytes
|
|
589
978
|
if remaining:
|
|
590
979
|
data = self.upload(remaining)
|
|
591
980
|
result.extend(data[:remaining])
|
|
981
|
+
|
|
592
982
|
return bytes(result)
|
|
593
983
|
|
|
594
984
|
pull = fetch # fetch() may be completely replaced by pull() someday.
|
|
595
985
|
|
|
596
|
-
def push(self, address: int, address_ext: int, data: bytes, callback:
|
|
986
|
+
def push(self, address: int, address_ext: int, data: bytes, callback: Callable[[int], None] | None = None) -> None:
|
|
597
987
|
"""Convenience function for data-transfer from master to slave.
|
|
598
|
-
|
|
988
|
+
|
|
989
|
+
This method transfers data from the master to the slave in chunks,
|
|
990
|
+
handling the details of breaking up large transfers into smaller
|
|
991
|
+
pieces. It's not part of the XCP Specification but provides a
|
|
992
|
+
convenient way to push data.
|
|
599
993
|
|
|
600
994
|
Parameters
|
|
601
995
|
----------
|
|
602
|
-
address: int
|
|
603
|
-
|
|
996
|
+
address : int
|
|
997
|
+
The memory address to write to
|
|
998
|
+
address_ext : int
|
|
999
|
+
The address extension
|
|
604
1000
|
data : bytes
|
|
605
|
-
|
|
1001
|
+
The data bytes to write
|
|
1002
|
+
callback : Callable[[int], None], optional
|
|
1003
|
+
A callback function that is called with the percentage of completion,
|
|
1004
|
+
by default None
|
|
606
1005
|
|
|
607
|
-
|
|
608
|
-
|
|
1006
|
+
Note
|
|
1007
|
+
----
|
|
1008
|
+
This method uses the download and downloadNext methods internally.
|
|
609
1009
|
"""
|
|
1010
|
+
# Use the generalized downloader to transfer the data
|
|
610
1011
|
self._generalized_downloader(
|
|
611
1012
|
address=address,
|
|
612
1013
|
address_ext=address_ext,
|
|
@@ -620,20 +1021,31 @@ class Master:
|
|
|
620
1021
|
callback=callback,
|
|
621
1022
|
)
|
|
622
1023
|
|
|
623
|
-
def flash_program(self, address: int, data: bytes, callback:
|
|
624
|
-
"""Convenience function for flash
|
|
625
|
-
|
|
1024
|
+
def flash_program(self, address: int, data: bytes, callback: Callable[[int], None] | None = None) -> None:
|
|
1025
|
+
"""Convenience function for flash programming.
|
|
1026
|
+
|
|
1027
|
+
This method programs flash memory on the slave in chunks, handling
|
|
1028
|
+
the details of breaking up large transfers into smaller pieces.
|
|
1029
|
+
It's not part of the XCP Specification but provides a convenient
|
|
1030
|
+
way to program flash memory.
|
|
626
1031
|
|
|
627
1032
|
Parameters
|
|
628
1033
|
----------
|
|
629
|
-
address: int
|
|
630
|
-
|
|
1034
|
+
address : int
|
|
1035
|
+
The memory address to program
|
|
631
1036
|
data : bytes
|
|
632
|
-
|
|
1037
|
+
The data bytes to program
|
|
1038
|
+
callback : Callable[[int], None], optional
|
|
1039
|
+
A callback function that is called with the percentage of completion,
|
|
1040
|
+
by default None
|
|
633
1041
|
|
|
634
|
-
|
|
635
|
-
|
|
1042
|
+
Note
|
|
1043
|
+
----
|
|
1044
|
+
This method uses the program and programNext methods internally.
|
|
1045
|
+
It automatically uses the programming-specific parameters from the
|
|
1046
|
+
slave properties (maxCtoPgm, maxBsPgm, minStPgm, masterBlockMode).
|
|
636
1047
|
"""
|
|
1048
|
+
# Use the generalized downloader to program the flash
|
|
637
1049
|
self._generalized_downloader(
|
|
638
1050
|
address=address,
|
|
639
1051
|
data=data,
|
|
@@ -655,110 +1067,252 @@ class Master:
|
|
|
655
1067
|
maxBs: int,
|
|
656
1068
|
minSt: int,
|
|
657
1069
|
master_block_mode: bool,
|
|
658
|
-
dl_func,
|
|
659
|
-
dl_next_func,
|
|
660
|
-
callback=None,
|
|
661
|
-
):
|
|
662
|
-
"""
|
|
1070
|
+
dl_func: Callable[[bytes, int, bool], Any],
|
|
1071
|
+
dl_next_func: Callable[[bytes, int, bool], Any],
|
|
1072
|
+
callback: Callable[[int], None] | None = None,
|
|
1073
|
+
) -> None:
|
|
1074
|
+
"""Generic implementation for downloading data to the slave.
|
|
1075
|
+
|
|
1076
|
+
This method is a generic implementation for downloading data to the slave.
|
|
1077
|
+
It handles the details of breaking up large transfers into smaller pieces,
|
|
1078
|
+
and supports both master block mode and normal mode.
|
|
1079
|
+
|
|
1080
|
+
Parameters
|
|
1081
|
+
----------
|
|
1082
|
+
address : int
|
|
1083
|
+
The memory address to write to
|
|
1084
|
+
address_ext : int
|
|
1085
|
+
The address extension
|
|
1086
|
+
data : bytes
|
|
1087
|
+
The data bytes to write
|
|
1088
|
+
maxCto : int
|
|
1089
|
+
Maximum Command Transfer Object size
|
|
1090
|
+
maxBs : int
|
|
1091
|
+
Maximum Block Size
|
|
1092
|
+
minSt : int
|
|
1093
|
+
Minimum Separation Time in 100µs units
|
|
1094
|
+
master_block_mode : bool
|
|
1095
|
+
Whether to use master block mode
|
|
1096
|
+
dl_func : Callable[[bytes, int, bool], Any]
|
|
1097
|
+
Function to use for the first download packet
|
|
1098
|
+
dl_next_func : Callable[[bytes, int, bool], Any]
|
|
1099
|
+
Function to use for subsequent download packets
|
|
1100
|
+
callback : Callable[[int], None], optional
|
|
1101
|
+
A callback function that is called with the percentage of completion,
|
|
1102
|
+
by default None
|
|
1103
|
+
"""
|
|
1104
|
+
# Set the Memory Transfer Address
|
|
663
1105
|
self.setMta(address, address_ext)
|
|
664
|
-
|
|
1106
|
+
|
|
1107
|
+
# Convert minSt from 100µs units to seconds
|
|
1108
|
+
minSt_seconds = minSt / 10000.0
|
|
1109
|
+
|
|
1110
|
+
# Create a partial function for block downloading
|
|
665
1111
|
block_downloader = functools.partial(
|
|
666
1112
|
self._block_downloader,
|
|
667
1113
|
dl_func=dl_func,
|
|
668
1114
|
dl_next_func=dl_next_func,
|
|
669
|
-
minSt=
|
|
1115
|
+
minSt=minSt_seconds,
|
|
670
1116
|
)
|
|
1117
|
+
|
|
1118
|
+
# Calculate total length and maximum payload size
|
|
671
1119
|
total_length = len(data)
|
|
672
1120
|
if master_block_mode:
|
|
673
1121
|
max_payload = min(maxBs * (maxCto - 2), 255)
|
|
674
1122
|
else:
|
|
675
1123
|
max_payload = maxCto - 2
|
|
1124
|
+
|
|
1125
|
+
# Initialize offset
|
|
676
1126
|
offset = 0
|
|
1127
|
+
|
|
1128
|
+
# Handle master block mode
|
|
677
1129
|
if master_block_mode:
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
percent_complete = 1
|
|
681
|
-
remaining_block_size = total_length % max_payload
|
|
682
|
-
for _ in blocks:
|
|
683
|
-
block = data[offset : offset + max_payload]
|
|
684
|
-
block_downloader(block)
|
|
685
|
-
offset += max_payload
|
|
686
|
-
remaining -= max_payload
|
|
687
|
-
if callback and remaining <= total_length - (total_length / 100) * percent_complete:
|
|
688
|
-
callback(percent_complete)
|
|
689
|
-
percent_complete += 1
|
|
690
|
-
if remaining_block_size:
|
|
691
|
-
block = data[offset : offset + remaining_block_size]
|
|
692
|
-
block_downloader(block)
|
|
693
|
-
if callback:
|
|
694
|
-
callback(percent_complete)
|
|
1130
|
+
self._download_master_block_mode(data, total_length, max_payload, offset, block_downloader, callback)
|
|
1131
|
+
# Handle normal mode
|
|
695
1132
|
else:
|
|
696
|
-
|
|
697
|
-
chunks = range(total_length // chunk_size)
|
|
698
|
-
remaining = total_length % chunk_size
|
|
699
|
-
percent_complete = 1
|
|
700
|
-
callback_remaining = total_length
|
|
701
|
-
for _ in chunks:
|
|
702
|
-
block = data[offset : offset + max_payload]
|
|
703
|
-
dl_func(block, max_payload, last=False)
|
|
704
|
-
offset += max_payload
|
|
705
|
-
callback_remaining -= chunk_size
|
|
706
|
-
if callback and callback_remaining <= total_length - (total_length / 100) * percent_complete:
|
|
707
|
-
callback(percent_complete)
|
|
708
|
-
percent_complete += 1
|
|
709
|
-
if remaining:
|
|
710
|
-
block = data[offset : offset + remaining]
|
|
711
|
-
dl_func(block, remaining, last=True)
|
|
712
|
-
if callback:
|
|
713
|
-
callback(percent_complete)
|
|
1133
|
+
self._download_normal_mode(data, total_length, max_payload, offset, dl_func, callback)
|
|
714
1134
|
|
|
715
|
-
def
|
|
716
|
-
self,
|
|
717
|
-
|
|
718
|
-
|
|
1135
|
+
def _download_master_block_mode(
|
|
1136
|
+
self,
|
|
1137
|
+
data: bytes,
|
|
1138
|
+
total_length: int,
|
|
1139
|
+
max_payload: int,
|
|
1140
|
+
offset: int,
|
|
1141
|
+
block_downloader: Callable[[bytes], Any],
|
|
1142
|
+
callback: Callable[[int], None] | None = None,
|
|
1143
|
+
) -> None:
|
|
1144
|
+
"""Download data using master block mode.
|
|
719
1145
|
|
|
720
1146
|
Parameters
|
|
721
1147
|
----------
|
|
722
1148
|
data : bytes
|
|
723
|
-
|
|
1149
|
+
The data bytes to write
|
|
1150
|
+
total_length : int
|
|
1151
|
+
The total length of the data
|
|
1152
|
+
max_payload : int
|
|
1153
|
+
Maximum payload size
|
|
1154
|
+
offset : int
|
|
1155
|
+
Starting offset in the data
|
|
1156
|
+
block_downloader : Callable[[bytes], Any]
|
|
1157
|
+
Function to use for downloading blocks
|
|
1158
|
+
callback : Callable[[int], None], optional
|
|
1159
|
+
A callback function that is called with the percentage of completion,
|
|
1160
|
+
by default None
|
|
1161
|
+
"""
|
|
1162
|
+
remaining = total_length
|
|
1163
|
+
blocks = range(total_length // max_payload)
|
|
1164
|
+
percent_complete = 1
|
|
1165
|
+
remaining_block_size = total_length % max_payload
|
|
1166
|
+
|
|
1167
|
+
# Process full blocks
|
|
1168
|
+
for _ in blocks:
|
|
1169
|
+
block = data[offset : offset + max_payload]
|
|
1170
|
+
block_downloader(block)
|
|
1171
|
+
offset += max_payload
|
|
1172
|
+
remaining -= max_payload
|
|
1173
|
+
|
|
1174
|
+
# Call callback if provided
|
|
1175
|
+
if callback and remaining <= total_length - (total_length / 100) * percent_complete:
|
|
1176
|
+
callback(percent_complete)
|
|
1177
|
+
percent_complete += 1
|
|
1178
|
+
|
|
1179
|
+
# Process remaining partial block
|
|
1180
|
+
if remaining_block_size:
|
|
1181
|
+
block = data[offset : offset + remaining_block_size]
|
|
1182
|
+
block_downloader(block)
|
|
1183
|
+
if callback:
|
|
1184
|
+
callback(percent_complete)
|
|
1185
|
+
|
|
1186
|
+
def _download_normal_mode(
|
|
1187
|
+
self,
|
|
1188
|
+
data: bytes,
|
|
1189
|
+
total_length: int,
|
|
1190
|
+
max_payload: int,
|
|
1191
|
+
offset: int,
|
|
1192
|
+
dl_func: Callable[[bytes, int, bool], Any],
|
|
1193
|
+
callback: Callable[[int], None] | None = None,
|
|
1194
|
+
) -> None:
|
|
1195
|
+
"""Download data using normal mode.
|
|
1196
|
+
|
|
1197
|
+
Parameters
|
|
1198
|
+
----------
|
|
1199
|
+
data : bytes
|
|
1200
|
+
The data bytes to write
|
|
1201
|
+
total_length : int
|
|
1202
|
+
The total length of the data
|
|
1203
|
+
max_payload : int
|
|
1204
|
+
Maximum payload size
|
|
1205
|
+
offset : int
|
|
1206
|
+
Starting offset in the data
|
|
1207
|
+
dl_func : Callable[[bytes, int, bool], Any]
|
|
1208
|
+
Function to use for downloading
|
|
1209
|
+
callback : Callable[[int], None], optional
|
|
1210
|
+
A callback function that is called with the percentage of completion,
|
|
1211
|
+
by default None
|
|
1212
|
+
"""
|
|
1213
|
+
chunk_size = max_payload
|
|
1214
|
+
chunks = range(total_length // chunk_size)
|
|
1215
|
+
remaining = total_length % chunk_size
|
|
1216
|
+
percent_complete = 1
|
|
1217
|
+
callback_remaining = total_length
|
|
724
1218
|
|
|
725
|
-
|
|
726
|
-
|
|
1219
|
+
# Process full chunks
|
|
1220
|
+
for _ in chunks:
|
|
1221
|
+
block = data[offset : offset + max_payload]
|
|
1222
|
+
dl_func(block, max_payload, last=False)
|
|
1223
|
+
offset += max_payload
|
|
1224
|
+
callback_remaining -= chunk_size
|
|
1225
|
+
|
|
1226
|
+
# Call callback if provided
|
|
1227
|
+
if callback and callback_remaining <= total_length - (total_length / 100) * percent_complete:
|
|
1228
|
+
callback(percent_complete)
|
|
1229
|
+
percent_complete += 1
|
|
1230
|
+
|
|
1231
|
+
# Process remaining partial chunk
|
|
1232
|
+
if remaining:
|
|
1233
|
+
block = data[offset : offset + remaining]
|
|
1234
|
+
dl_func(block, remaining, last=True)
|
|
1235
|
+
if callback:
|
|
1236
|
+
callback(percent_complete)
|
|
1237
|
+
|
|
1238
|
+
def _block_downloader(
|
|
1239
|
+
self,
|
|
1240
|
+
data: bytes,
|
|
1241
|
+
dl_func: Callable[[bytes, int, bool], Any] | None = None,
|
|
1242
|
+
dl_next_func: Callable[[bytes, int, bool], Any] | None = None,
|
|
1243
|
+
minSt: float = 0.0,
|
|
1244
|
+
) -> None:
|
|
1245
|
+
"""Re-usable block downloader for transferring data in blocks.
|
|
727
1246
|
|
|
728
|
-
|
|
729
|
-
|
|
1247
|
+
This method breaks up a block of data into packets and sends them
|
|
1248
|
+
using the provided download functions. It handles the details of
|
|
1249
|
+
calculating packet sizes, setting the 'last' flag, and applying
|
|
1250
|
+
the minimum separation time between packets.
|
|
730
1251
|
|
|
731
|
-
|
|
732
|
-
|
|
1252
|
+
Parameters
|
|
1253
|
+
----------
|
|
1254
|
+
data : bytes
|
|
1255
|
+
The data bytes to download
|
|
1256
|
+
dl_func : Callable[[bytes, int, bool], Any] | None, optional
|
|
1257
|
+
Function to use for the first download packet,
|
|
1258
|
+
usually :meth:`download` or :meth:`program`, by default None
|
|
1259
|
+
dl_next_func : Callable[[bytes, int, bool], Any] | None, optional
|
|
1260
|
+
Function to use for subsequent download packets,
|
|
1261
|
+
usually :meth:`downloadNext` or :meth:`programNext`, by default None
|
|
1262
|
+
minSt : float, optional
|
|
1263
|
+
Minimum separation time between frames in seconds, by default 0.0
|
|
733
1264
|
"""
|
|
1265
|
+
# Calculate sizes and offsets
|
|
734
1266
|
length = len(data)
|
|
735
1267
|
max_packet_size = self.slaveProperties.maxCto - 2 # Command ID + Length
|
|
736
1268
|
packets = range(length // max_packet_size)
|
|
737
1269
|
offset = 0
|
|
738
1270
|
remaining = length % max_packet_size
|
|
739
1271
|
remaining_block_size = length
|
|
1272
|
+
|
|
1273
|
+
# Process full packets
|
|
740
1274
|
index = 0
|
|
741
1275
|
for index in packets:
|
|
1276
|
+
# Extract packet data
|
|
742
1277
|
packet_data = data[offset : offset + max_packet_size]
|
|
1278
|
+
|
|
1279
|
+
# Determine if this is the last packet
|
|
743
1280
|
last = (remaining_block_size - max_packet_size) == 0
|
|
1281
|
+
|
|
1282
|
+
# Send packet using appropriate function
|
|
744
1283
|
if index == 0:
|
|
745
|
-
|
|
1284
|
+
# First packet: use dl_func and transmit the complete length
|
|
1285
|
+
dl_func(packet_data, length, last)
|
|
746
1286
|
else:
|
|
1287
|
+
# Subsequent packets: use dl_next_func
|
|
747
1288
|
dl_next_func(packet_data, remaining_block_size, last)
|
|
1289
|
+
|
|
1290
|
+
# Update offsets and remaining size
|
|
748
1291
|
offset += max_packet_size
|
|
749
1292
|
remaining_block_size -= max_packet_size
|
|
1293
|
+
|
|
1294
|
+
# Apply minimum separation time
|
|
750
1295
|
delay(minSt)
|
|
1296
|
+
|
|
1297
|
+
# Process remaining partial packet
|
|
751
1298
|
if remaining:
|
|
1299
|
+
# Extract remaining data
|
|
752
1300
|
packet_data = data[offset : offset + remaining]
|
|
1301
|
+
|
|
1302
|
+
# Send packet using appropriate function
|
|
753
1303
|
if index == 0:
|
|
754
|
-
#
|
|
1304
|
+
# If there were no full packets, use dl_func
|
|
1305
|
+
# (length of data is smaller than maxCto - 2)
|
|
755
1306
|
dl_func(packet_data, remaining, last=True)
|
|
756
1307
|
else:
|
|
1308
|
+
# Otherwise use dl_next_func
|
|
757
1309
|
dl_next_func(packet_data, remaining, last=True)
|
|
1310
|
+
|
|
1311
|
+
# Apply minimum separation time
|
|
758
1312
|
delay(minSt)
|
|
759
1313
|
|
|
760
1314
|
@wrapped
|
|
761
|
-
def download(self, data: bytes, block_mode_length:
|
|
1315
|
+
def download(self, data: bytes, block_mode_length: int | None = None, last: bool = False):
|
|
762
1316
|
"""Transfer data from master to slave.
|
|
763
1317
|
|
|
764
1318
|
Parameters
|
|
@@ -1763,7 +2317,10 @@ class Master:
|
|
|
1763
2317
|
def getCurrentProtectionStatus(self):
|
|
1764
2318
|
""""""
|
|
1765
2319
|
if self.currentProtectionStatus is None:
|
|
1766
|
-
|
|
2320
|
+
try:
|
|
2321
|
+
status = self.getStatus()
|
|
2322
|
+
except Exception: # may temporary ERR_OUT_OF_RANGE
|
|
2323
|
+
return {"dbg": None, "pgm": None, "stim": None, "daq": None, "calpag": None}
|
|
1767
2324
|
self._setProtectionStatus(status.resourceProtectionStatus)
|
|
1768
2325
|
return self.currentProtectionStatus
|
|
1769
2326
|
|
|
@@ -1885,7 +2442,7 @@ class Master:
|
|
|
1885
2442
|
value = self.fetch(gid.length)
|
|
1886
2443
|
return decode_bytes(value)
|
|
1887
2444
|
|
|
1888
|
-
def id_scanner(self, scan_ranges:
|
|
2445
|
+
def id_scanner(self, scan_ranges: Collection[Collection[int]] | None = None) -> dict[str, str]:
|
|
1889
2446
|
"""Scan for available standard identification types (GET_ID).
|
|
1890
2447
|
|
|
1891
2448
|
Parameters
|
|
@@ -1935,18 +2492,34 @@ class Master:
|
|
|
1935
2492
|
name = STD_IDS[id_value]
|
|
1936
2493
|
else:
|
|
1937
2494
|
name = f"USER_{idx}"
|
|
1938
|
-
yield
|
|
2495
|
+
yield (
|
|
2496
|
+
id_value,
|
|
2497
|
+
name,
|
|
2498
|
+
)
|
|
1939
2499
|
|
|
1940
2500
|
return generate()
|
|
1941
2501
|
|
|
1942
2502
|
gen = make_generator(scan_ranges)
|
|
1943
2503
|
for id_value, name in gen:
|
|
1944
|
-
|
|
2504
|
+
# Avoid noisy warnings while probing
|
|
2505
|
+
status, response = self.try_command(self.identifier, id_value, silent=True)
|
|
1945
2506
|
if status == types.TryCommandResult.OK and response:
|
|
1946
2507
|
result[name] = response
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2508
|
+
continue
|
|
2509
|
+
if status == types.TryCommandResult.NOT_IMPLEMENTED:
|
|
2510
|
+
# GET_ID not supported by the slave at all → stop scanning
|
|
2511
|
+
break
|
|
2512
|
+
if status == types.TryCommandResult.XCP_ERROR:
|
|
2513
|
+
# Some IDs may not be supported; ignore typical probe errors
|
|
2514
|
+
try:
|
|
2515
|
+
err = response.error_code
|
|
2516
|
+
except Exception:
|
|
2517
|
+
err = None
|
|
2518
|
+
if err in (types.XcpError.ERR_OUT_OF_RANGE, types.XcpError.ERR_CMD_SYNTAX):
|
|
2519
|
+
continue
|
|
2520
|
+
# For any other XCP error, keep scanning (best-effort) instead of aborting
|
|
2521
|
+
continue
|
|
2522
|
+
if status == types.TryCommandResult.OTHER_ERROR:
|
|
1950
2523
|
raise RuntimeError(f"Error while scanning for ID {id_value}: {response!r}")
|
|
1951
2524
|
return result
|
|
1952
2525
|
|
|
@@ -1955,7 +2528,7 @@ class Master:
|
|
|
1955
2528
|
""""""
|
|
1956
2529
|
return self.transport.start_datetime
|
|
1957
2530
|
|
|
1958
|
-
def try_command(self, cmd: Callable, *args, **kws) ->
|
|
2531
|
+
def try_command(self, cmd: Callable, *args, **kws) -> tuple[types.TryCommandResult, Any]:
|
|
1959
2532
|
"""Call master functions and handle XCP errors more gracefuly.
|
|
1960
2533
|
|
|
1961
2534
|
Parameter
|
|
@@ -1982,12 +2555,12 @@ class Master:
|
|
|
1982
2555
|
_prev_suppress = is_suppress_xcp_error_log()
|
|
1983
2556
|
set_suppress_xcp_error_log(True)
|
|
1984
2557
|
try:
|
|
1985
|
-
extra_msg:
|
|
2558
|
+
extra_msg: str | None = kws.get("extra_msg")
|
|
1986
2559
|
if extra_msg:
|
|
1987
2560
|
kws.pop("extra_msg")
|
|
1988
2561
|
else:
|
|
1989
2562
|
extra_msg = ""
|
|
1990
|
-
silent:
|
|
2563
|
+
silent: bool | None = kws.get("silent")
|
|
1991
2564
|
if silent:
|
|
1992
2565
|
kws.pop("silent")
|
|
1993
2566
|
else:
|
|
@@ -2013,10 +2586,8 @@ class Master:
|
|
|
2013
2586
|
return (types.TryCommandResult.OK, res)
|
|
2014
2587
|
finally:
|
|
2015
2588
|
# Ensure suppression flag is restored even on success/other exceptions
|
|
2016
|
-
|
|
2589
|
+
with suppress(Exception):
|
|
2017
2590
|
set_suppress_xcp_error_log(_prev_suppress)
|
|
2018
|
-
except Exception:
|
|
2019
|
-
pass
|
|
2020
2591
|
|
|
2021
2592
|
|
|
2022
2593
|
def ticks_to_seconds(ticks, resolution):
|