pyxcp 0.23.3__cp312-cp312-win_arm64.whl → 0.25.6__cp312-cp312-win_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/asamkeydll.exe +0 -0
- pyxcp/cmdline.py +15 -30
- pyxcp/config/__init__.py +73 -20
- pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
- pyxcp/cpp_ext/bin.hpp +7 -6
- pyxcp/cpp_ext/cpp_ext.cp310-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/cpp_ext.cp311-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/cpp_ext.cp312-win_arm64.pyd +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/mcobject.hpp +5 -3
- pyxcp/cpp_ext/sxi_framing.hpp +332 -0
- pyxcp/daq_stim/__init__.py +182 -45
- pyxcp/daq_stim/optimize/binpacking.py +2 -2
- pyxcp/daq_stim/scheduler.cpp +8 -8
- pyxcp/daq_stim/stim.cp310-win_arm64.pyd +0 -0
- pyxcp/daq_stim/stim.cp311-win_arm64.pyd +0 -0
- pyxcp/daq_stim/stim.cp312-win_arm64.pyd +0 -0
- pyxcp/errormatrix.py +2 -2
- pyxcp/examples/run_daq.py +5 -3
- 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 +248 -13
- pyxcp/master/master.py +838 -250
- 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 +5 -10
- pyxcp/recorder/converter/__init__.py +4 -10
- pyxcp/recorder/reader.hpp +0 -1
- pyxcp/recorder/reco.py +1 -0
- pyxcp/recorder/rekorder.cp310-win_arm64.pyd +0 -0
- pyxcp/recorder/rekorder.cp311-win_arm64.pyd +0 -0
- pyxcp/recorder/rekorder.cp312-win_arm64.pyd +0 -0
- pyxcp/recorder/unfolder.hpp +129 -107
- pyxcp/recorder/wrap.cpp +3 -8
- 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 +187 -143
- pyxcp/transport/can.py +117 -13
- pyxcp/transport/eth.py +55 -20
- pyxcp/transport/hdf5_policy.py +167 -0
- pyxcp/transport/sxi.py +126 -52
- pyxcp/transport/transport_ext.cp310-win_arm64.pyd +0 -0
- pyxcp/transport/transport_ext.cp311-win_arm64.pyd +0 -0
- pyxcp/transport/transport_ext.cp312-win_arm64.pyd +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} +3 -4
- pyxcp/utils/cli.py +78 -0
- pyxcp-0.25.6.dist-info/METADATA +341 -0
- pyxcp-0.25.6.dist-info/RECORD +153 -0
- {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info}/WHEEL +1 -1
- pyxcp/examples/conf_sxi.json +0 -9
- pyxcp/examples/conf_sxi.toml +0 -7
- pyxcp-0.23.3.dist-info/METADATA +0 -219
- pyxcp-0.23.3.dist-info/RECORD +0 -131
- {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info}/entry_points.txt +0 -0
- {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.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,11 +28,20 @@ from pyxcp.constants import (
|
|
|
24
28
|
makeWordPacker,
|
|
25
29
|
makeWordUnpacker,
|
|
26
30
|
)
|
|
27
|
-
from pyxcp.
|
|
28
|
-
|
|
31
|
+
from pyxcp.master.errorhandler import (
|
|
32
|
+
SystemExit,
|
|
33
|
+
disable_error_handling,
|
|
34
|
+
is_suppress_xcp_error_log,
|
|
35
|
+
set_suppress_xcp_error_log,
|
|
36
|
+
wrapped,
|
|
37
|
+
)
|
|
29
38
|
from pyxcp.transport.base import create_transport
|
|
30
39
|
from pyxcp.utils import decode_bytes, delay, short_sleep
|
|
31
40
|
|
|
41
|
+
# Type variables for better type hinting
|
|
42
|
+
T = TypeVar("T")
|
|
43
|
+
R = TypeVar("R")
|
|
44
|
+
|
|
32
45
|
|
|
33
46
|
def broadcasted(func: Callable):
|
|
34
47
|
""""""
|
|
@@ -36,107 +49,215 @@ def broadcasted(func: Callable):
|
|
|
36
49
|
|
|
37
50
|
|
|
38
51
|
class SlaveProperties(dict):
|
|
39
|
-
"""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
|
+
"""
|
|
40
56
|
|
|
41
|
-
def __init__(self, *args, **kws):
|
|
57
|
+
def __init__(self, *args: Any, **kws: Any) -> None:
|
|
58
|
+
"""Initialize a new SlaveProperties instance.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
*args : Any
|
|
63
|
+
Positional arguments passed to dict.__init__
|
|
64
|
+
**kws : Any
|
|
65
|
+
Keyword arguments passed to dict.__init__
|
|
66
|
+
"""
|
|
42
67
|
super().__init__(*args, **kws)
|
|
43
68
|
|
|
44
|
-
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
|
+
"""
|
|
45
82
|
return self[name]
|
|
46
83
|
|
|
47
|
-
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
|
+
"""
|
|
48
94
|
self[name] = value
|
|
49
95
|
|
|
50
|
-
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
|
+
"""
|
|
51
104
|
return self
|
|
52
105
|
|
|
53
|
-
def __setstate__(self, state):
|
|
54
|
-
|
|
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
|
|
55
115
|
|
|
56
116
|
|
|
57
117
|
class Master:
|
|
58
118
|
"""Common part of lowlevel XCP API.
|
|
59
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
|
+
|
|
60
124
|
Parameters
|
|
61
125
|
----------
|
|
62
|
-
transport_name : str
|
|
126
|
+
transport_name : str | None
|
|
63
127
|
XCP transport layer name ['can', 'eth', 'sxi']
|
|
64
|
-
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
|
|
65
134
|
"""
|
|
66
135
|
|
|
67
|
-
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
|
+
"""
|
|
68
155
|
if transport_name is None:
|
|
69
156
|
raise ValueError("No transport-layer selected") # Never reached -- to keep type-checkers happy.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
self.
|
|
73
|
-
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
|
|
74
165
|
disable_error_handling(self.config.disable_error_handling)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
self.
|
|
78
|
-
|
|
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)
|
|
79
174
|
self.stim.clear()
|
|
80
175
|
self.stim.set_policy_feeder(self.transport.policy.feed)
|
|
81
176
|
self.stim.set_frame_sender(self.transport.block_request)
|
|
82
177
|
|
|
83
178
|
# In some cases the transport-layer needs to communicate with us.
|
|
84
179
|
self.transport.parent = self
|
|
85
|
-
self.service = None
|
|
180
|
+
self.service: Any = None
|
|
86
181
|
|
|
87
|
-
# Policies may issue XCP commands on
|
|
182
|
+
# Policies may issue XCP commands on their own.
|
|
88
183
|
self.transport.policy.xcp_master = self
|
|
89
184
|
|
|
90
185
|
# (D)Word (un-)packers are byte-order dependent
|
|
91
186
|
# -- byte-order is returned by CONNECT_Resp (COMM_MODE_BASIC)
|
|
92
|
-
self.BYTE_pack = None
|
|
93
|
-
self.BYTE_unpack = None
|
|
94
|
-
self.WORD_pack = None
|
|
95
|
-
self.WORD_unpack = None
|
|
96
|
-
self.DWORD_pack = None
|
|
97
|
-
self.DWORD_unpack = None
|
|
98
|
-
self.DLONG_pack = None
|
|
99
|
-
self.DLONG_unpack = None
|
|
100
|
-
self.AG_pack = None
|
|
101
|
-
self.AG_unpack = None
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
self.
|
|
105
|
-
self.
|
|
106
|
-
self.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
self.
|
|
110
|
-
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()
|
|
111
211
|
self.slaveProperties.pgmProcessor = SlaveProperties()
|
|
112
212
|
self.slaveProperties.transport_layer = self.transport_name.upper()
|
|
113
213
|
|
|
114
214
|
def __enter__(self):
|
|
115
|
-
"""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
|
+
"""
|
|
116
225
|
self.transport.connect()
|
|
117
226
|
return self
|
|
118
227
|
|
|
119
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
120
|
-
"""Context manager exit part.
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
123
244
|
self.close()
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
245
|
+
|
|
246
|
+
# Handle any exceptions that were raised
|
|
247
|
+
if exc_type is not None:
|
|
127
248
|
self.succeeded = False
|
|
128
|
-
# print("=" * 79)
|
|
129
|
-
# print("Exception while in Context-Manager:\n")
|
|
130
249
|
self.logger.error("".join(traceback.format_exception(exc_type, exc_val, exc_tb)))
|
|
131
|
-
# print("=" * 79)
|
|
132
|
-
# return True
|
|
133
250
|
|
|
134
|
-
def _setService(self, service):
|
|
135
|
-
"""
|
|
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.
|
|
136
256
|
|
|
137
257
|
Parameters
|
|
138
258
|
----------
|
|
139
|
-
service:
|
|
259
|
+
service : Any
|
|
260
|
+
The service being processed, typically a `pyxcp.types.Command`
|
|
140
261
|
|
|
141
262
|
Note
|
|
142
263
|
----
|
|
@@ -144,66 +265,122 @@ class Master:
|
|
|
144
265
|
"""
|
|
145
266
|
self.service = service
|
|
146
267
|
|
|
147
|
-
def close(self):
|
|
148
|
-
"""
|
|
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
|
+
"""
|
|
149
274
|
self.transport.policy.finalize()
|
|
150
275
|
self.transport.close()
|
|
151
276
|
|
|
152
277
|
# Mandatory Commands.
|
|
153
278
|
@wrapped
|
|
154
|
-
def connect(self, mode=0x00):
|
|
279
|
+
def connect(self, mode: int = 0x00) -> types.ConnectResponse:
|
|
155
280
|
"""Build up connection to an XCP slave.
|
|
156
281
|
|
|
157
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.
|
|
158
285
|
|
|
159
286
|
Parameters
|
|
160
287
|
----------
|
|
161
|
-
mode : int
|
|
162
|
-
|
|
288
|
+
mode : int, optional
|
|
289
|
+
Connection mode, by default 0x00 (normal mode)
|
|
163
290
|
|
|
164
291
|
Returns
|
|
165
292
|
-------
|
|
166
|
-
|
|
167
|
-
|
|
293
|
+
types.ConnectResponse
|
|
294
|
+
Response object containing fundamental client properties
|
|
168
295
|
|
|
169
296
|
Note
|
|
170
297
|
----
|
|
171
298
|
Every XCP slave supports at most one connection,
|
|
172
299
|
more attempts to connect are silently ignored.
|
|
173
|
-
|
|
174
300
|
"""
|
|
301
|
+
# Send CONNECT command to the slave
|
|
175
302
|
response = self.transport.request(types.Command.CONNECT, mode & 0xFF)
|
|
176
303
|
|
|
177
|
-
# First get byte-order
|
|
178
|
-
|
|
179
|
-
|
|
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)
|
|
180
316
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
317
|
+
# Set up address granularity dependent properties
|
|
318
|
+
self._setup_address_granularity()
|
|
319
|
+
|
|
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
|
|
184
334
|
self.slaveProperties.maxCto = result.maxCto
|
|
185
335
|
self.slaveProperties.maxDto = result.maxDto
|
|
336
|
+
|
|
337
|
+
# Set resource support flags
|
|
186
338
|
self.slaveProperties.supportsPgm = result.resource.pgm
|
|
187
339
|
self.slaveProperties.supportsStim = result.resource.stim
|
|
188
340
|
self.slaveProperties.supportsDaq = result.resource.daq
|
|
189
341
|
self.slaveProperties.supportsCalpag = result.resource.calpag
|
|
342
|
+
|
|
343
|
+
# Set communication mode properties
|
|
190
344
|
self.slaveProperties.slaveBlockMode = result.commModeBasic.slaveBlockMode
|
|
191
345
|
self.slaveProperties.addressGranularity = result.commModeBasic.addressGranularity
|
|
346
|
+
self.slaveProperties.optionalCommMode = result.commModeBasic.optional
|
|
347
|
+
|
|
348
|
+
# Set version information
|
|
192
349
|
self.slaveProperties.protocolLayerVersion = result.protocolLayerVersion
|
|
193
350
|
self.slaveProperties.transportLayerVersion = result.transportLayerVersion
|
|
194
|
-
|
|
351
|
+
|
|
352
|
+
# Calculate derived properties
|
|
195
353
|
self.slaveProperties.maxWriteDaqMultipleElements = (
|
|
196
354
|
0 if self.slaveProperties.maxCto < 10 else int((self.slaveProperties.maxCto - 2) // 8)
|
|
197
355
|
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
self.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
207
384
|
if self.slaveProperties.addressGranularity == types.AddressGranularity.BYTE:
|
|
208
385
|
self.AG_pack = struct.Struct("<B").pack
|
|
209
386
|
self.AG_unpack = struct.Struct("<B").unpack
|
|
@@ -217,15 +394,23 @@ class Master:
|
|
|
217
394
|
self.AG_unpack = self.DWORD_unpack
|
|
218
395
|
self.slaveProperties.bytesPerElement = 4
|
|
219
396
|
# self.connected = True
|
|
220
|
-
|
|
397
|
+
status = self.getStatus()
|
|
398
|
+
if status.sessionStatus.daqRunning:
|
|
399
|
+
# TODO: resume
|
|
400
|
+
self.startStopSynch(0x00)
|
|
221
401
|
|
|
222
402
|
@wrapped
|
|
223
|
-
def disconnect(self):
|
|
224
|
-
"""
|
|
403
|
+
def disconnect(self) -> bytes:
|
|
404
|
+
"""Release the connection to the XCP slave.
|
|
225
405
|
|
|
226
|
-
|
|
227
|
-
|
|
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`).
|
|
228
409
|
|
|
410
|
+
Returns
|
|
411
|
+
-------
|
|
412
|
+
bytes
|
|
413
|
+
The raw response from the slave, typically empty
|
|
229
414
|
|
|
230
415
|
Note
|
|
231
416
|
-----
|
|
@@ -235,58 +420,106 @@ class Master:
|
|
|
235
420
|
- `"DISCONNECT_RESPONSE_OPTIONAL": true` (JSON)
|
|
236
421
|
to your configuration file.
|
|
237
422
|
"""
|
|
423
|
+
# Send DISCONNECT command to the slave
|
|
238
424
|
if self.disconnect_response_optional:
|
|
239
425
|
response = self.transport.request_optional_response(types.Command.DISCONNECT)
|
|
240
426
|
else:
|
|
241
427
|
response = self.transport.request(types.Command.DISCONNECT)
|
|
242
|
-
|
|
428
|
+
|
|
243
429
|
return response
|
|
244
430
|
|
|
245
431
|
@wrapped
|
|
246
|
-
def getStatus(self):
|
|
432
|
+
def getStatus(self) -> types.GetStatusResponse:
|
|
247
433
|
"""Get current status information of the slave device.
|
|
248
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.
|
|
249
437
|
This includes the status of the resource protection, pending store
|
|
250
438
|
requests and the general status of data acquisition and stimulation.
|
|
251
439
|
|
|
252
440
|
Returns
|
|
253
441
|
-------
|
|
254
|
-
|
|
442
|
+
types.GetStatusResponse
|
|
443
|
+
Response object containing status information
|
|
255
444
|
"""
|
|
445
|
+
# Send GET_STATUS command to the slave
|
|
256
446
|
response = self.transport.request(types.Command.GET_STATUS)
|
|
447
|
+
|
|
448
|
+
# Parse the response with the correct byte order
|
|
257
449
|
result = types.GetStatusResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
|
|
450
|
+
|
|
451
|
+
# Update the current protection status
|
|
258
452
|
self._setProtectionStatus(result.resourceProtectionStatus)
|
|
453
|
+
|
|
259
454
|
return result
|
|
260
455
|
|
|
261
456
|
@wrapped
|
|
262
|
-
def synch(self):
|
|
263
|
-
"""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
|
|
264
470
|
response = self.transport.request(types.Command.SYNCH)
|
|
265
471
|
return response
|
|
266
472
|
|
|
267
473
|
@wrapped
|
|
268
|
-
def getCommModeInfo(self):
|
|
474
|
+
def getCommModeInfo(self) -> types.GetCommModeInfoResponse:
|
|
269
475
|
"""Get optional information on different Communication Modes supported
|
|
270
476
|
by the slave.
|
|
271
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
|
+
|
|
272
482
|
Returns
|
|
273
483
|
-------
|
|
274
|
-
|
|
484
|
+
types.GetCommModeInfoResponse
|
|
485
|
+
Response object containing communication mode information
|
|
275
486
|
"""
|
|
487
|
+
# Send GET_COMM_MODE_INFO command to the slave
|
|
276
488
|
response = self.transport.request(types.Command.GET_COMM_MODE_INFO)
|
|
489
|
+
|
|
490
|
+
# Parse the response with the correct byte order
|
|
277
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
|
|
278
507
|
self.slaveProperties.interleavedMode = result.commModeOptional.interleavedMode
|
|
279
508
|
self.slaveProperties.masterBlockMode = result.commModeOptional.masterBlockMode
|
|
509
|
+
|
|
510
|
+
# Set basic communication properties
|
|
280
511
|
self.slaveProperties.maxBs = result.maxBs
|
|
281
512
|
self.slaveProperties.minSt = result.minSt
|
|
282
513
|
self.slaveProperties.queueSize = result.queueSize
|
|
283
514
|
self.slaveProperties.xcpDriverVersionNumber = result.xcpDriverVersionNumber
|
|
284
|
-
return result
|
|
285
515
|
|
|
286
516
|
@wrapped
|
|
287
|
-
def getId(self, mode: int):
|
|
288
|
-
"""
|
|
289
|
-
|
|
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.
|
|
290
523
|
|
|
291
524
|
Parameters
|
|
292
525
|
----------
|
|
@@ -301,16 +534,23 @@ class Master:
|
|
|
301
534
|
|
|
302
535
|
Returns
|
|
303
536
|
-------
|
|
304
|
-
|
|
537
|
+
types.GetIDResponse
|
|
538
|
+
Response object containing identification information
|
|
305
539
|
"""
|
|
540
|
+
# Send GET_ID command to the slave
|
|
306
541
|
response = self.transport.request(types.Command.GET_ID, mode)
|
|
307
542
|
result = types.GetIDResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
|
|
308
543
|
result.length = self.DWORD_unpack(response[3:7])[0]
|
|
544
|
+
|
|
309
545
|
return result
|
|
310
546
|
|
|
311
547
|
@wrapped
|
|
312
|
-
def setRequest(self, mode: int, session_configuration_id: int):
|
|
313
|
-
"""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.
|
|
314
554
|
|
|
315
555
|
Parameters
|
|
316
556
|
----------
|
|
@@ -319,42 +559,68 @@ class Master:
|
|
|
319
559
|
- 2 Request to store DAQ list, no resume
|
|
320
560
|
- 4 Request to store DAQ list, resume enabled
|
|
321
561
|
- 8 Request to clear DAQ configuration
|
|
322
|
-
|
|
562
|
+
session_configuration_id : int
|
|
563
|
+
Identifier for the session configuration
|
|
323
564
|
|
|
565
|
+
Returns
|
|
566
|
+
-------
|
|
567
|
+
bytes
|
|
568
|
+
The raw response from the slave
|
|
324
569
|
"""
|
|
570
|
+
# Send SET_REQUEST command to the slave
|
|
571
|
+
# Split the session_configuration_id into high and low bytes
|
|
325
572
|
return self.transport.request(
|
|
326
573
|
types.Command.SET_REQUEST,
|
|
327
574
|
mode,
|
|
328
|
-
session_configuration_id >> 8,
|
|
329
|
-
session_configuration_id & 0xFF,
|
|
575
|
+
session_configuration_id >> 8, # High byte
|
|
576
|
+
session_configuration_id & 0xFF, # Low byte
|
|
330
577
|
)
|
|
331
578
|
|
|
332
579
|
@wrapped
|
|
333
|
-
def getSeed(self, first: int, resource: int):
|
|
580
|
+
def getSeed(self, first: int, resource: int) -> types.GetSeedResponse:
|
|
334
581
|
"""Get seed from slave for unlocking a protected resource.
|
|
335
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
|
+
|
|
336
588
|
Parameters
|
|
337
589
|
----------
|
|
338
590
|
first : int
|
|
339
591
|
- 0 - first part of seed
|
|
340
592
|
- 1 - remaining part
|
|
341
593
|
resource : int
|
|
342
|
-
- Mode
|
|
594
|
+
- Mode == 0 - Resource to unlock
|
|
343
595
|
- Mode == 1 - Don't care
|
|
344
596
|
|
|
345
597
|
Returns
|
|
346
598
|
-------
|
|
347
|
-
|
|
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.
|
|
348
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
|
|
349
613
|
if self.transport_name == "can":
|
|
350
|
-
#
|
|
351
|
-
#
|
|
352
|
-
# followed by the
|
|
353
|
-
# the master must call getSeed several times until the complete seed is received
|
|
354
|
-
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
|
|
355
617
|
size, seed = response[0], response[1:]
|
|
618
|
+
|
|
619
|
+
# Truncate seed if necessary
|
|
356
620
|
if size < len(seed):
|
|
357
621
|
seed = seed[:size]
|
|
622
|
+
|
|
623
|
+
# Create and populate response object
|
|
358
624
|
reply = types.GetSeedResponse.parse(
|
|
359
625
|
types.GetSeedResponse.build({"length": size, "seed": bytes(size)}),
|
|
360
626
|
byteOrder=self.slaveProperties.byteOrder,
|
|
@@ -362,245 +628,386 @@ class Master:
|
|
|
362
628
|
reply.seed = seed
|
|
363
629
|
return reply
|
|
364
630
|
else:
|
|
365
|
-
|
|
631
|
+
# For other transports, parse the response directly
|
|
366
632
|
return types.GetSeedResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
|
|
367
633
|
|
|
368
634
|
@wrapped
|
|
369
|
-
def unlock(self, length: int, key: bytes):
|
|
635
|
+
def unlock(self, length: int, key: bytes) -> types.ResourceType:
|
|
370
636
|
"""Send key to slave for unlocking a protected resource.
|
|
371
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
|
+
|
|
372
642
|
Parameters
|
|
373
643
|
----------
|
|
374
644
|
length : int
|
|
375
|
-
|
|
645
|
+
Indicates the (remaining) number of key bytes
|
|
376
646
|
key : bytes
|
|
647
|
+
The key bytes to send to the slave
|
|
377
648
|
|
|
378
649
|
Returns
|
|
379
650
|
-------
|
|
380
|
-
|
|
651
|
+
types.ResourceType
|
|
652
|
+
Response object containing the resource protection status
|
|
381
653
|
|
|
382
654
|
Note
|
|
383
655
|
----
|
|
384
656
|
The master has to use :meth:`unlock` in a defined sequence together
|
|
385
|
-
with :meth:`getSeed`. The master only
|
|
657
|
+
with :meth:`getSeed`. The master can only send an :meth:`unlock` sequence
|
|
386
658
|
if previously there was a :meth:`getSeed` sequence. The master has
|
|
387
659
|
to send the first `unlocking` after a :meth:`getSeed` sequence with
|
|
388
660
|
a Length containing the total length of the key.
|
|
389
661
|
"""
|
|
662
|
+
# Send UNLOCK command to the slave
|
|
390
663
|
response = self.transport.request(types.Command.UNLOCK, length, *key)
|
|
664
|
+
|
|
665
|
+
# Parse the response with the correct byte order
|
|
391
666
|
result = types.ResourceType.parse(response, byteOrder=self.slaveProperties.byteOrder)
|
|
667
|
+
|
|
668
|
+
# Update the current protection status
|
|
392
669
|
self._setProtectionStatus(result)
|
|
670
|
+
|
|
393
671
|
return result
|
|
394
672
|
|
|
395
673
|
@wrapped
|
|
396
|
-
def setMta(self, address: int, address_ext: int = 0x00):
|
|
674
|
+
def setMta(self, address: int, address_ext: int = 0x00) -> bytes:
|
|
397
675
|
"""Set Memory Transfer Address in slave.
|
|
398
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
|
+
|
|
399
682
|
Parameters
|
|
400
683
|
----------
|
|
401
684
|
address : int
|
|
402
|
-
|
|
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
|
|
403
693
|
|
|
404
694
|
Note
|
|
405
695
|
----
|
|
406
696
|
The MTA is used by :meth:`buildChecksum`, :meth:`upload`, :meth:`download`, :meth:`downloadNext`,
|
|
407
697
|
:meth:`downloadMax`, :meth:`modifyBits`, :meth:`programClear`, :meth:`program`, :meth:`programNext`
|
|
408
698
|
and :meth:`programMax`.
|
|
409
|
-
|
|
410
699
|
"""
|
|
411
|
-
|
|
700
|
+
# Keep track of MTA (needed for error-handling)
|
|
701
|
+
self.mta = types.MtaType(address, address_ext)
|
|
702
|
+
|
|
703
|
+
# Pack the address into bytes
|
|
412
704
|
addr = self.DWORD_pack(address)
|
|
705
|
+
|
|
706
|
+
# Send SET_MTA command to the slave
|
|
413
707
|
return self.transport.request(types.Command.SET_MTA, 0, 0, address_ext, *addr)
|
|
414
708
|
|
|
415
709
|
@wrapped
|
|
416
|
-
def upload(self, length: int):
|
|
710
|
+
def upload(self, length: int) -> bytes:
|
|
417
711
|
"""Transfer data from slave to master.
|
|
418
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
|
+
|
|
419
718
|
Parameters
|
|
420
719
|
----------
|
|
421
720
|
length : int
|
|
422
|
-
Number of elements (address granularity)
|
|
423
|
-
|
|
424
|
-
Note
|
|
425
|
-
----
|
|
426
|
-
Adress is set via :meth:`setMta` (Some services like :meth:`getID` also set the MTA).
|
|
721
|
+
Number of elements (address granularity) to upload
|
|
427
722
|
|
|
428
723
|
Returns
|
|
429
724
|
-------
|
|
430
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).
|
|
431
731
|
"""
|
|
732
|
+
# Calculate the number of bytes to upload
|
|
432
733
|
byte_count = length * self.slaveProperties.bytesPerElement
|
|
734
|
+
|
|
735
|
+
# Send UPLOAD command to the slave
|
|
433
736
|
response = self.transport.request(types.Command.UPLOAD, length)
|
|
737
|
+
|
|
738
|
+
# Handle block mode for large uploads
|
|
434
739
|
if byte_count > (self.slaveProperties.maxCto - 1):
|
|
740
|
+
# Receive the remaining bytes in block mode
|
|
435
741
|
block_response = self.transport.block_receive(length_required=(byte_count - len(response)))
|
|
436
742
|
response += block_response
|
|
743
|
+
# Handle CAN-specific upload format
|
|
437
744
|
elif self.transport_name == "can":
|
|
438
|
-
#
|
|
439
|
-
#
|
|
440
|
-
#
|
|
441
|
-
|
|
442
|
-
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)
|
|
749
|
+
while remaining_bytes:
|
|
443
750
|
if len(self.transport.resQueue):
|
|
444
751
|
data = self.transport.resQueue.popleft()
|
|
445
|
-
response += data[1 :
|
|
446
|
-
|
|
752
|
+
response += data[1 : remaining_bytes + 1]
|
|
753
|
+
remaining_bytes = byte_count - len(response)
|
|
447
754
|
else:
|
|
448
755
|
short_sleep()
|
|
449
756
|
return response
|
|
450
757
|
|
|
451
758
|
@wrapped
|
|
452
|
-
def shortUpload(self, length: int, address: int, address_ext: int = 0x00):
|
|
453
|
-
"""Transfer data from slave to master.
|
|
454
|
-
|
|
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.
|
|
455
766
|
|
|
456
767
|
Parameters
|
|
457
768
|
----------
|
|
458
769
|
length : int
|
|
459
|
-
Number of elements (address granularity)
|
|
770
|
+
Number of elements (address granularity) to upload
|
|
460
771
|
address : int
|
|
461
|
-
|
|
772
|
+
The memory address to read from
|
|
773
|
+
address_ext : int, optional
|
|
774
|
+
The address extension, by default 0x00
|
|
462
775
|
|
|
463
776
|
Returns
|
|
464
777
|
-------
|
|
465
778
|
bytes
|
|
779
|
+
The uploaded data
|
|
466
780
|
"""
|
|
781
|
+
# Pack the address into bytes
|
|
467
782
|
addr = self.DWORD_pack(address)
|
|
783
|
+
|
|
784
|
+
# Calculate the number of bytes to upload
|
|
468
785
|
byte_count = length * self.slaveProperties.bytesPerElement
|
|
469
786
|
max_byte_count = self.slaveProperties.maxCto - 1
|
|
787
|
+
|
|
788
|
+
# Check if the requested byte count exceeds the maximum
|
|
470
789
|
if byte_count > max_byte_count:
|
|
471
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
|
|
472
793
|
response = self.transport.request(types.Command.SHORT_UPLOAD, length, 0, address_ext, *addr)
|
|
794
|
+
|
|
795
|
+
# Return only the requested number of bytes
|
|
473
796
|
return response[:byte_count]
|
|
474
797
|
|
|
475
798
|
@wrapped
|
|
476
|
-
def buildChecksum(self, blocksize: int):
|
|
799
|
+
def buildChecksum(self, blocksize: int) -> types.BuildChecksumResponse:
|
|
477
800
|
"""Build checksum over memory range.
|
|
478
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
|
+
|
|
479
806
|
Parameters
|
|
480
807
|
----------
|
|
481
808
|
blocksize : int
|
|
809
|
+
The number of elements (address granularity) to include in the checksum
|
|
482
810
|
|
|
483
811
|
Returns
|
|
484
812
|
-------
|
|
485
|
-
|
|
813
|
+
types.BuildChecksumResponse
|
|
814
|
+
Response object containing the checksum information
|
|
486
815
|
|
|
487
|
-
|
|
816
|
+
Note
|
|
817
|
+
----
|
|
818
|
+
Address is set via :meth:`setMta`
|
|
488
819
|
|
|
489
820
|
See Also
|
|
490
821
|
--------
|
|
491
822
|
:mod:`~pyxcp.checksum`
|
|
492
823
|
"""
|
|
824
|
+
# Pack the blocksize into bytes
|
|
493
825
|
bs = self.DWORD_pack(blocksize)
|
|
826
|
+
|
|
827
|
+
# Send BUILD_CHECKSUM command to the slave
|
|
494
828
|
response = self.transport.request(types.Command.BUILD_CHECKSUM, 0, 0, 0, *bs)
|
|
829
|
+
|
|
830
|
+
# Parse the response with the correct byte order
|
|
495
831
|
return types.BuildChecksumResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
|
|
496
832
|
|
|
497
833
|
@wrapped
|
|
498
|
-
def transportLayerCmd(self, sub_command: int, *data:
|
|
834
|
+
def transportLayerCmd(self, sub_command: int, *data: bytes) -> bytes:
|
|
499
835
|
"""Execute transfer-layer specific command.
|
|
500
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
|
+
|
|
501
841
|
Parameters
|
|
502
842
|
----------
|
|
503
|
-
|
|
504
|
-
|
|
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
|
|
505
852
|
|
|
506
853
|
Note
|
|
507
854
|
----
|
|
508
855
|
For details refer to XCP specification.
|
|
509
856
|
"""
|
|
857
|
+
# Send TRANSPORT_LAYER_CMD command to the slave
|
|
510
858
|
return self.transport.request_optional_response(types.Command.TRANSPORT_LAYER_CMD, sub_command, *data)
|
|
511
859
|
|
|
512
860
|
@wrapped
|
|
513
|
-
def userCmd(self, sub_command: int, data: bytes):
|
|
861
|
+
def userCmd(self, sub_command: int, data: bytes) -> bytes:
|
|
514
862
|
"""Execute proprietary command implemented in your XCP client.
|
|
515
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
|
+
|
|
516
868
|
Parameters
|
|
517
869
|
----------
|
|
518
|
-
|
|
870
|
+
sub_command : int
|
|
871
|
+
The sub-command to execute
|
|
519
872
|
data : bytes
|
|
873
|
+
The data bytes to send with the command
|
|
520
874
|
|
|
875
|
+
Returns
|
|
876
|
+
-------
|
|
877
|
+
bytes
|
|
878
|
+
The raw response from the slave
|
|
521
879
|
|
|
522
|
-
|
|
880
|
+
Note
|
|
881
|
+
----
|
|
882
|
+
For details refer to your XCP client vendor.
|
|
523
883
|
"""
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
return response
|
|
884
|
+
# Send USER_CMD command to the slave
|
|
885
|
+
return self.transport.request(types.Command.USER_CMD, sub_command, *data)
|
|
527
886
|
|
|
528
887
|
@wrapped
|
|
529
|
-
def getVersion(self):
|
|
530
|
-
"""Get version information.
|
|
888
|
+
def getVersion(self) -> types.GetVersionResponse:
|
|
889
|
+
"""Get version information from the slave.
|
|
531
890
|
|
|
532
|
-
This
|
|
533
|
-
|
|
534
|
-
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.
|
|
535
894
|
|
|
536
895
|
Returns
|
|
537
896
|
-------
|
|
538
|
-
|
|
897
|
+
types.GetVersionResponse
|
|
898
|
+
Response object containing version information
|
|
539
899
|
"""
|
|
540
|
-
|
|
900
|
+
# Send GET_VERSION command to the slave
|
|
541
901
|
response = self.transport.request(types.Command.GET_VERSION)
|
|
902
|
+
|
|
903
|
+
# Parse the response with the correct byte order
|
|
542
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
|
|
543
920
|
self.slaveProperties.protocolMajor = result.protocolMajor
|
|
544
921
|
self.slaveProperties.protocolMinor = result.protocolMinor
|
|
545
922
|
self.slaveProperties.transportMajor = result.transportMajor
|
|
546
923
|
self.slaveProperties.transportMinor = result.transportMinor
|
|
547
|
-
return result
|
|
548
924
|
|
|
549
|
-
def fetch(self, length: int, limit_payload: int = None): # TODO: pull
|
|
550
|
-
"""Convenience function for data-transfer from slave to master
|
|
551
|
-
|
|
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.
|
|
552
932
|
|
|
553
933
|
Parameters
|
|
554
934
|
----------
|
|
555
935
|
length : int
|
|
556
|
-
|
|
557
|
-
|
|
936
|
+
The number of bytes to fetch
|
|
937
|
+
limit_payload : int, optional
|
|
938
|
+
Transfer less bytes than supported by transport-layer, by default None
|
|
558
939
|
|
|
559
940
|
Returns
|
|
560
941
|
-------
|
|
561
942
|
bytes
|
|
943
|
+
The fetched data
|
|
944
|
+
|
|
945
|
+
Raises
|
|
946
|
+
------
|
|
947
|
+
ValueError
|
|
948
|
+
If limit_payload is less than 8 bytes
|
|
562
949
|
|
|
563
950
|
Note
|
|
564
951
|
----
|
|
565
|
-
|
|
952
|
+
Address is not included because of services implicitly setting
|
|
953
|
+
address information like :meth:`getID`.
|
|
566
954
|
"""
|
|
567
|
-
|
|
955
|
+
# Validate limit_payload
|
|
956
|
+
if limit_payload is not None and limit_payload < 8:
|
|
568
957
|
raise ValueError(f"Payload must be at least 8 bytes - given: {limit_payload}")
|
|
569
958
|
|
|
959
|
+
# Determine maximum payload size
|
|
570
960
|
slave_block_mode = self.slaveProperties.slaveBlockMode
|
|
571
|
-
if slave_block_mode
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
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
|
|
575
964
|
payload = min(limit_payload, max_payload) if limit_payload else max_payload
|
|
965
|
+
|
|
966
|
+
# Calculate number of chunks and remaining bytes
|
|
576
967
|
chunk_size = payload
|
|
577
968
|
chunks = range(length // chunk_size)
|
|
578
969
|
remaining = length % chunk_size
|
|
970
|
+
|
|
971
|
+
# Fetch data in chunks
|
|
579
972
|
result = []
|
|
580
973
|
for _ in chunks:
|
|
581
974
|
data = self.upload(chunk_size)
|
|
582
975
|
result.extend(data[:chunk_size])
|
|
976
|
+
|
|
977
|
+
# Fetch remaining bytes
|
|
583
978
|
if remaining:
|
|
584
979
|
data = self.upload(remaining)
|
|
585
980
|
result.extend(data[:remaining])
|
|
981
|
+
|
|
586
982
|
return bytes(result)
|
|
587
983
|
|
|
588
984
|
pull = fetch # fetch() may be completely replaced by pull() someday.
|
|
589
985
|
|
|
590
|
-
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:
|
|
591
987
|
"""Convenience function for data-transfer from master to slave.
|
|
592
|
-
|
|
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.
|
|
593
993
|
|
|
594
994
|
Parameters
|
|
595
995
|
----------
|
|
596
|
-
address: int
|
|
597
|
-
|
|
996
|
+
address : int
|
|
997
|
+
The memory address to write to
|
|
998
|
+
address_ext : int
|
|
999
|
+
The address extension
|
|
598
1000
|
data : bytes
|
|
599
|
-
|
|
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
|
|
600
1005
|
|
|
601
|
-
|
|
602
|
-
|
|
1006
|
+
Note
|
|
1007
|
+
----
|
|
1008
|
+
This method uses the download and downloadNext methods internally.
|
|
603
1009
|
"""
|
|
1010
|
+
# Use the generalized downloader to transfer the data
|
|
604
1011
|
self._generalized_downloader(
|
|
605
1012
|
address=address,
|
|
606
1013
|
address_ext=address_ext,
|
|
@@ -614,20 +1021,31 @@ class Master:
|
|
|
614
1021
|
callback=callback,
|
|
615
1022
|
)
|
|
616
1023
|
|
|
617
|
-
def flash_program(self, address: int, data: bytes, callback:
|
|
618
|
-
"""Convenience function for flash
|
|
619
|
-
|
|
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.
|
|
620
1031
|
|
|
621
1032
|
Parameters
|
|
622
1033
|
----------
|
|
623
|
-
address: int
|
|
624
|
-
|
|
1034
|
+
address : int
|
|
1035
|
+
The memory address to program
|
|
625
1036
|
data : bytes
|
|
626
|
-
|
|
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
|
|
627
1041
|
|
|
628
|
-
|
|
629
|
-
|
|
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).
|
|
630
1047
|
"""
|
|
1048
|
+
# Use the generalized downloader to program the flash
|
|
631
1049
|
self._generalized_downloader(
|
|
632
1050
|
address=address,
|
|
633
1051
|
data=data,
|
|
@@ -649,110 +1067,252 @@ class Master:
|
|
|
649
1067
|
maxBs: int,
|
|
650
1068
|
minSt: int,
|
|
651
1069
|
master_block_mode: bool,
|
|
652
|
-
dl_func,
|
|
653
|
-
dl_next_func,
|
|
654
|
-
callback=None,
|
|
655
|
-
):
|
|
656
|
-
"""
|
|
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
|
|
657
1105
|
self.setMta(address, address_ext)
|
|
658
|
-
|
|
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
|
|
659
1111
|
block_downloader = functools.partial(
|
|
660
1112
|
self._block_downloader,
|
|
661
1113
|
dl_func=dl_func,
|
|
662
1114
|
dl_next_func=dl_next_func,
|
|
663
|
-
minSt=
|
|
1115
|
+
minSt=minSt_seconds,
|
|
664
1116
|
)
|
|
1117
|
+
|
|
1118
|
+
# Calculate total length and maximum payload size
|
|
665
1119
|
total_length = len(data)
|
|
666
1120
|
if master_block_mode:
|
|
667
1121
|
max_payload = min(maxBs * (maxCto - 2), 255)
|
|
668
1122
|
else:
|
|
669
1123
|
max_payload = maxCto - 2
|
|
1124
|
+
|
|
1125
|
+
# Initialize offset
|
|
670
1126
|
offset = 0
|
|
1127
|
+
|
|
1128
|
+
# Handle master block mode
|
|
671
1129
|
if master_block_mode:
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
percent_complete = 1
|
|
675
|
-
remaining_block_size = total_length % max_payload
|
|
676
|
-
for _ in blocks:
|
|
677
|
-
block = data[offset : offset + max_payload]
|
|
678
|
-
block_downloader(block)
|
|
679
|
-
offset += max_payload
|
|
680
|
-
remaining -= max_payload
|
|
681
|
-
if callback and remaining <= total_length - (total_length / 100) * percent_complete:
|
|
682
|
-
callback(percent_complete)
|
|
683
|
-
percent_complete += 1
|
|
684
|
-
if remaining_block_size:
|
|
685
|
-
block = data[offset : offset + remaining_block_size]
|
|
686
|
-
block_downloader(block)
|
|
687
|
-
if callback:
|
|
688
|
-
callback(percent_complete)
|
|
1130
|
+
self._download_master_block_mode(data, total_length, max_payload, offset, block_downloader, callback)
|
|
1131
|
+
# Handle normal mode
|
|
689
1132
|
else:
|
|
690
|
-
|
|
691
|
-
chunks = range(total_length // chunk_size)
|
|
692
|
-
remaining = total_length % chunk_size
|
|
693
|
-
percent_complete = 1
|
|
694
|
-
callback_remaining = total_length
|
|
695
|
-
for _ in chunks:
|
|
696
|
-
block = data[offset : offset + max_payload]
|
|
697
|
-
dl_func(block, max_payload, last=False)
|
|
698
|
-
offset += max_payload
|
|
699
|
-
callback_remaining -= chunk_size
|
|
700
|
-
if callback and callback_remaining <= total_length - (total_length / 100) * percent_complete:
|
|
701
|
-
callback(percent_complete)
|
|
702
|
-
percent_complete += 1
|
|
703
|
-
if remaining:
|
|
704
|
-
block = data[offset : offset + remaining]
|
|
705
|
-
dl_func(block, remaining, last=True)
|
|
706
|
-
if callback:
|
|
707
|
-
callback(percent_complete)
|
|
1133
|
+
self._download_normal_mode(data, total_length, max_payload, offset, dl_func, callback)
|
|
708
1134
|
|
|
709
|
-
def
|
|
710
|
-
self,
|
|
711
|
-
|
|
712
|
-
|
|
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.
|
|
1145
|
+
|
|
1146
|
+
Parameters
|
|
1147
|
+
----------
|
|
1148
|
+
data : bytes
|
|
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.
|
|
713
1196
|
|
|
714
1197
|
Parameters
|
|
715
1198
|
----------
|
|
716
1199
|
data : bytes
|
|
717
|
-
|
|
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
|
|
1218
|
+
|
|
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
|
|
718
1225
|
|
|
719
|
-
|
|
720
|
-
|
|
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
|
|
721
1230
|
|
|
722
|
-
|
|
723
|
-
|
|
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)
|
|
724
1237
|
|
|
725
|
-
|
|
726
|
-
|
|
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.
|
|
1246
|
+
|
|
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.
|
|
1251
|
+
|
|
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
|
|
727
1264
|
"""
|
|
1265
|
+
# Calculate sizes and offsets
|
|
728
1266
|
length = len(data)
|
|
729
1267
|
max_packet_size = self.slaveProperties.maxCto - 2 # Command ID + Length
|
|
730
1268
|
packets = range(length // max_packet_size)
|
|
731
1269
|
offset = 0
|
|
732
1270
|
remaining = length % max_packet_size
|
|
733
1271
|
remaining_block_size = length
|
|
1272
|
+
|
|
1273
|
+
# Process full packets
|
|
734
1274
|
index = 0
|
|
735
1275
|
for index in packets:
|
|
1276
|
+
# Extract packet data
|
|
736
1277
|
packet_data = data[offset : offset + max_packet_size]
|
|
1278
|
+
|
|
1279
|
+
# Determine if this is the last packet
|
|
737
1280
|
last = (remaining_block_size - max_packet_size) == 0
|
|
1281
|
+
|
|
1282
|
+
# Send packet using appropriate function
|
|
738
1283
|
if index == 0:
|
|
739
|
-
|
|
1284
|
+
# First packet: use dl_func and transmit the complete length
|
|
1285
|
+
dl_func(packet_data, length, last)
|
|
740
1286
|
else:
|
|
1287
|
+
# Subsequent packets: use dl_next_func
|
|
741
1288
|
dl_next_func(packet_data, remaining_block_size, last)
|
|
1289
|
+
|
|
1290
|
+
# Update offsets and remaining size
|
|
742
1291
|
offset += max_packet_size
|
|
743
1292
|
remaining_block_size -= max_packet_size
|
|
1293
|
+
|
|
1294
|
+
# Apply minimum separation time
|
|
744
1295
|
delay(minSt)
|
|
1296
|
+
|
|
1297
|
+
# Process remaining partial packet
|
|
745
1298
|
if remaining:
|
|
1299
|
+
# Extract remaining data
|
|
746
1300
|
packet_data = data[offset : offset + remaining]
|
|
1301
|
+
|
|
1302
|
+
# Send packet using appropriate function
|
|
747
1303
|
if index == 0:
|
|
748
|
-
#
|
|
1304
|
+
# If there were no full packets, use dl_func
|
|
1305
|
+
# (length of data is smaller than maxCto - 2)
|
|
749
1306
|
dl_func(packet_data, remaining, last=True)
|
|
750
1307
|
else:
|
|
1308
|
+
# Otherwise use dl_next_func
|
|
751
1309
|
dl_next_func(packet_data, remaining, last=True)
|
|
1310
|
+
|
|
1311
|
+
# Apply minimum separation time
|
|
752
1312
|
delay(minSt)
|
|
753
1313
|
|
|
754
1314
|
@wrapped
|
|
755
|
-
def download(self, data: bytes, block_mode_length:
|
|
1315
|
+
def download(self, data: bytes, block_mode_length: int | None = None, last: bool = False):
|
|
756
1316
|
"""Transfer data from master to slave.
|
|
757
1317
|
|
|
758
1318
|
Parameters
|
|
@@ -1757,7 +2317,10 @@ class Master:
|
|
|
1757
2317
|
def getCurrentProtectionStatus(self):
|
|
1758
2318
|
""""""
|
|
1759
2319
|
if self.currentProtectionStatus is None:
|
|
1760
|
-
|
|
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}
|
|
1761
2324
|
self._setProtectionStatus(status.resourceProtectionStatus)
|
|
1762
2325
|
return self.currentProtectionStatus
|
|
1763
2326
|
|
|
@@ -1879,7 +2442,7 @@ class Master:
|
|
|
1879
2442
|
value = self.fetch(gid.length)
|
|
1880
2443
|
return decode_bytes(value)
|
|
1881
2444
|
|
|
1882
|
-
def id_scanner(self, scan_ranges:
|
|
2445
|
+
def id_scanner(self, scan_ranges: Collection[Collection[int]] | None = None) -> dict[str, str]:
|
|
1883
2446
|
"""Scan for available standard identification types (GET_ID).
|
|
1884
2447
|
|
|
1885
2448
|
Parameters
|
|
@@ -1929,18 +2492,34 @@ class Master:
|
|
|
1929
2492
|
name = STD_IDS[id_value]
|
|
1930
2493
|
else:
|
|
1931
2494
|
name = f"USER_{idx}"
|
|
1932
|
-
yield
|
|
2495
|
+
yield (
|
|
2496
|
+
id_value,
|
|
2497
|
+
name,
|
|
2498
|
+
)
|
|
1933
2499
|
|
|
1934
2500
|
return generate()
|
|
1935
2501
|
|
|
1936
2502
|
gen = make_generator(scan_ranges)
|
|
1937
2503
|
for id_value, name in gen:
|
|
1938
|
-
|
|
2504
|
+
# Avoid noisy warnings while probing
|
|
2505
|
+
status, response = self.try_command(self.identifier, id_value, silent=True)
|
|
1939
2506
|
if status == types.TryCommandResult.OK and response:
|
|
1940
2507
|
result[name] = response
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
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:
|
|
1944
2523
|
raise RuntimeError(f"Error while scanning for ID {id_value}: {response!r}")
|
|
1945
2524
|
return result
|
|
1946
2525
|
|
|
@@ -1949,7 +2528,7 @@ class Master:
|
|
|
1949
2528
|
""""""
|
|
1950
2529
|
return self.transport.start_datetime
|
|
1951
2530
|
|
|
1952
|
-
def try_command(self, cmd: Callable, *args, **kws) ->
|
|
2531
|
+
def try_command(self, cmd: Callable, *args, **kws) -> tuple[types.TryCommandResult, Any]:
|
|
1953
2532
|
"""Call master functions and handle XCP errors more gracefuly.
|
|
1954
2533
|
|
|
1955
2534
|
Parameter
|
|
@@ -1972,19 +2551,24 @@ class Master:
|
|
|
1972
2551
|
is normal for this kind of applications -- or to test for optional commands.
|
|
1973
2552
|
Use carefuly not to hide serious error causes.
|
|
1974
2553
|
"""
|
|
2554
|
+
# Suppress logging of expected XCP negative responses during try_command
|
|
2555
|
+
_prev_suppress = is_suppress_xcp_error_log()
|
|
2556
|
+
set_suppress_xcp_error_log(True)
|
|
1975
2557
|
try:
|
|
1976
|
-
extra_msg:
|
|
2558
|
+
extra_msg: str | None = kws.get("extra_msg")
|
|
1977
2559
|
if extra_msg:
|
|
1978
2560
|
kws.pop("extra_msg")
|
|
1979
2561
|
else:
|
|
1980
2562
|
extra_msg = ""
|
|
1981
|
-
silent:
|
|
2563
|
+
silent: bool | None = kws.get("silent")
|
|
1982
2564
|
if silent:
|
|
1983
2565
|
kws.pop("silent")
|
|
1984
2566
|
else:
|
|
1985
2567
|
silent = False
|
|
1986
2568
|
res = cmd(*args, **kws)
|
|
1987
2569
|
except SystemExit as e:
|
|
2570
|
+
# restore suppression flag before handling
|
|
2571
|
+
set_suppress_xcp_error_log(_prev_suppress)
|
|
1988
2572
|
# print(f"\tUnexpected error while executing command {cmd.__name__!r}: {e!r}")
|
|
1989
2573
|
if e.error_code == types.XcpError.ERR_CMD_UNKNOWN:
|
|
1990
2574
|
# This is a rather common use-case, so let the user know that there is some functionality missing.
|
|
@@ -2000,6 +2584,10 @@ class Master:
|
|
|
2000
2584
|
return (types.TryCommandResult.OTHER_ERROR, e)
|
|
2001
2585
|
else:
|
|
2002
2586
|
return (types.TryCommandResult.OK, res)
|
|
2587
|
+
finally:
|
|
2588
|
+
# Ensure suppression flag is restored even on success/other exceptions
|
|
2589
|
+
with suppress(Exception):
|
|
2590
|
+
set_suppress_xcp_error_log(_prev_suppress)
|
|
2003
2591
|
|
|
2004
2592
|
|
|
2005
2593
|
def ticks_to_seconds(ticks, resolution):
|