pyxcp 0.25.2__cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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.
Files changed (151) hide show
  1. pyxcp/__init__.py +20 -0
  2. pyxcp/aml/EtasCANMonitoring.a2l +82 -0
  3. pyxcp/aml/EtasCANMonitoring.aml +67 -0
  4. pyxcp/aml/XCP_Common.aml +408 -0
  5. pyxcp/aml/XCPonCAN.aml +78 -0
  6. pyxcp/aml/XCPonEth.aml +33 -0
  7. pyxcp/aml/XCPonFlx.aml +113 -0
  8. pyxcp/aml/XCPonSxI.aml +66 -0
  9. pyxcp/aml/XCPonUSB.aml +106 -0
  10. pyxcp/aml/ifdata_CAN.a2l +20 -0
  11. pyxcp/aml/ifdata_Eth.a2l +11 -0
  12. pyxcp/aml/ifdata_Flx.a2l +94 -0
  13. pyxcp/aml/ifdata_SxI.a2l +13 -0
  14. pyxcp/aml/ifdata_USB.a2l +81 -0
  15. pyxcp/asam/__init__.py +0 -0
  16. pyxcp/asam/types.py +131 -0
  17. pyxcp/asamkeydll +0 -0
  18. pyxcp/asamkeydll.c +116 -0
  19. pyxcp/asamkeydll.sh +2 -0
  20. pyxcp/checksum.py +732 -0
  21. pyxcp/cmdline.py +83 -0
  22. pyxcp/config/__init__.py +1257 -0
  23. pyxcp/config/legacy.py +120 -0
  24. pyxcp/constants.py +47 -0
  25. pyxcp/cpp_ext/__init__.py +0 -0
  26. pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
  27. pyxcp/cpp_ext/bin.hpp +105 -0
  28. pyxcp/cpp_ext/blockmem.hpp +58 -0
  29. pyxcp/cpp_ext/cpp_ext.cpython-310-x86_64-linux-gnu.so +0 -0
  30. pyxcp/cpp_ext/cpp_ext.cpython-311-x86_64-linux-gnu.so +0 -0
  31. pyxcp/cpp_ext/cpp_ext.cpython-312-x86_64-linux-gnu.so +0 -0
  32. pyxcp/cpp_ext/cpp_ext.cpython-313-x86_64-linux-gnu.so +0 -0
  33. pyxcp/cpp_ext/daqlist.hpp +374 -0
  34. pyxcp/cpp_ext/event.hpp +67 -0
  35. pyxcp/cpp_ext/extension_wrapper.cpp +131 -0
  36. pyxcp/cpp_ext/framing.hpp +360 -0
  37. pyxcp/cpp_ext/helper.hpp +280 -0
  38. pyxcp/cpp_ext/mcobject.hpp +248 -0
  39. pyxcp/cpp_ext/sxi_framing.hpp +332 -0
  40. pyxcp/cpp_ext/tsqueue.hpp +46 -0
  41. pyxcp/daq_stim/__init__.py +306 -0
  42. pyxcp/daq_stim/optimize/__init__.py +67 -0
  43. pyxcp/daq_stim/optimize/binpacking.py +41 -0
  44. pyxcp/daq_stim/scheduler.cpp +62 -0
  45. pyxcp/daq_stim/scheduler.hpp +75 -0
  46. pyxcp/daq_stim/stim.cpp +13 -0
  47. pyxcp/daq_stim/stim.cpython-310-x86_64-linux-gnu.so +0 -0
  48. pyxcp/daq_stim/stim.cpython-311-x86_64-linux-gnu.so +0 -0
  49. pyxcp/daq_stim/stim.cpython-312-x86_64-linux-gnu.so +0 -0
  50. pyxcp/daq_stim/stim.cpython-313-x86_64-linux-gnu.so +0 -0
  51. pyxcp/daq_stim/stim.hpp +604 -0
  52. pyxcp/daq_stim/stim_wrapper.cpp +50 -0
  53. pyxcp/dllif.py +100 -0
  54. pyxcp/errormatrix.py +878 -0
  55. pyxcp/examples/conf_can.toml +19 -0
  56. pyxcp/examples/conf_can_user.toml +16 -0
  57. pyxcp/examples/conf_can_vector.json +11 -0
  58. pyxcp/examples/conf_can_vector.toml +11 -0
  59. pyxcp/examples/conf_eth.toml +9 -0
  60. pyxcp/examples/conf_nixnet.json +20 -0
  61. pyxcp/examples/conf_socket_can.toml +12 -0
  62. pyxcp/examples/run_daq.py +165 -0
  63. pyxcp/examples/xcp_policy.py +60 -0
  64. pyxcp/examples/xcp_read_benchmark.py +38 -0
  65. pyxcp/examples/xcp_skel.py +48 -0
  66. pyxcp/examples/xcp_unlock.py +38 -0
  67. pyxcp/examples/xcp_user_supplied_driver.py +43 -0
  68. pyxcp/examples/xcphello.py +79 -0
  69. pyxcp/examples/xcphello_recorder.py +107 -0
  70. pyxcp/master/__init__.py +10 -0
  71. pyxcp/master/errorhandler.py +677 -0
  72. pyxcp/master/master.py +2645 -0
  73. pyxcp/py.typed +0 -0
  74. pyxcp/recorder/.idea/.gitignore +8 -0
  75. pyxcp/recorder/.idea/misc.xml +4 -0
  76. pyxcp/recorder/.idea/modules.xml +8 -0
  77. pyxcp/recorder/.idea/recorder.iml +6 -0
  78. pyxcp/recorder/.idea/sonarlint/issuestore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +7 -0
  79. pyxcp/recorder/.idea/sonarlint/issuestore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
  80. pyxcp/recorder/.idea/sonarlint/issuestore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
  81. pyxcp/recorder/.idea/sonarlint/issuestore/index.pb +7 -0
  82. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +0 -0
  83. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
  84. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
  85. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/index.pb +7 -0
  86. pyxcp/recorder/.idea/vcs.xml +10 -0
  87. pyxcp/recorder/__init__.py +96 -0
  88. pyxcp/recorder/build_clang.cmd +1 -0
  89. pyxcp/recorder/build_clang.sh +2 -0
  90. pyxcp/recorder/build_gcc.cmd +1 -0
  91. pyxcp/recorder/build_gcc.sh +2 -0
  92. pyxcp/recorder/build_gcc_arm.sh +2 -0
  93. pyxcp/recorder/converter/__init__.py +445 -0
  94. pyxcp/recorder/lz4.c +2829 -0
  95. pyxcp/recorder/lz4.h +879 -0
  96. pyxcp/recorder/lz4hc.c +2041 -0
  97. pyxcp/recorder/lz4hc.h +413 -0
  98. pyxcp/recorder/mio.hpp +1714 -0
  99. pyxcp/recorder/reader.hpp +138 -0
  100. pyxcp/recorder/reco.py +278 -0
  101. pyxcp/recorder/recorder.rst +0 -0
  102. pyxcp/recorder/rekorder.cpp +59 -0
  103. pyxcp/recorder/rekorder.cpython-310-x86_64-linux-gnu.so +0 -0
  104. pyxcp/recorder/rekorder.cpython-311-x86_64-linux-gnu.so +0 -0
  105. pyxcp/recorder/rekorder.cpython-312-x86_64-linux-gnu.so +0 -0
  106. pyxcp/recorder/rekorder.cpython-313-x86_64-linux-gnu.so +0 -0
  107. pyxcp/recorder/rekorder.hpp +274 -0
  108. pyxcp/recorder/setup.py +41 -0
  109. pyxcp/recorder/test_reko.py +34 -0
  110. pyxcp/recorder/unfolder.hpp +1354 -0
  111. pyxcp/recorder/wrap.cpp +184 -0
  112. pyxcp/recorder/writer.hpp +302 -0
  113. pyxcp/scripts/__init__.py +0 -0
  114. pyxcp/scripts/pyxcp_probe_can_drivers.py +20 -0
  115. pyxcp/scripts/xcp_examples.py +64 -0
  116. pyxcp/scripts/xcp_fetch_a2l.py +40 -0
  117. pyxcp/scripts/xcp_id_scanner.py +18 -0
  118. pyxcp/scripts/xcp_info.py +144 -0
  119. pyxcp/scripts/xcp_profile.py +26 -0
  120. pyxcp/scripts/xmraw_converter.py +31 -0
  121. pyxcp/stim/__init__.py +0 -0
  122. pyxcp/tests/test_asam_types.py +24 -0
  123. pyxcp/tests/test_binpacking.py +186 -0
  124. pyxcp/tests/test_can.py +1324 -0
  125. pyxcp/tests/test_checksum.py +95 -0
  126. pyxcp/tests/test_daq.py +193 -0
  127. pyxcp/tests/test_daq_opt.py +426 -0
  128. pyxcp/tests/test_frame_padding.py +156 -0
  129. pyxcp/tests/test_framing.py +262 -0
  130. pyxcp/tests/test_master.py +2116 -0
  131. pyxcp/tests/test_transport.py +177 -0
  132. pyxcp/tests/test_utils.py +30 -0
  133. pyxcp/timing.py +60 -0
  134. pyxcp/transport/__init__.py +13 -0
  135. pyxcp/transport/base.py +484 -0
  136. pyxcp/transport/base_transport.hpp +0 -0
  137. pyxcp/transport/can.py +660 -0
  138. pyxcp/transport/eth.py +254 -0
  139. pyxcp/transport/sxi.py +209 -0
  140. pyxcp/transport/transport_ext.hpp +214 -0
  141. pyxcp/transport/transport_wrapper.cpp +249 -0
  142. pyxcp/transport/usb_transport.py +229 -0
  143. pyxcp/types.py +987 -0
  144. pyxcp/utils.py +127 -0
  145. pyxcp/vector/__init__.py +0 -0
  146. pyxcp/vector/map.py +82 -0
  147. pyxcp-0.25.2.dist-info/METADATA +341 -0
  148. pyxcp-0.25.2.dist-info/RECORD +151 -0
  149. pyxcp-0.25.2.dist-info/WHEEL +6 -0
  150. pyxcp-0.25.2.dist-info/entry_points.txt +9 -0
  151. pyxcp-0.25.2.dist-info/licenses/LICENSE +165 -0
pyxcp/master/master.py ADDED
@@ -0,0 +1,2645 @@
1
+ #!/usr/bin/env python
2
+ """Lowlevel API reflecting available XCP services.
3
+
4
+ .. note:: For technical reasons the API is split into two parts;
5
+ common methods (this file) and a Python version specific part.
6
+
7
+ .. [1] XCP Specification, Part 2 - Protocol Layer Specification
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import functools
13
+ import logging
14
+ import struct
15
+ import traceback
16
+ import warnings
17
+ from typing import Any, Callable, Collection, TypeVar
18
+
19
+ from pyxcp import checksum, types
20
+ from pyxcp.constants import (
21
+ makeBytePacker,
22
+ makeByteUnpacker,
23
+ makeDLongPacker,
24
+ makeDLongUnpacker,
25
+ makeDWordPacker,
26
+ makeDWordUnpacker,
27
+ makeWordPacker,
28
+ makeWordUnpacker,
29
+ )
30
+ from pyxcp.daq_stim.stim import DaqEventInfo, Stim
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
+ )
38
+ from pyxcp.transport.base import create_transport
39
+ from pyxcp.utils import decode_bytes, delay, short_sleep
40
+
41
+
42
+ # Type variables for better type hinting
43
+ T = TypeVar("T")
44
+ R = TypeVar("R")
45
+
46
+
47
+ def broadcasted(func: Callable):
48
+ """"""
49
+ return func
50
+
51
+
52
+ class SlaveProperties(dict):
53
+ """Container class for fixed parameters, like byte-order, maxCTO, ...
54
+
55
+ This class extends dict to provide attribute-style access to dictionary items.
56
+ """
57
+
58
+ def __init__(self, *args: Any, **kws: Any) -> None:
59
+ """Initialize a new SlaveProperties instance.
60
+
61
+ Parameters
62
+ ----------
63
+ *args : Any
64
+ Positional arguments passed to dict.__init__
65
+ **kws : Any
66
+ Keyword arguments passed to dict.__init__
67
+ """
68
+ super().__init__(*args, **kws)
69
+
70
+ def __getattr__(self, name: str) -> Any:
71
+ """Get an attribute by name.
72
+
73
+ Parameters
74
+ ----------
75
+ name : str
76
+ The name of the attribute to get
77
+
78
+ Returns
79
+ -------
80
+ Any
81
+ The value of the attribute
82
+ """
83
+ return self[name]
84
+
85
+ def __setattr__(self, name: str, value: Any) -> None:
86
+ """Set an attribute by name.
87
+
88
+ Parameters
89
+ ----------
90
+ name : str
91
+ The name of the attribute to set
92
+ value : Any
93
+ The value to set
94
+ """
95
+ self[name] = value
96
+
97
+ def __getstate__(self) -> dict:
98
+ """Get the state of the object for pickling.
99
+
100
+ Returns
101
+ -------
102
+ dict
103
+ The state of the object
104
+ """
105
+ return self
106
+
107
+ def __setstate__(self, state: dict) -> None:
108
+ """Set the state of the object from unpickling.
109
+
110
+ Parameters
111
+ ----------
112
+ state : dict
113
+ The state to set
114
+ """
115
+ self.update(state) # Use update instead of direct assignment
116
+
117
+
118
+ class Master:
119
+ """Common part of lowlevel XCP API.
120
+
121
+ This class provides methods for interacting with an XCP slave device.
122
+ It handles the communication protocol and provides a high-level API
123
+ for sending commands and receiving responses.
124
+
125
+ Parameters
126
+ ----------
127
+ transport_name : str | None
128
+ XCP transport layer name ['can', 'eth', 'sxi']
129
+ config : Any
130
+ Configuration object containing transport and general settings
131
+ policy : Any, optional
132
+ Policy object for handling frames, by default None
133
+ transport_layer_interface : Any, optional
134
+ Custom transport layer interface, by default None
135
+ """
136
+
137
+ def __init__(self, transport_name: str | None, config: Any, policy: Any = None, transport_layer_interface: Any = None) -> None:
138
+ """Initialize a new Master instance.
139
+
140
+ Parameters
141
+ ----------
142
+ transport_name : str | None
143
+ XCP transport layer name ['can', 'eth', 'sxi']
144
+ config : Any
145
+ Configuration object containing transport and general settings
146
+ policy : Any, optional
147
+ Policy object for handling frames, by default None
148
+ transport_layer_interface : Any, optional
149
+ Custom transport layer interface, by default None
150
+
151
+ Raises
152
+ ------
153
+ ValueError
154
+ If transport_name is None
155
+ """
156
+ if transport_name is None:
157
+ raise ValueError("No transport-layer selected") # Never reached -- to keep type-checkers happy.
158
+
159
+ # Initialize basic properties
160
+ self.ctr: int = 0
161
+ self.succeeded: bool = True
162
+ self.config: Any = config.general
163
+ self.logger: logging.Logger = logging.getLogger("PyXCP")
164
+
165
+ # Configure error handling
166
+ disable_error_handling(self.config.disable_error_handling)
167
+
168
+ # Set up transport layer
169
+ self.transport_name: str = transport_name.lower()
170
+ transport_config: Any = config.transport
171
+ self.transport: BaseTransport = create_transport(transport_name, transport_config, policy, transport_layer_interface)
172
+
173
+ # Set up STIM (stimulation) support
174
+ self.stim: Stim = Stim(self.config.stim_support)
175
+ self.stim.clear()
176
+ self.stim.set_policy_feeder(self.transport.policy.feed)
177
+ self.stim.set_frame_sender(self.transport.block_request)
178
+
179
+ # In some cases the transport-layer needs to communicate with us.
180
+ self.transport.parent = self
181
+ self.service: Any = None
182
+
183
+ # Policies may issue XCP commands on their own.
184
+ self.transport.policy.xcp_master = self
185
+
186
+ # (D)Word (un-)packers are byte-order dependent
187
+ # -- byte-order is returned by CONNECT_Resp (COMM_MODE_BASIC)
188
+ self.BYTE_pack: Callable[[int], bytes] | None = None
189
+ self.BYTE_unpack: Callable[[bytes], tuple[int]] | None = None
190
+ self.WORD_pack: Callable[[int], bytes] | None = None
191
+ self.WORD_unpack: Callable[[bytes], tuple[int]] | None = None
192
+ self.DWORD_pack: Callable[[int], bytes] | None = None
193
+ self.DWORD_unpack: Callable[[bytes], tuple[int]] | None = None
194
+ self.DLONG_pack: Callable[[int], bytes] | None = None
195
+ self.DLONG_unpack: Callable[[bytes], tuple[int]] | None = None
196
+ self.AG_pack: Callable[[int], bytes] | None = None
197
+ self.AG_unpack: Callable[[bytes], tuple[int]] | None = None
198
+
199
+ # Initialize state variables
200
+ self.mta: types.MtaType = types.MtaType(None, None)
201
+ self.currentDaqPtr: Any = None
202
+ self.currentProtectionStatus: dict[str, bool] | None = None
203
+
204
+ # Configuration for seed and key
205
+ self.seed_n_key_dll: str | None = self.config.seed_n_key_dll
206
+ self.seed_n_key_function: Callable | None = self.config.seed_n_key_function
207
+ self.seed_n_key_dll_same_bit_width: bool = self.config.seed_n_key_dll_same_bit_width
208
+ self.disconnect_response_optional: bool = self.config.disconnect_response_optional
209
+
210
+ # Initialize slave properties
211
+ self.slaveProperties: SlaveProperties = SlaveProperties()
212
+ self.slaveProperties.pgmProcessor = SlaveProperties()
213
+ self.slaveProperties.transport_layer = self.transport_name.upper()
214
+
215
+ def __enter__(self) -> Master:
216
+ """Context manager entry part.
217
+
218
+ This method is called when entering a context manager block.
219
+ It connects to the XCP slave and returns the Master instance.
220
+
221
+ Returns
222
+ -------
223
+ Master
224
+ The Master instance
225
+ """
226
+ self.transport.connect()
227
+ return self
228
+
229
+ def __exit__(
230
+ self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: traceback.TracebackType | None
231
+ ) -> None:
232
+ """Context manager exit part.
233
+
234
+ This method is called when exiting a context manager block.
235
+ It closes the connection to the XCP slave and logs any exceptions.
236
+
237
+ Parameters
238
+ ----------
239
+ exc_type : type[BaseException] | None
240
+ The type of the exception that was raised, or None if no exception was raised
241
+ exc_val : BaseException | None
242
+ The exception instance that was raised, or None if no exception was raised
243
+ exc_tb : traceback.TracebackType | None
244
+ The traceback of the exception that was raised, or None if no exception was raised
245
+ """
246
+ # Close the connection to the XCP slave
247
+ self.close()
248
+
249
+ # Handle any exceptions that were raised
250
+ if exc_type is not None:
251
+ self.succeeded = False
252
+ self.logger.error("".join(traceback.format_exception(exc_type, exc_val, exc_tb)))
253
+
254
+ def _setService(self, service: Any) -> None:
255
+ """Record the currently processed service.
256
+
257
+ This method is called by the transport layer to record the
258
+ currently processed service.
259
+
260
+ Parameters
261
+ ----------
262
+ service : Any
263
+ The service being processed, typically a `pyxcp.types.Command`
264
+
265
+ Note
266
+ ----
267
+ Internal Function, only to be used by transport-layer.
268
+ """
269
+ self.service = service
270
+
271
+ def close(self) -> None:
272
+ """Close the transport layer connection.
273
+
274
+ This method finalizes the policy and closes the transport layer connection.
275
+ It should be called when the Master instance is no longer needed.
276
+ """
277
+ self.transport.policy.finalize()
278
+ self.transport.close()
279
+
280
+ # Mandatory Commands.
281
+ @wrapped
282
+ def connect(self, mode: int = 0x00) -> types.ConnectResponse:
283
+ """Build up connection to an XCP slave.
284
+
285
+ Before the actual XCP traffic starts a connection is required.
286
+ This method sends a CONNECT command to the slave and processes
287
+ the response to set up various properties of the slave.
288
+
289
+ Parameters
290
+ ----------
291
+ mode : int, optional
292
+ Connection mode, by default 0x00 (normal mode)
293
+
294
+ Returns
295
+ -------
296
+ types.ConnectResponse
297
+ Response object containing fundamental client properties
298
+
299
+ Note
300
+ ----
301
+ Every XCP slave supports at most one connection,
302
+ more attempts to connect are silently ignored.
303
+ """
304
+ # Send CONNECT command to the slave
305
+ response = self.transport.request(types.Command.CONNECT, mode & 0xFF)
306
+
307
+ # First get byte-order from partial response
308
+ result_partial = types.ConnectResponsePartial.parse(response)
309
+ byte_order = result_partial.commModeBasic.byteOrder
310
+
311
+ # Parse the full response with the correct byte order
312
+ result = types.ConnectResponse.parse(response, byteOrder=byte_order)
313
+
314
+ # Set up byte order dependent properties
315
+ self._setup_slave_properties(result, byte_order)
316
+
317
+ # Set up byte order dependent packers and unpackers
318
+ self._setup_packers_and_unpackers(byte_order)
319
+
320
+ # Set up address granularity dependent properties
321
+ self._setup_address_granularity()
322
+
323
+ return result
324
+
325
+ def _setup_slave_properties(self, result: types.ConnectResponse, byte_order: types.ByteOrder) -> None:
326
+ """Set up slave properties based on the connect response.
327
+
328
+ Parameters
329
+ ----------
330
+ result : types.ConnectResponse
331
+ The parsed connect response
332
+ byte_order : types.ByteOrder
333
+ The byte order reported by the slave
334
+ """
335
+ # Set basic properties
336
+ self.slaveProperties.byteOrder = byte_order
337
+ self.slaveProperties.maxCto = result.maxCto
338
+ self.slaveProperties.maxDto = result.maxDto
339
+
340
+ # Set resource support flags
341
+ self.slaveProperties.supportsPgm = result.resource.pgm
342
+ self.slaveProperties.supportsStim = result.resource.stim
343
+ self.slaveProperties.supportsDaq = result.resource.daq
344
+ self.slaveProperties.supportsCalpag = result.resource.calpag
345
+
346
+ # Set communication mode properties
347
+ self.slaveProperties.slaveBlockMode = result.commModeBasic.slaveBlockMode
348
+ self.slaveProperties.addressGranularity = result.commModeBasic.addressGranularity
349
+ self.slaveProperties.optionalCommMode = result.commModeBasic.optional
350
+
351
+ # Set version information
352
+ self.slaveProperties.protocolLayerVersion = result.protocolLayerVersion
353
+ self.slaveProperties.transportLayerVersion = result.transportLayerVersion
354
+
355
+ # Calculate derived properties
356
+ self.slaveProperties.maxWriteDaqMultipleElements = (
357
+ 0 if self.slaveProperties.maxCto < 10 else int((self.slaveProperties.maxCto - 2) // 8)
358
+ )
359
+
360
+ # Initialize bytesPerElement (will be set in _setup_address_granularity)
361
+ self.slaveProperties.bytesPerElement = None
362
+
363
+ def _setup_packers_and_unpackers(self, byte_order: types.ByteOrder) -> None:
364
+ """Set up byte order dependent packers and unpackers.
365
+
366
+ Parameters
367
+ ----------
368
+ byte_order : types.ByteOrder
369
+ The byte order reported by the slave
370
+ """
371
+ # Determine byte order prefix for struct format strings
372
+ byte_order_prefix = "<" if byte_order == types.ByteOrder.INTEL else ">"
373
+
374
+ # Create packers and unpackers for different data types
375
+ self.BYTE_pack = makeBytePacker(byte_order_prefix)
376
+ self.BYTE_unpack = makeByteUnpacker(byte_order_prefix)
377
+ self.WORD_pack = makeWordPacker(byte_order_prefix)
378
+ self.WORD_unpack = makeWordUnpacker(byte_order_prefix)
379
+ self.DWORD_pack = makeDWordPacker(byte_order_prefix)
380
+ self.DWORD_unpack = makeDWordUnpacker(byte_order_prefix)
381
+ self.DLONG_pack = makeDLongPacker(byte_order_prefix)
382
+ self.DLONG_unpack = makeDLongUnpacker(byte_order_prefix)
383
+
384
+ def _setup_address_granularity(self) -> None:
385
+ """Set up address granularity dependent properties and packers/unpackers."""
386
+ # Set up address granularity dependent packers and unpackers
387
+ if self.slaveProperties.addressGranularity == types.AddressGranularity.BYTE:
388
+ self.AG_pack = struct.Struct("<B").pack
389
+ self.AG_unpack = struct.Struct("<B").unpack
390
+ self.slaveProperties.bytesPerElement = 1
391
+ elif self.slaveProperties.addressGranularity == types.AddressGranularity.WORD:
392
+ self.AG_pack = self.WORD_pack
393
+ self.AG_unpack = self.WORD_unpack
394
+ self.slaveProperties.bytesPerElement = 2
395
+ elif self.slaveProperties.addressGranularity == types.AddressGranularity.DWORD:
396
+ self.AG_pack = self.DWORD_pack
397
+ self.AG_unpack = self.DWORD_unpack
398
+ self.slaveProperties.bytesPerElement = 4
399
+ # self.connected = True
400
+ status = self.getStatus()
401
+ if status.sessionStatus.daqRunning:
402
+ # TODO: resume
403
+ self.startStopSynch(0x00)
404
+
405
+ @wrapped
406
+ def disconnect(self) -> bytes:
407
+ """Release the connection to the XCP slave.
408
+
409
+ This method sends a DISCONNECT command to the slave, which releases
410
+ the connection. Thereafter, no further communication with the slave
411
+ is possible (besides `connect`).
412
+
413
+ Returns
414
+ -------
415
+ bytes
416
+ The raw response from the slave, typically empty
417
+
418
+ Note
419
+ -----
420
+ - If DISCONNECT is currently not possible, ERR_CMD_BUSY will be returned.
421
+ - While XCP spec. requires a response, this behavior can be made optional by adding
422
+ - `DISCONNECT_RESPONSE_OPTIONAL = true` (TOML)
423
+ - `"DISCONNECT_RESPONSE_OPTIONAL": true` (JSON)
424
+ to your configuration file.
425
+ """
426
+ # Send DISCONNECT command to the slave
427
+ if self.disconnect_response_optional:
428
+ response = self.transport.request_optional_response(types.Command.DISCONNECT)
429
+ else:
430
+ response = self.transport.request(types.Command.DISCONNECT)
431
+
432
+ return response
433
+
434
+ @wrapped
435
+ def getStatus(self) -> types.GetStatusResponse:
436
+ """Get current status information of the slave device.
437
+
438
+ This method sends a GET_STATUS command to the slave and processes
439
+ the response to get information about the current status of the slave.
440
+ This includes the status of the resource protection, pending store
441
+ requests and the general status of data acquisition and stimulation.
442
+
443
+ Returns
444
+ -------
445
+ types.GetStatusResponse
446
+ Response object containing status information
447
+ """
448
+ # Send GET_STATUS command to the slave
449
+ response = self.transport.request(types.Command.GET_STATUS)
450
+
451
+ # Parse the response with the correct byte order
452
+ result = types.GetStatusResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
453
+
454
+ # Update the current protection status
455
+ self._setProtectionStatus(result.resourceProtectionStatus)
456
+
457
+ return result
458
+
459
+ @wrapped
460
+ def synch(self) -> bytes:
461
+ """Synchronize command execution after timeout conditions.
462
+
463
+ This method sends a SYNCH command to the slave, which synchronizes
464
+ command execution after timeout conditions. This is useful when
465
+ the slave has timed out and needs to be resynchronized.
466
+
467
+ Returns
468
+ -------
469
+ bytes
470
+ The raw response from the slave
471
+ """
472
+ # Send SYNCH command to the slave
473
+ response = self.transport.request(types.Command.SYNCH)
474
+ return response
475
+
476
+ @wrapped
477
+ def getCommModeInfo(self) -> types.GetCommModeInfoResponse:
478
+ """Get optional information on different Communication Modes supported
479
+ by the slave.
480
+
481
+ This method sends a GET_COMM_MODE_INFO command to the slave and processes
482
+ the response to get information about the communication modes supported
483
+ by the slave.
484
+
485
+ Returns
486
+ -------
487
+ types.GetCommModeInfoResponse
488
+ Response object containing communication mode information
489
+ """
490
+ # Send GET_COMM_MODE_INFO command to the slave
491
+ response = self.transport.request(types.Command.GET_COMM_MODE_INFO)
492
+
493
+ # Parse the response with the correct byte order
494
+ result = types.GetCommModeInfoResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
495
+
496
+ # Update slave properties with communication mode information
497
+ self._update_comm_mode_properties(result)
498
+
499
+ return result
500
+
501
+ def _update_comm_mode_properties(self, result: types.GetCommModeInfoResponse) -> None:
502
+ """Update slave properties with communication mode information.
503
+
504
+ Parameters
505
+ ----------
506
+ result : types.GetCommModeInfoResponse
507
+ The parsed GET_COMM_MODE_INFO response
508
+ """
509
+ # Set optional communication mode properties
510
+ self.slaveProperties.interleavedMode = result.commModeOptional.interleavedMode
511
+ self.slaveProperties.masterBlockMode = result.commModeOptional.masterBlockMode
512
+
513
+ # Set basic communication properties
514
+ self.slaveProperties.maxBs = result.maxBs
515
+ self.slaveProperties.minSt = result.minSt
516
+ self.slaveProperties.queueSize = result.queueSize
517
+ self.slaveProperties.xcpDriverVersionNumber = result.xcpDriverVersionNumber
518
+
519
+ @wrapped
520
+ def getId(self, mode: int) -> types.GetIDResponse:
521
+ """Get identification information from the slave device.
522
+
523
+ This command is used for automatic session configuration and for
524
+ slave device identification. It sends a GET_ID command to the slave
525
+ and processes the response to get identification information.
526
+
527
+ Parameters
528
+ ----------
529
+ mode : int
530
+ The following identification types may be requested:
531
+ - 0 ASCII text
532
+ - 1 ASAM-MC2 filename without path and extension
533
+ - 2 ASAM-MC2 filename with path and extension
534
+ - 3 URL where the ASAM-MC2 file can be found
535
+ - 4 ASAM-MC2 file to upload
536
+ - 128..255 User defined
537
+
538
+ Returns
539
+ -------
540
+ types.GetIDResponse
541
+ Response object containing identification information
542
+ """
543
+ # Send GET_ID command to the slave
544
+ response = self.transport.request(types.Command.GET_ID, mode)
545
+
546
+ # Parse the response with the correct byte order
547
+ result = types.GetIDResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
548
+
549
+ # Extract the length from the response
550
+ result.length = self.DWORD_unpack(response[3:7])[0]
551
+
552
+ return result
553
+
554
+ @wrapped
555
+ def setRequest(self, mode: int, session_configuration_id: int) -> bytes:
556
+ """Request to save data to non-volatile memory.
557
+
558
+ This method sends a SET_REQUEST command to the slave, which requests
559
+ the slave to save data to non-volatile memory. The data to be saved
560
+ is specified by the mode parameter.
561
+
562
+ Parameters
563
+ ----------
564
+ mode : int (bitfield)
565
+ - 1 Request to store calibration data
566
+ - 2 Request to store DAQ list, no resume
567
+ - 4 Request to store DAQ list, resume enabled
568
+ - 8 Request to clear DAQ configuration
569
+ session_configuration_id : int
570
+ Identifier for the session configuration
571
+
572
+ Returns
573
+ -------
574
+ bytes
575
+ The raw response from the slave
576
+ """
577
+ # Send SET_REQUEST command to the slave
578
+ # Split the session_configuration_id into high and low bytes
579
+ return self.transport.request(
580
+ types.Command.SET_REQUEST,
581
+ mode,
582
+ session_configuration_id >> 8, # High byte
583
+ session_configuration_id & 0xFF, # Low byte
584
+ )
585
+
586
+ @wrapped
587
+ def getSeed(self, first: int, resource: int) -> types.GetSeedResponse:
588
+ """Get seed from slave for unlocking a protected resource.
589
+
590
+ This method sends a GET_SEED command to the slave, which returns a seed
591
+ that can be used to generate a key for unlocking a protected resource.
592
+ The seed is used as input to a key generation algorithm, and the resulting
593
+ key is sent back to the slave using the unlock method.
594
+
595
+ Parameters
596
+ ----------
597
+ first : int
598
+ - 0 - first part of seed
599
+ - 1 - remaining part
600
+ resource : int
601
+ - Mode == 0 - Resource to unlock
602
+ - Mode == 1 - Don't care
603
+
604
+ Returns
605
+ -------
606
+ types.GetSeedResponse
607
+ Response object containing the seed
608
+
609
+ Note
610
+ ----
611
+ For CAN transport, the seed may be split across multiple frames if it's
612
+ longer than the maximum DLC. In this case, the first byte of the response
613
+ indicates the remaining seed size, and the master must call getSeed
614
+ multiple times until the complete seed is received.
615
+ """
616
+ # Send GET_SEED command to the slave
617
+ response = self.transport.request(types.Command.GET_SEED, first, resource)
618
+
619
+ # Handle CAN-specific seed format
620
+ if self.transport_name == "can":
621
+ # For CAN it might happen that the seed is longer than the max DLC
622
+ # In this case the first byte will be the current remaining seed size
623
+ # followed by the seed bytes that can fit in the current frame
624
+ size, seed = response[0], response[1:]
625
+
626
+ # Truncate seed if necessary
627
+ if size < len(seed):
628
+ seed = seed[:size]
629
+
630
+ # Create and populate response object
631
+ reply = types.GetSeedResponse.parse(
632
+ types.GetSeedResponse.build({"length": size, "seed": bytes(size)}),
633
+ byteOrder=self.slaveProperties.byteOrder,
634
+ )
635
+ reply.seed = seed
636
+ return reply
637
+ else:
638
+ # For other transports, parse the response directly
639
+ return types.GetSeedResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
640
+
641
+ @wrapped
642
+ def unlock(self, length: int, key: bytes) -> types.ResourceType:
643
+ """Send key to slave for unlocking a protected resource.
644
+
645
+ This method sends an UNLOCK command to the slave, which attempts to
646
+ unlock a protected resource using the provided key. The key is generated
647
+ from the seed obtained using the getSeed method.
648
+
649
+ Parameters
650
+ ----------
651
+ length : int
652
+ Indicates the (remaining) number of key bytes
653
+ key : bytes
654
+ The key bytes to send to the slave
655
+
656
+ Returns
657
+ -------
658
+ types.ResourceType
659
+ Response object containing the resource protection status
660
+
661
+ Note
662
+ ----
663
+ The master has to use :meth:`unlock` in a defined sequence together
664
+ with :meth:`getSeed`. The master can only send an :meth:`unlock` sequence
665
+ if previously there was a :meth:`getSeed` sequence. The master has
666
+ to send the first `unlocking` after a :meth:`getSeed` sequence with
667
+ a Length containing the total length of the key.
668
+ """
669
+ # Send UNLOCK command to the slave
670
+ response = self.transport.request(types.Command.UNLOCK, length, *key)
671
+
672
+ # Parse the response with the correct byte order
673
+ result = types.ResourceType.parse(response, byteOrder=self.slaveProperties.byteOrder)
674
+
675
+ # Update the current protection status
676
+ self._setProtectionStatus(result)
677
+
678
+ return result
679
+
680
+ @wrapped
681
+ def setMta(self, address: int, address_ext: int = 0x00) -> bytes:
682
+ """Set Memory Transfer Address in slave.
683
+
684
+ This method sends a SET_MTA command to the slave, which sets the
685
+ Memory Transfer Address (MTA) to the specified address. The MTA is
686
+ used by various commands that transfer data between the master and
687
+ the slave.
688
+
689
+ Parameters
690
+ ----------
691
+ address : int
692
+ The memory address to set
693
+ address_ext : int, optional
694
+ The address extension, by default 0x00
695
+
696
+ Returns
697
+ -------
698
+ bytes
699
+ The raw response from the slave
700
+
701
+ Note
702
+ ----
703
+ The MTA is used by :meth:`buildChecksum`, :meth:`upload`, :meth:`download`, :meth:`downloadNext`,
704
+ :meth:`downloadMax`, :meth:`modifyBits`, :meth:`programClear`, :meth:`program`, :meth:`programNext`
705
+ and :meth:`programMax`.
706
+ """
707
+ # Keep track of MTA (needed for error-handling)
708
+ self.mta = types.MtaType(address, address_ext)
709
+
710
+ # Pack the address into bytes
711
+ addr = self.DWORD_pack(address)
712
+
713
+ # Send SET_MTA command to the slave
714
+ return self.transport.request(types.Command.SET_MTA, 0, 0, address_ext, *addr)
715
+
716
+ @wrapped
717
+ def upload(self, length: int) -> bytes:
718
+ """Transfer data from slave to master.
719
+
720
+ This method sends an UPLOAD command to the slave, which transfers
721
+ data from the slave to the master. The data is read from the memory
722
+ address specified by the MTA, which must be set before calling this
723
+ method.
724
+
725
+ Parameters
726
+ ----------
727
+ length : int
728
+ Number of elements (address granularity) to upload
729
+
730
+ Returns
731
+ -------
732
+ bytes
733
+ The uploaded data
734
+
735
+ Note
736
+ ----
737
+ Address is set via :meth:`setMta` (Some services like :meth:`getID` also set the MTA).
738
+ """
739
+ # Calculate the number of bytes to upload
740
+ byte_count = length * self.slaveProperties.bytesPerElement
741
+
742
+ # Send UPLOAD command to the slave
743
+ response = self.transport.request(types.Command.UPLOAD, length)
744
+
745
+ # Handle block mode for large uploads
746
+ if byte_count > (self.slaveProperties.maxCto - 1):
747
+ # Receive the remaining bytes in block mode
748
+ block_response = self.transport.block_receive(length_required=(byte_count - len(response)))
749
+ response += block_response
750
+ # Handle CAN-specific upload format
751
+ elif self.transport_name == "can":
752
+ # Larger sizes will send in multiple CAN messages
753
+ # Each valid message will start with 0xFF followed by the upload bytes
754
+ # The last message might be padded to the required DLC
755
+ remaining_bytes = byte_count - len(response)
756
+ while remaining_bytes:
757
+ if len(self.transport.resQueue):
758
+ data = self.transport.resQueue.popleft()
759
+ response += data[1 : remaining_bytes + 1]
760
+ remaining_bytes = byte_count - len(response)
761
+ else:
762
+ short_sleep()
763
+
764
+ return response
765
+
766
+ @wrapped
767
+ def shortUpload(self, length: int, address: int, address_ext: int = 0x00) -> bytes:
768
+ """Transfer data from slave to master with address information.
769
+
770
+ This method sends a SHORT_UPLOAD command to the slave, which transfers
771
+ data from the slave to the master. Unlike the :meth:`upload` method,
772
+ this method includes the address information in the command, so it
773
+ doesn't require setting the MTA first.
774
+
775
+ Parameters
776
+ ----------
777
+ length : int
778
+ Number of elements (address granularity) to upload
779
+ address : int
780
+ The memory address to read from
781
+ address_ext : int, optional
782
+ The address extension, by default 0x00
783
+
784
+ Returns
785
+ -------
786
+ bytes
787
+ The uploaded data
788
+ """
789
+ # Pack the address into bytes
790
+ addr = self.DWORD_pack(address)
791
+
792
+ # Calculate the number of bytes to upload
793
+ byte_count = length * self.slaveProperties.bytesPerElement
794
+ max_byte_count = self.slaveProperties.maxCto - 1
795
+
796
+ # Check if the requested byte count exceeds the maximum
797
+ if byte_count > max_byte_count:
798
+ self.logger.warn(f"SHORT_UPLOAD: {byte_count} bytes exceeds the maximum value of {max_byte_count}.")
799
+
800
+ # Send SHORT_UPLOAD command to the slave
801
+ response = self.transport.request(types.Command.SHORT_UPLOAD, length, 0, address_ext, *addr)
802
+
803
+ # Return only the requested number of bytes
804
+ return response[:byte_count]
805
+
806
+ @wrapped
807
+ def buildChecksum(self, blocksize: int) -> types.BuildChecksumResponse:
808
+ """Build checksum over memory range.
809
+
810
+ This method sends a BUILD_CHECKSUM command to the slave, which calculates
811
+ a checksum over a memory range. The memory range starts at the address
812
+ specified by the MTA and has a size of `blocksize` elements.
813
+
814
+ Parameters
815
+ ----------
816
+ blocksize : int
817
+ The number of elements (address granularity) to include in the checksum
818
+
819
+ Returns
820
+ -------
821
+ types.BuildChecksumResponse
822
+ Response object containing the checksum information
823
+
824
+ Note
825
+ ----
826
+ Address is set via :meth:`setMta`
827
+
828
+ See Also
829
+ --------
830
+ :mod:`~pyxcp.checksum`
831
+ """
832
+ # Pack the blocksize into bytes
833
+ bs = self.DWORD_pack(blocksize)
834
+
835
+ # Send BUILD_CHECKSUM command to the slave
836
+ response = self.transport.request(types.Command.BUILD_CHECKSUM, 0, 0, 0, *bs)
837
+
838
+ # Parse the response with the correct byte order
839
+ return types.BuildChecksumResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
840
+
841
+ @wrapped
842
+ def transportLayerCmd(self, sub_command: int, *data: bytes) -> bytes:
843
+ """Execute transfer-layer specific command.
844
+
845
+ This method sends a TRANSPORT_LAYER_CMD command to the slave, which
846
+ executes a transport-layer specific command. The exact behavior of
847
+ this command depends on the transport layer being used.
848
+
849
+ Parameters
850
+ ----------
851
+ sub_command : int
852
+ The sub-command to execute
853
+ *data : bytes
854
+ Variable number of data bytes to send with the command
855
+
856
+ Returns
857
+ -------
858
+ bytes
859
+ The raw response from the slave, or None if no response is expected
860
+
861
+ Note
862
+ ----
863
+ For details refer to XCP specification.
864
+ """
865
+ # Send TRANSPORT_LAYER_CMD command to the slave
866
+ return self.transport.request_optional_response(types.Command.TRANSPORT_LAYER_CMD, sub_command, *data)
867
+
868
+ @wrapped
869
+ def userCmd(self, sub_command: int, data: bytes) -> bytes:
870
+ """Execute proprietary command implemented in your XCP client.
871
+
872
+ This method sends a USER_CMD command to the slave, which executes
873
+ a proprietary command implemented in the XCP client. The exact behavior
874
+ of this command depends on the XCP client vendor.
875
+
876
+ Parameters
877
+ ----------
878
+ sub_command : int
879
+ The sub-command to execute
880
+ data : bytes
881
+ The data bytes to send with the command
882
+
883
+ Returns
884
+ -------
885
+ bytes
886
+ The raw response from the slave
887
+
888
+ Note
889
+ ----
890
+ For details refer to your XCP client vendor.
891
+ """
892
+ # Send USER_CMD command to the slave
893
+ return self.transport.request(types.Command.USER_CMD, sub_command, *data)
894
+
895
+ @wrapped
896
+ def getVersion(self) -> types.GetVersionResponse:
897
+ """Get version information from the slave.
898
+
899
+ This method sends a GET_VERSION command to the slave, which returns
900
+ detailed information about the implemented protocol layer version
901
+ of the XCP slave and the transport layer currently in use.
902
+
903
+ Returns
904
+ -------
905
+ types.GetVersionResponse
906
+ Response object containing version information
907
+ """
908
+ # Send GET_VERSION command to the slave
909
+ response = self.transport.request(types.Command.GET_VERSION)
910
+
911
+ # Parse the response with the correct byte order
912
+ result = types.GetVersionResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
913
+
914
+ # Update slave properties with version information
915
+ self._update_version_properties(result)
916
+
917
+ return result
918
+
919
+ def _update_version_properties(self, result: types.GetVersionResponse) -> None:
920
+ """Update slave properties with version information.
921
+
922
+ Parameters
923
+ ----------
924
+ result : types.GetVersionResponse
925
+ The parsed GET_VERSION response
926
+ """
927
+ # Set version information
928
+ self.slaveProperties.protocolMajor = result.protocolMajor
929
+ self.slaveProperties.protocolMinor = result.protocolMinor
930
+ self.slaveProperties.transportMajor = result.transportMajor
931
+ self.slaveProperties.transportMinor = result.transportMinor
932
+
933
+ def fetch(self, length: int, limit_payload: int = None) -> bytes: # TODO: pull
934
+ """Convenience function for data-transfer from slave to master.
935
+
936
+ This method transfers data from the slave to the master in chunks,
937
+ handling the details of breaking up large transfers into smaller
938
+ pieces. It's not part of the XCP Specification but provides a
939
+ convenient way to fetch data.
940
+
941
+ Parameters
942
+ ----------
943
+ length : int
944
+ The number of bytes to fetch
945
+ limit_payload : int, optional
946
+ Transfer less bytes than supported by transport-layer, by default None
947
+
948
+ Returns
949
+ -------
950
+ bytes
951
+ The fetched data
952
+
953
+ Raises
954
+ ------
955
+ ValueError
956
+ If limit_payload is less than 8 bytes
957
+
958
+ Note
959
+ ----
960
+ Address is not included because of services implicitly setting
961
+ address information like :meth:`getID`.
962
+ """
963
+ # Validate limit_payload
964
+ if limit_payload is not None and limit_payload < 8:
965
+ raise ValueError(f"Payload must be at least 8 bytes - given: {limit_payload}")
966
+
967
+ # Determine maximum payload size
968
+ slave_block_mode = self.slaveProperties.slaveBlockMode
969
+ max_payload = 255 if slave_block_mode else self.slaveProperties.maxCto - 1
970
+
971
+ # Apply limit_payload if specified
972
+ payload = min(limit_payload, max_payload) if limit_payload else max_payload
973
+
974
+ # Calculate number of chunks and remaining bytes
975
+ chunk_size = payload
976
+ chunks = range(length // chunk_size)
977
+ remaining = length % chunk_size
978
+
979
+ # Fetch data in chunks
980
+ result = []
981
+ for _ in chunks:
982
+ data = self.upload(chunk_size)
983
+ result.extend(data[:chunk_size])
984
+
985
+ # Fetch remaining bytes
986
+ if remaining:
987
+ data = self.upload(remaining)
988
+ result.extend(data[:remaining])
989
+
990
+ return bytes(result)
991
+
992
+ pull = fetch # fetch() may be completely replaced by pull() someday.
993
+
994
+ def push(self, address: int, address_ext: int, data: bytes, callback: Callable[[int], None] | None = None) -> None:
995
+ """Convenience function for data-transfer from master to slave.
996
+
997
+ This method transfers data from the master to the slave in chunks,
998
+ handling the details of breaking up large transfers into smaller
999
+ pieces. It's not part of the XCP Specification but provides a
1000
+ convenient way to push data.
1001
+
1002
+ Parameters
1003
+ ----------
1004
+ address : int
1005
+ The memory address to write to
1006
+ address_ext : int
1007
+ The address extension
1008
+ data : bytes
1009
+ The data bytes to write
1010
+ callback : Callable[[int], None], optional
1011
+ A callback function that is called with the percentage of completion,
1012
+ by default None
1013
+
1014
+ Note
1015
+ ----
1016
+ This method uses the download and downloadNext methods internally.
1017
+ """
1018
+ # Use the generalized downloader to transfer the data
1019
+ self._generalized_downloader(
1020
+ address=address,
1021
+ address_ext=address_ext,
1022
+ data=data,
1023
+ maxCto=self.slaveProperties.maxCto,
1024
+ maxBs=self.slaveProperties.maxBs,
1025
+ minSt=self.slaveProperties.minSt,
1026
+ master_block_mode=self.slaveProperties.masterBlockMode,
1027
+ dl_func=self.download,
1028
+ dl_next_func=self.downloadNext,
1029
+ callback=callback,
1030
+ )
1031
+
1032
+ def flash_program(self, address: int, data: bytes, callback: Callable[[int], None] | None = None) -> None:
1033
+ """Convenience function for flash programming.
1034
+
1035
+ This method programs flash memory on the slave in chunks, handling
1036
+ the details of breaking up large transfers into smaller pieces.
1037
+ It's not part of the XCP Specification but provides a convenient
1038
+ way to program flash memory.
1039
+
1040
+ Parameters
1041
+ ----------
1042
+ address : int
1043
+ The memory address to program
1044
+ data : bytes
1045
+ The data bytes to program
1046
+ callback : Callable[[int], None], optional
1047
+ A callback function that is called with the percentage of completion,
1048
+ by default None
1049
+
1050
+ Note
1051
+ ----
1052
+ This method uses the program and programNext methods internally.
1053
+ It automatically uses the programming-specific parameters from the
1054
+ slave properties (maxCtoPgm, maxBsPgm, minStPgm, masterBlockMode).
1055
+ """
1056
+ # Use the generalized downloader to program the flash
1057
+ self._generalized_downloader(
1058
+ address=address,
1059
+ data=data,
1060
+ maxCto=self.slaveProperties.pgmProcessor.maxCtoPgm,
1061
+ maxBs=self.slaveProperties.pgmProcessor.maxBsPgm,
1062
+ minSt=self.slaveProperties.pgmProcessor.minStPgm,
1063
+ master_block_mode=self.slaveProperties.pgmProcessor.masterBlockMode,
1064
+ dl_func=self.program,
1065
+ dl_next_func=self.programNext,
1066
+ callback=callback,
1067
+ )
1068
+
1069
+ def _generalized_downloader(
1070
+ self,
1071
+ address: int,
1072
+ address_ext: int,
1073
+ data: bytes,
1074
+ maxCto: int,
1075
+ maxBs: int,
1076
+ minSt: int,
1077
+ master_block_mode: bool,
1078
+ dl_func: Callable[[bytes, int, bool], Any],
1079
+ dl_next_func: Callable[[bytes, int, bool], Any],
1080
+ callback: Callable[[int], None] | None = None,
1081
+ ) -> None:
1082
+ """Generic implementation for downloading data to the slave.
1083
+
1084
+ This method is a generic implementation for downloading data to the slave.
1085
+ It handles the details of breaking up large transfers into smaller pieces,
1086
+ and supports both master block mode and normal mode.
1087
+
1088
+ Parameters
1089
+ ----------
1090
+ address : int
1091
+ The memory address to write to
1092
+ address_ext : int
1093
+ The address extension
1094
+ data : bytes
1095
+ The data bytes to write
1096
+ maxCto : int
1097
+ Maximum Command Transfer Object size
1098
+ maxBs : int
1099
+ Maximum Block Size
1100
+ minSt : int
1101
+ Minimum Separation Time in 100µs units
1102
+ master_block_mode : bool
1103
+ Whether to use master block mode
1104
+ dl_func : Callable[[bytes, int, bool], Any]
1105
+ Function to use for the first download packet
1106
+ dl_next_func : Callable[[bytes, int, bool], Any]
1107
+ Function to use for subsequent download packets
1108
+ callback : Callable[[int], None], optional
1109
+ A callback function that is called with the percentage of completion,
1110
+ by default None
1111
+ """
1112
+ # Set the Memory Transfer Address
1113
+ self.setMta(address, address_ext)
1114
+
1115
+ # Convert minSt from 100µs units to seconds
1116
+ minSt_seconds = minSt / 10000.0
1117
+
1118
+ # Create a partial function for block downloading
1119
+ block_downloader = functools.partial(
1120
+ self._block_downloader,
1121
+ dl_func=dl_func,
1122
+ dl_next_func=dl_next_func,
1123
+ minSt=minSt_seconds,
1124
+ )
1125
+
1126
+ # Calculate total length and maximum payload size
1127
+ total_length = len(data)
1128
+ if master_block_mode:
1129
+ max_payload = min(maxBs * (maxCto - 2), 255)
1130
+ else:
1131
+ max_payload = maxCto - 2
1132
+
1133
+ # Initialize offset
1134
+ offset = 0
1135
+
1136
+ # Handle master block mode
1137
+ if master_block_mode:
1138
+ self._download_master_block_mode(data, total_length, max_payload, offset, block_downloader, callback)
1139
+ # Handle normal mode
1140
+ else:
1141
+ self._download_normal_mode(data, total_length, max_payload, offset, dl_func, callback)
1142
+
1143
+ def _download_master_block_mode(
1144
+ self,
1145
+ data: bytes,
1146
+ total_length: int,
1147
+ max_payload: int,
1148
+ offset: int,
1149
+ block_downloader: Callable[[bytes], Any],
1150
+ callback: Callable[[int], None] | None = None,
1151
+ ) -> None:
1152
+ """Download data using master block mode.
1153
+
1154
+ Parameters
1155
+ ----------
1156
+ data : bytes
1157
+ The data bytes to write
1158
+ total_length : int
1159
+ The total length of the data
1160
+ max_payload : int
1161
+ Maximum payload size
1162
+ offset : int
1163
+ Starting offset in the data
1164
+ block_downloader : Callable[[bytes], Any]
1165
+ Function to use for downloading blocks
1166
+ callback : Callable[[int], None], optional
1167
+ A callback function that is called with the percentage of completion,
1168
+ by default None
1169
+ """
1170
+ remaining = total_length
1171
+ blocks = range(total_length // max_payload)
1172
+ percent_complete = 1
1173
+ remaining_block_size = total_length % max_payload
1174
+
1175
+ # Process full blocks
1176
+ for _ in blocks:
1177
+ block = data[offset : offset + max_payload]
1178
+ block_downloader(block)
1179
+ offset += max_payload
1180
+ remaining -= max_payload
1181
+
1182
+ # Call callback if provided
1183
+ if callback and remaining <= total_length - (total_length / 100) * percent_complete:
1184
+ callback(percent_complete)
1185
+ percent_complete += 1
1186
+
1187
+ # Process remaining partial block
1188
+ if remaining_block_size:
1189
+ block = data[offset : offset + remaining_block_size]
1190
+ block_downloader(block)
1191
+ if callback:
1192
+ callback(percent_complete)
1193
+
1194
+ def _download_normal_mode(
1195
+ self,
1196
+ data: bytes,
1197
+ total_length: int,
1198
+ max_payload: int,
1199
+ offset: int,
1200
+ dl_func: Callable[[bytes, int, bool], Any],
1201
+ callback: Callable[[int], None] | None = None,
1202
+ ) -> None:
1203
+ """Download data using normal mode.
1204
+
1205
+ Parameters
1206
+ ----------
1207
+ data : bytes
1208
+ The data bytes to write
1209
+ total_length : int
1210
+ The total length of the data
1211
+ max_payload : int
1212
+ Maximum payload size
1213
+ offset : int
1214
+ Starting offset in the data
1215
+ dl_func : Callable[[bytes, int, bool], Any]
1216
+ Function to use for downloading
1217
+ callback : Callable[[int], None], optional
1218
+ A callback function that is called with the percentage of completion,
1219
+ by default None
1220
+ """
1221
+ chunk_size = max_payload
1222
+ chunks = range(total_length // chunk_size)
1223
+ remaining = total_length % chunk_size
1224
+ percent_complete = 1
1225
+ callback_remaining = total_length
1226
+
1227
+ # Process full chunks
1228
+ for _ in chunks:
1229
+ block = data[offset : offset + max_payload]
1230
+ dl_func(block, max_payload, last=False)
1231
+ offset += max_payload
1232
+ callback_remaining -= chunk_size
1233
+
1234
+ # Call callback if provided
1235
+ if callback and callback_remaining <= total_length - (total_length / 100) * percent_complete:
1236
+ callback(percent_complete)
1237
+ percent_complete += 1
1238
+
1239
+ # Process remaining partial chunk
1240
+ if remaining:
1241
+ block = data[offset : offset + remaining]
1242
+ dl_func(block, remaining, last=True)
1243
+ if callback:
1244
+ callback(percent_complete)
1245
+
1246
+ def _block_downloader(
1247
+ self,
1248
+ data: bytes,
1249
+ dl_func: Callable[[bytes, int, bool], Any] | None = None,
1250
+ dl_next_func: Callable[[bytes, int, bool], Any] | None = None,
1251
+ minSt: float = 0.0,
1252
+ ) -> None:
1253
+ """Re-usable block downloader for transferring data in blocks.
1254
+
1255
+ This method breaks up a block of data into packets and sends them
1256
+ using the provided download functions. It handles the details of
1257
+ calculating packet sizes, setting the 'last' flag, and applying
1258
+ the minimum separation time between packets.
1259
+
1260
+ Parameters
1261
+ ----------
1262
+ data : bytes
1263
+ The data bytes to download
1264
+ dl_func : Callable[[bytes, int, bool], Any] | None, optional
1265
+ Function to use for the first download packet,
1266
+ usually :meth:`download` or :meth:`program`, by default None
1267
+ dl_next_func : Callable[[bytes, int, bool], Any] | None, optional
1268
+ Function to use for subsequent download packets,
1269
+ usually :meth:`downloadNext` or :meth:`programNext`, by default None
1270
+ minSt : float, optional
1271
+ Minimum separation time between frames in seconds, by default 0.0
1272
+ """
1273
+ # Calculate sizes and offsets
1274
+ length = len(data)
1275
+ max_packet_size = self.slaveProperties.maxCto - 2 # Command ID + Length
1276
+ packets = range(length // max_packet_size)
1277
+ offset = 0
1278
+ remaining = length % max_packet_size
1279
+ remaining_block_size = length
1280
+
1281
+ # Process full packets
1282
+ index = 0
1283
+ for index in packets:
1284
+ # Extract packet data
1285
+ packet_data = data[offset : offset + max_packet_size]
1286
+
1287
+ # Determine if this is the last packet
1288
+ last = (remaining_block_size - max_packet_size) == 0
1289
+
1290
+ # Send packet using appropriate function
1291
+ if index == 0:
1292
+ # First packet: use dl_func and transmit the complete length
1293
+ dl_func(packet_data, length, last)
1294
+ else:
1295
+ # Subsequent packets: use dl_next_func
1296
+ dl_next_func(packet_data, remaining_block_size, last)
1297
+
1298
+ # Update offsets and remaining size
1299
+ offset += max_packet_size
1300
+ remaining_block_size -= max_packet_size
1301
+
1302
+ # Apply minimum separation time
1303
+ delay(minSt)
1304
+
1305
+ # Process remaining partial packet
1306
+ if remaining:
1307
+ # Extract remaining data
1308
+ packet_data = data[offset : offset + remaining]
1309
+
1310
+ # Send packet using appropriate function
1311
+ if index == 0:
1312
+ # If there were no full packets, use dl_func
1313
+ # (length of data is smaller than maxCto - 2)
1314
+ dl_func(packet_data, remaining, last=True)
1315
+ else:
1316
+ # Otherwise use dl_next_func
1317
+ dl_next_func(packet_data, remaining, last=True)
1318
+
1319
+ # Apply minimum separation time
1320
+ delay(minSt)
1321
+
1322
+ @wrapped
1323
+ def download(self, data: bytes, block_mode_length: int | None = None, last: bool = False):
1324
+ """Transfer data from master to slave.
1325
+
1326
+ Parameters
1327
+ ----------
1328
+ data : bytes
1329
+ Data to send to slave.
1330
+ block_mode_length : int or None
1331
+ for block mode, the download request must contain the length of the whole block,
1332
+ not just the length in the current packet. The whole block length can be given here for block-mode
1333
+ transfers. For normal mode, the length indicates the actual packet's payload length.
1334
+
1335
+ Note
1336
+ ----
1337
+ Adress is set via :meth:`setMta`
1338
+ """
1339
+
1340
+ if block_mode_length is None or last:
1341
+ # standard mode
1342
+ length = len(data)
1343
+ response = self.transport.request(types.Command.DOWNLOAD, length, *data)
1344
+ return response
1345
+ else:
1346
+ # block mode
1347
+ if not isinstance(block_mode_length, int):
1348
+ raise TypeError("block_mode_length must be int!")
1349
+ self.transport.block_request(types.Command.DOWNLOAD, block_mode_length, *data)
1350
+ return None
1351
+
1352
+ @wrapped
1353
+ def downloadNext(self, data: bytes, remaining_block_length: int, last: bool = False):
1354
+ """Transfer data from master to slave (block mode).
1355
+
1356
+ Parameters
1357
+ ----------
1358
+ data : bytes
1359
+ remainingBlockLength : int
1360
+ This parameter has to be given the remaining length in the block
1361
+ last : bool
1362
+ The block mode implementation shall indicate the last packet in the block with this parameter, because
1363
+ the slave device will send the response after this.
1364
+ """
1365
+
1366
+ if last:
1367
+ # last DOWNLOAD_NEXT packet in a block: the slave device has to send the response after this.
1368
+ response = self.transport.request(types.Command.DOWNLOAD_NEXT, remaining_block_length, *data)
1369
+ return response
1370
+ else:
1371
+ # the slave device won't respond to consecutive DOWNLOAD_NEXT packets in block mode,
1372
+ # so we must not wait for any response
1373
+ self.transport.block_request(types.Command.DOWNLOAD_NEXT, remaining_block_length, *data)
1374
+ return None
1375
+
1376
+ @wrapped
1377
+ def downloadMax(self, data: bytes):
1378
+ """Transfer data from master to slave (fixed size).
1379
+
1380
+ Parameters
1381
+ ----------
1382
+ data : bytes
1383
+ """
1384
+ return self.transport.request(types.Command.DOWNLOAD_MAX, *data)
1385
+
1386
+ @wrapped
1387
+ def shortDownload(self, address: int, address_ext: int, data: bytes):
1388
+ length = len(data)
1389
+ addr = self.DWORD_pack(address)
1390
+ return self.transport.request(types.Command.SHORT_DOWNLOAD, length, 0, address_ext, *addr, *data)
1391
+
1392
+ @wrapped
1393
+ def modifyBits(self, shift_value: int, and_mask: int, xor_mask: int):
1394
+ # A = ( (A) & ((~((dword)(((word)~MA)<<S))) )^((dword)(MX<<S)) )
1395
+ am = self.WORD_pack(and_mask)
1396
+ xm = self.WORD_pack(xor_mask)
1397
+ return self.transport.request(types.Command.MODIFY_BITS, shift_value, *am, *xm)
1398
+
1399
+ # Page Switching Commands (PAG)
1400
+ @wrapped
1401
+ def setCalPage(self, mode: int, logical_data_segment: int, logical_data_page: int):
1402
+ """Set calibration page.
1403
+
1404
+ Parameters
1405
+ ----------
1406
+ mode : int (bitfield)
1407
+ - 0x01 - The given page will be used by the slave device application.
1408
+ - 0x02 - The slave device XCP driver will access the given page.
1409
+ - 0x80 - The logical segment number is ignored. The command applies to all segments
1410
+ logicalDataSegment : int
1411
+ logicalDataPage : int
1412
+ """
1413
+ return self.transport.request(types.Command.SET_CAL_PAGE, mode, logical_data_segment, logical_data_page)
1414
+
1415
+ @wrapped
1416
+ def getCalPage(self, mode: int, logical_data_segment: int):
1417
+ """Get calibration page
1418
+
1419
+ Parameters
1420
+ ----------
1421
+ mode : int
1422
+ logicalDataSegment : int
1423
+ """
1424
+ response = self.transport.request(types.Command.GET_CAL_PAGE, mode, logical_data_segment)
1425
+ return response[2]
1426
+
1427
+ @wrapped
1428
+ def getPagProcessorInfo(self):
1429
+ """Get general information on PAG processor.
1430
+
1431
+ Returns
1432
+ -------
1433
+ `pydbc.types.GetPagProcessorInfoResponse`
1434
+ """
1435
+ response = self.transport.request(types.Command.GET_PAG_PROCESSOR_INFO)
1436
+ return types.GetPagProcessorInfoResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1437
+
1438
+ @wrapped
1439
+ def getSegmentInfo(self, mode: int, segment_number: int, segment_info: int, mapping_index: int):
1440
+ """Get specific information for a segment.
1441
+
1442
+ Parameters
1443
+ ----------
1444
+ mode : int
1445
+ - 0 = get basic address info for this segment
1446
+ - 1 = get standard info for this segment
1447
+ - 2 = get address mapping info for this segment
1448
+
1449
+ segmentNumber : int
1450
+ segmentInfo : int
1451
+ Mode 0:
1452
+ - 0 = address
1453
+ - 1 = length
1454
+
1455
+ Mode 1:
1456
+ - don't care
1457
+
1458
+ Mode 2:
1459
+ - 0 = source address
1460
+ - 1 = destination address
1461
+ - 2 = length address
1462
+
1463
+ mappingIndex : int
1464
+ - Mode 0: don't care
1465
+ - Mode 1: don't care
1466
+ - Mode 2: identifier for address mapping range that mapping_info belongs to.
1467
+
1468
+ """
1469
+ response = self.transport.request(
1470
+ types.Command.GET_SEGMENT_INFO,
1471
+ mode,
1472
+ segment_number,
1473
+ segment_info,
1474
+ mapping_index,
1475
+ )
1476
+ if mode == 0:
1477
+ return types.GetSegmentInfoMode0Response.parse(response, byteOrder=self.slaveProperties.byteOrder)
1478
+ elif mode == 1:
1479
+ return types.GetSegmentInfoMode1Response.parse(response, byteOrder=self.slaveProperties.byteOrder)
1480
+ elif mode == 2:
1481
+ return types.GetSegmentInfoMode2Response.parse(response, byteOrder=self.slaveProperties.byteOrder)
1482
+
1483
+ @wrapped
1484
+ def getPageInfo(self, segment_number: int, page_number: int):
1485
+ """Get specific information for a page.
1486
+
1487
+ Parameters
1488
+ ----------
1489
+ segmentNumber : int
1490
+ pageNumber : int
1491
+ """
1492
+ response = self.transport.request(types.Command.GET_PAGE_INFO, 0, segment_number, page_number)
1493
+ return (
1494
+ types.PageProperties.parse(bytes([response[0]]), byteOrder=self.slaveProperties.byteOrder),
1495
+ response[1],
1496
+ )
1497
+
1498
+ @wrapped
1499
+ def setSegmentMode(self, mode: int, segment_number: int):
1500
+ """Set mode for a segment.
1501
+
1502
+ Parameters
1503
+ ----------
1504
+ mode : int (bitfield)
1505
+ 1 = enable FREEZE Mode
1506
+ segmentNumber : int
1507
+ """
1508
+ return self.transport.request(types.Command.SET_SEGMENT_MODE, mode, segment_number)
1509
+
1510
+ @wrapped
1511
+ def getSegmentMode(self, segment_number: int):
1512
+ """Get mode for a segment.
1513
+
1514
+ Parameters
1515
+ ----------
1516
+ segmentNumber : int
1517
+ """
1518
+ response = self.transport.request(types.Command.GET_SEGMENT_MODE, 0, segment_number)
1519
+ if response:
1520
+ return response[1]
1521
+
1522
+ @wrapped
1523
+ def copyCalPage(self, src_segment: int, src_page: int, dst_segment: int, dst_page: int):
1524
+ """Copy page.
1525
+
1526
+ Parameters
1527
+ ----------
1528
+ srcSegment : int
1529
+ srcPage : int
1530
+ dstSegment : int
1531
+ dstPage : int
1532
+ """
1533
+ return self.transport.request(types.Command.COPY_CAL_PAGE, src_segment, src_page, dst_segment, dst_page)
1534
+
1535
+ # DAQ
1536
+
1537
+ @wrapped
1538
+ def setDaqPtr(self, daq_list_number: int, odt_number: int, odt_entry_number: int):
1539
+ self.currentDaqPtr = types.DaqPtr(daq_list_number, odt_number, odt_entry_number) # Needed for errorhandling.
1540
+ daq_list = self.WORD_pack(daq_list_number)
1541
+ response = self.transport.request(types.Command.SET_DAQ_PTR, 0, *daq_list, odt_number, odt_entry_number)
1542
+ self.stim.setDaqPtr(daq_list_number, odt_number, odt_entry_number)
1543
+ return response
1544
+
1545
+ @wrapped
1546
+ def clearDaqList(self, daq_list_number: int):
1547
+ """Clear DAQ list configuration.
1548
+
1549
+ Parameters
1550
+ ----------
1551
+ daqListNumber : int
1552
+ """
1553
+ daq_list = self.WORD_pack(daq_list_number)
1554
+ result = self.transport.request(types.Command.CLEAR_DAQ_LIST, 0, *daq_list)
1555
+ self.stim.clearDaqList(daq_list_number)
1556
+ return result
1557
+
1558
+ @wrapped
1559
+ def writeDaq(self, bit_offset: int, entry_size: int, address_ext: int, address: int):
1560
+ """Write element in ODT entry.
1561
+
1562
+ Parameters
1563
+ ----------
1564
+ bitOffset : int
1565
+ Position of bit in 32-bit variable referenced by the address and
1566
+ extension below
1567
+ entrySize : int
1568
+ addressExt : int
1569
+ address : int
1570
+ """
1571
+ addr = self.DWORD_pack(address)
1572
+ result = self.transport.request(types.Command.WRITE_DAQ, bit_offset, entry_size, address_ext, *addr)
1573
+ self.stim.writeDaq(bit_offset, entry_size, address_ext, address)
1574
+ return result
1575
+
1576
+ @wrapped
1577
+ def setDaqListMode(self, mode: int, daq_list_number: int, event_channel_number: int, prescaler: int, priority: int):
1578
+ dln = self.WORD_pack(daq_list_number)
1579
+ ecn = self.WORD_pack(event_channel_number)
1580
+ self.stim.setDaqListMode(mode, daq_list_number, event_channel_number, prescaler, priority)
1581
+ return self.transport.request(types.Command.SET_DAQ_LIST_MODE, mode, *dln, *ecn, prescaler, priority)
1582
+
1583
+ @wrapped
1584
+ def getDaqListMode(self, daq_list_number: int):
1585
+ """Get mode from DAQ list.
1586
+
1587
+ Parameters
1588
+ ----------
1589
+ daqListNumber : int
1590
+
1591
+ Returns
1592
+ -------
1593
+ `pyxcp.types.GetDaqListModeResponse`
1594
+ """
1595
+ dln = self.WORD_pack(daq_list_number)
1596
+ response = self.transport.request(types.Command.GET_DAQ_LIST_MODE, 0, *dln)
1597
+ return types.GetDaqListModeResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1598
+
1599
+ @wrapped
1600
+ def startStopDaqList(self, mode: int, daq_list_number: int):
1601
+ """Start /stop/select DAQ list.
1602
+
1603
+ Parameters
1604
+ ----------
1605
+ mode : int
1606
+ 0 = stop
1607
+ 1 = start
1608
+ 2 = select
1609
+ daqListNumber : int
1610
+ """
1611
+ dln = self.WORD_pack(daq_list_number)
1612
+ response = self.transport.request(types.Command.START_STOP_DAQ_LIST, mode, *dln)
1613
+ self.stim.startStopDaqList(mode, daq_list_number)
1614
+ first_pid = types.StartStopDaqListResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1615
+ self.stim.set_first_pid(daq_list_number, first_pid.firstPid)
1616
+ return first_pid
1617
+
1618
+ @wrapped
1619
+ def startStopSynch(self, mode: int):
1620
+ """Start/stop DAQ lists (synchronously).
1621
+
1622
+ Parameters
1623
+ ----------
1624
+ mode : int
1625
+ 0 = stop all
1626
+ 1 = start selected
1627
+ 2 = stop selected
1628
+ """
1629
+ res = self.transport.request(types.Command.START_STOP_SYNCH, mode)
1630
+ self.stim.startStopSynch(mode)
1631
+ return res
1632
+
1633
+ @wrapped
1634
+ def writeDaqMultiple(self, daq_elements: dict):
1635
+ """Write multiple elements in ODT.
1636
+
1637
+ Parameters
1638
+ ----------
1639
+ daqElements : list of `dict` containing the following keys: *bitOffset*, *size*, *address*, *addressExt*.
1640
+ """
1641
+ if len(daq_elements) > self.slaveProperties.maxWriteDaqMultipleElements:
1642
+ raise ValueError(f"At most {self.slaveProperties.maxWriteDaqMultipleElements} daqElements are permitted.")
1643
+ data = bytearray()
1644
+ data.append(len(daq_elements))
1645
+
1646
+ for daq_element in daq_elements:
1647
+ data.extend(types.DaqElement.build(daq_element, byteOrder=self.slaveProperties.byteOrder))
1648
+
1649
+ return self.transport.request(types.Command.WRITE_DAQ_MULTIPLE, *data)
1650
+
1651
+ # optional
1652
+ @wrapped
1653
+ def getDaqClock(self):
1654
+ """Get DAQ clock from slave.
1655
+
1656
+ Returns
1657
+ -------
1658
+ int
1659
+ Current timestamp, format specified by `getDaqResolutionInfo`
1660
+ """
1661
+ response = self.transport.request(types.Command.GET_DAQ_CLOCK)
1662
+ result = types.GetDaqClockResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1663
+ return result.timestamp
1664
+
1665
+ @wrapped
1666
+ def readDaq(self):
1667
+ """Read element from ODT entry.
1668
+
1669
+ Returns
1670
+ -------
1671
+ `pyxcp.types.ReadDaqResponse`
1672
+ """
1673
+ response = self.transport.request(types.Command.READ_DAQ)
1674
+ return types.ReadDaqResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1675
+
1676
+ @wrapped
1677
+ def getDaqProcessorInfo(self):
1678
+ """Get general information on DAQ processor.
1679
+
1680
+ Returns
1681
+ -------
1682
+ `pyxcp.types.GetDaqProcessorInfoResponse`
1683
+ """
1684
+ response = self.transport.request(types.Command.GET_DAQ_PROCESSOR_INFO)
1685
+ return types.GetDaqProcessorInfoResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1686
+
1687
+ @wrapped
1688
+ def getDaqResolutionInfo(self):
1689
+ """Get general information on DAQ processing resolution.
1690
+
1691
+ Returns
1692
+ -------
1693
+ `pyxcp.types.GetDaqResolutionInfoResponse`
1694
+ """
1695
+ response = self.transport.request(types.Command.GET_DAQ_RESOLUTION_INFO)
1696
+ return types.GetDaqResolutionInfoResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1697
+
1698
+ @wrapped
1699
+ def getDaqListInfo(self, daq_list_number: int):
1700
+ """Get specific information for a DAQ list.
1701
+
1702
+ Parameters
1703
+ ----------
1704
+ daqListNumber : int
1705
+ """
1706
+ dln = self.WORD_pack(daq_list_number)
1707
+ response = self.transport.request(types.Command.GET_DAQ_LIST_INFO, 0, *dln)
1708
+ return types.GetDaqListInfoResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1709
+
1710
+ @wrapped
1711
+ def getDaqEventInfo(self, event_channel_number: int):
1712
+ """Get specific information for an event channel.
1713
+
1714
+ Parameters
1715
+ ----------
1716
+ eventChannelNumber : int
1717
+
1718
+ Returns
1719
+ -------
1720
+ `pyxcp.types.GetEventChannelInfoResponse`
1721
+ """
1722
+ ecn = self.WORD_pack(event_channel_number)
1723
+ response = self.transport.request(types.Command.GET_DAQ_EVENT_INFO, 0, *ecn)
1724
+ return types.GetEventChannelInfoResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1725
+
1726
+ @wrapped
1727
+ def dtoCtrProperties(self, modifier: int, event_channel: int, related_event_channel: int, mode: int):
1728
+ """DTO CTR properties
1729
+
1730
+ Parameters
1731
+ ----------
1732
+ modifier :
1733
+ eventChannel : int
1734
+ relatedEventChannel : int
1735
+ mode :
1736
+
1737
+ Returns
1738
+ -------
1739
+ `pyxcp.types.DtoCtrPropertiesResponse`
1740
+ """
1741
+ data = bytearray()
1742
+ data.append(modifier)
1743
+ data.extend(self.WORD_pack(event_channel))
1744
+ data.extend(self.WORD_pack(related_event_channel))
1745
+ data.append(mode)
1746
+ response = self.transport.request(types.Command.DTO_CTR_PROPERTIES, *data)
1747
+ return types.DtoCtrPropertiesResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1748
+
1749
+ @wrapped
1750
+ def setDaqPackedMode(
1751
+ self, daq_list_number: int, daq_packed_mode: int, dpm_timestamp_mode: int = None, dpm_sample_count: int = None
1752
+ ):
1753
+ """Set DAQ List Packed Mode.
1754
+
1755
+ Parameters
1756
+ ----------
1757
+ daqListNumber : int
1758
+ daqPackedMode : int
1759
+ """
1760
+ params = []
1761
+ dln = self.WORD_pack(daq_list_number)
1762
+ params.extend(dln)
1763
+ params.append(daq_packed_mode)
1764
+
1765
+ if daq_packed_mode == 1 or daq_packed_mode == 2:
1766
+ params.append(dpm_timestamp_mode)
1767
+ dsc = self.WORD_pack(dpm_sample_count)
1768
+ params.extend(dsc)
1769
+
1770
+ return self.transport.request(types.Command.SET_DAQ_PACKED_MODE, *params)
1771
+
1772
+ @wrapped
1773
+ def getDaqPackedMode(self, daq_list_number: int):
1774
+ """Get DAQ List Packed Mode.
1775
+
1776
+ This command returns information of the currently active packed mode of
1777
+ the addressed DAQ list.
1778
+
1779
+ Parameters
1780
+ ----------
1781
+ daqListNumber : int
1782
+ """
1783
+ dln = self.WORD_pack(daq_list_number)
1784
+ response = self.transport.request(types.Command.GET_DAQ_PACKED_MODE, *dln)
1785
+ return types.GetDaqPackedModeResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1786
+
1787
+ # dynamic
1788
+ @wrapped
1789
+ def freeDaq(self):
1790
+ """Clear dynamic DAQ configuration."""
1791
+ result = self.transport.request(types.Command.FREE_DAQ)
1792
+ self.stim.freeDaq()
1793
+ return result
1794
+
1795
+ @wrapped
1796
+ def allocDaq(self, daq_count: int):
1797
+ """Allocate DAQ lists.
1798
+
1799
+ Parameters
1800
+ ----------
1801
+ daqCount : int
1802
+ number of DAQ lists to be allocated
1803
+ """
1804
+ dq = self.WORD_pack(daq_count)
1805
+ result = self.transport.request(types.Command.ALLOC_DAQ, 0, *dq)
1806
+ self.stim.allocDaq(daq_count)
1807
+ return result
1808
+
1809
+ @wrapped
1810
+ def allocOdt(self, daq_list_number: int, odt_count: int):
1811
+ dln = self.WORD_pack(daq_list_number)
1812
+ result = self.transport.request(types.Command.ALLOC_ODT, 0, *dln, odt_count)
1813
+ self.stim.allocOdt(daq_list_number, odt_count)
1814
+ return result
1815
+
1816
+ @wrapped
1817
+ def allocOdtEntry(self, daq_list_number: int, odt_number: int, odt_entries_count: int):
1818
+ dln = self.WORD_pack(daq_list_number)
1819
+ result = self.transport.request(types.Command.ALLOC_ODT_ENTRY, 0, *dln, odt_number, odt_entries_count)
1820
+ self.stim.allocOdtEntry(daq_list_number, odt_number, odt_entries_count)
1821
+ return result
1822
+
1823
+ # PGM
1824
+ @wrapped
1825
+ def programStart(self):
1826
+ """Indicate the beginning of a programming sequence.
1827
+
1828
+ Returns
1829
+ -------
1830
+ `pyxcp.types.ProgramStartResponse`
1831
+ """
1832
+ response = self.transport.request(types.Command.PROGRAM_START)
1833
+ result = types.ProgramStartResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1834
+ self.slaveProperties.pgmProcessor.commModePgm = result.commModePgm
1835
+ self.slaveProperties.pgmProcessor.maxCtoPgm = result.maxCtoPgm
1836
+ self.slaveProperties.pgmProcessor.maxBsPgm = result.maxBsPgm
1837
+ self.slaveProperties.pgmProcessor.minStPgm = result.minStPgm
1838
+ self.slaveProperties.pgmProcessor.queueSizePgm = result.queueSizePgm
1839
+ self.slaveProperties.pgmProcessor.slaveBlockMode = result.commModePgm.slaveBlockMode
1840
+ self.slaveProperties.pgmProcessor.interleavedMode = result.commModePgm.interleavedMode
1841
+ self.slaveProperties.pgmProcessor.masterBlockMode = result.commModePgm.masterBlockMode
1842
+ return result
1843
+
1844
+ @wrapped
1845
+ def programClear(self, mode: int, clear_range: int):
1846
+ """Clear a part of non-volatile memory.
1847
+
1848
+ Parameters
1849
+ ----------
1850
+ mode : int
1851
+ 0x00 = the absolute access mode is active (default)
1852
+ 0x01 = the functional access mode is active
1853
+ clearRange : int
1854
+ """
1855
+ cr = self.DWORD_pack(clear_range)
1856
+ response = self.transport.request(types.Command.PROGRAM_CLEAR, mode, 0, 0, *cr)
1857
+ # ERR_ACCESS_LOCKED
1858
+ return response
1859
+
1860
+ @wrapped
1861
+ def program(self, data: bytes, block_length: int, last: bool = False):
1862
+ """Parameters
1863
+ ----------
1864
+ data : bytes
1865
+ Data to send to slave.
1866
+ block_mode_length : int
1867
+ the program request must contain the length of the whole block, not just the length
1868
+ in the current packet.
1869
+ last : bool
1870
+ Indicates that this is the only packet in the block, because
1871
+ the slave device will send the response after this.
1872
+
1873
+ Note
1874
+ ----
1875
+ Adress is set via :meth:`setMta`
1876
+ """
1877
+ # d = bytearray()
1878
+ # d.append(len(data))
1879
+ # if self.slaveProperties.addressGranularity == types.AddressGranularity.DWORD:
1880
+ # d.extend(b"\x00\x00") # alignment bytes
1881
+ # for e in data:
1882
+ # d.extend(self.AG_pack(e))
1883
+ if last:
1884
+ # last PROGRAM_NEXT packet in a block: the slave device has to send the response after this.
1885
+ response = self.transport.request(types.Command.PROGRAM, block_length, *data)
1886
+ return response
1887
+ else:
1888
+ # the slave device won't respond to consecutive PROGRAM_NEXT packets in block mode,
1889
+ # so we must not wait for any response
1890
+ self.transport.block_request(types.Command.PROGRAM, block_length, *data)
1891
+ return None
1892
+
1893
+ @wrapped
1894
+ def programReset(self, wait_for_optional_response=True):
1895
+ """Indicate the end of a programming sequence."""
1896
+ if wait_for_optional_response:
1897
+ return self.transport.request_optional_response(types.Command.PROGRAM_RESET)
1898
+ else:
1899
+ return self.transport.request(types.Command.PROGRAM_RESET)
1900
+
1901
+ @wrapped
1902
+ def getPgmProcessorInfo(self):
1903
+ """Get general information on PGM processor."""
1904
+ response = self.transport.request(types.Command.GET_PGM_PROCESSOR_INFO)
1905
+ result = types.GetPgmProcessorInfoResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1906
+ self.slaveProperties.pgmProcessor.pgmProperties = result.pgmProperties
1907
+ self.slaveProperties.pgmProcessor.maxSector = result.maxSector
1908
+ return result
1909
+
1910
+ @wrapped
1911
+ def getSectorInfo(self, mode: int, sector_number: int):
1912
+ """Get specific information for a sector."""
1913
+ response = self.transport.request(types.Command.GET_SECTOR_INFO, mode, sector_number)
1914
+ if mode == 0 or mode == 1:
1915
+ return types.GetSectorInfoResponseMode01.parse(response, byteOrder=self.slaveProperties.byteOrder)
1916
+ elif mode == 2:
1917
+ return types.GetSectorInfoResponseMode2.parse(response, byteOrder=self.slaveProperties.byteOrder)
1918
+
1919
+ @wrapped
1920
+ def programPrepare(self, codesize):
1921
+ """Prepare non-volatile memory programming."""
1922
+ cs = self.WORD_pack(codesize)
1923
+ return self.transport.request(types.Command.PROGRAM_PREPARE, 0x00, *cs)
1924
+
1925
+ @wrapped
1926
+ def programFormat(self, compression_method: int, encryption_method: int, programming_method: int, access_method: int):
1927
+ return self.transport.request(
1928
+ types.Command.PROGRAM_FORMAT,
1929
+ compression_method,
1930
+ encryption_method,
1931
+ programming_method,
1932
+ access_method,
1933
+ )
1934
+
1935
+ @wrapped
1936
+ def programNext(self, data: bytes, remaining_block_length: int, last: bool = False):
1937
+ # d = bytearray()
1938
+ # d.append(len(data))
1939
+ # if self.slaveProperties.addressGranularity == types.AddressGranularity.DWORD:
1940
+ # d.extend(b"\x00\x00") # alignment bytes
1941
+ # for e in data:
1942
+ # d.extend(self.AG_pack(e))
1943
+ if last:
1944
+ # last PROGRAM_NEXT packet in a block: the slave device has to send the response after this.
1945
+ response = self.transport.request(types.Command.PROGRAM_NEXT, remaining_block_length, *data)
1946
+ return response
1947
+ else:
1948
+ # the slave device won't respond to consecutive PROGRAM_NEXT packets in block mode,
1949
+ # so we must not wait for any response
1950
+ self.transport.block_request(types.Command.PROGRAM_NEXT, remaining_block_length, *data)
1951
+ return None
1952
+
1953
+ @wrapped
1954
+ def programMax(self, data: bytes):
1955
+ d = bytearray()
1956
+ if self.slaveProperties.addressGranularity == types.AddressGranularity.WORD:
1957
+ d.extend(b"\x00") # alignment bytes
1958
+ elif self.slaveProperties.addressGranularity == types.AddressGranularity.DWORD:
1959
+ d.extend(b"\x00\x00\x00") # alignment bytes
1960
+ for e in data:
1961
+ d.extend(self.AG_pack(e))
1962
+ return self.transport.request(types.Command.PROGRAM_MAX, *d)
1963
+
1964
+ @wrapped
1965
+ def programVerify(self, ver_mode: int, ver_type: int, ver_value: int):
1966
+ data = bytearray()
1967
+ data.extend(self.WORD_pack(ver_type))
1968
+ data.extend(self.DWORD_pack(ver_value))
1969
+ return self.transport.request(types.Command.PROGRAM_VERIFY, ver_mode, *data)
1970
+
1971
+ # DBG
1972
+
1973
+ @wrapped
1974
+ def dbgAttach(self):
1975
+ """Returns detailed information about the implemented version of the SW-DBG feature of the XCP slave
1976
+
1977
+ Returns
1978
+ -------
1979
+ `pyxcp.types.DbgAttachResponse`
1980
+ """
1981
+ response = self.transport.request(types.Command.DBG_ATTACH)
1982
+ return types.DbgAttachResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1983
+
1984
+ @wrapped
1985
+ def dbgGetVendorInfo(self):
1986
+ """"""
1987
+ response = self.transport.request(types.Command.DBG_GET_VENDOR_INFO)
1988
+ return types.DbgGetVendorInfoResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1989
+
1990
+ @wrapped
1991
+ def dbgGetModeInfo(self):
1992
+ """"""
1993
+ response = self.transport.request(types.Command.DBG_GET_MODE_INFO)
1994
+ return types.DbgGetModeInfoResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
1995
+
1996
+ @wrapped
1997
+ def dbgGetJtagId(self):
1998
+ """"""
1999
+ response = self.transport.request(types.Command.DBG_GET_JTAG_ID)
2000
+ return types.DbgGetJtagIdResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
2001
+
2002
+ @wrapped
2003
+ def dbgHaltAfterReset(self):
2004
+ """"""
2005
+ return self.transport.request(types.Command.DBG_HALT_AFTER_RESET)
2006
+
2007
+ @wrapped
2008
+ def dbgGetHwioInfo(self, index: int):
2009
+ """"""
2010
+ response = self.transport.request(types.Command.DBG_GET_HWIO_INFO, index)
2011
+ return types.DbgGetHwioInfoResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
2012
+
2013
+ @wrapped
2014
+ def dbgSetHwioEvent(self, index: int, trigger: int):
2015
+ """"""
2016
+ return self.transport.request(types.Command.DBG_SET_HWIO_EVENT, index, trigger)
2017
+
2018
+ @wrapped
2019
+ def dbgHwioControl(self, pins):
2020
+ """"""
2021
+ d = bytearray()
2022
+ d.extend(self.BYTE_pack(len(pins)))
2023
+ for p in pins:
2024
+ d.extend(self.BYTE_pack(p[0])) # index
2025
+ d.extend(self.BYTE_pack(p[1])) # state
2026
+ d.extend(self.WORD_pack(p[2])) # frequency
2027
+
2028
+ response = self.transport.request(types.Command.DBG_HWIO_CONTROL, *d)
2029
+ return types.DbgHwioControlResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
2030
+
2031
+ @wrapped
2032
+ def dbgExclusiveTargetAccess(self, mode: int, context: int):
2033
+ """"""
2034
+ return self.transport.request(types.Command.DBG_EXCLUSIVE_TARGET_ACCESS, mode, context)
2035
+
2036
+ @wrapped
2037
+ def dbgSequenceMultiple(self, mode: int, num: int, *seq):
2038
+ """"""
2039
+ response = self.transport.request(types.Command.DBG_SEQUENCE_MULTIPLE, mode, self.WORD_pack(num), *seq)
2040
+ return types.DbgSequenceMultipleResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
2041
+
2042
+ @wrapped
2043
+ def dbgLlt(self, num: int, mode: int, *llts):
2044
+ """"""
2045
+ response = self.transport.request(types.Command.DBG_LLT, num, mode, *llts)
2046
+ return types.DbgLltResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
2047
+
2048
+ @wrapped
2049
+ def dbgReadModifyWrite(self, tri: int, width: int, address: int, mask: int, data: int):
2050
+ """"""
2051
+ d = bytearray()
2052
+ d.extend(b"\x00")
2053
+ d.append(tri)
2054
+ d.append(width)
2055
+ d.extend(b"\x00\x00")
2056
+ d.extend(self.DLONG_pack(address))
2057
+ if width == 0x01:
2058
+ d.extend(self.BYTE_pack(mask))
2059
+ d.extend(self.BYTE_pack(data))
2060
+ elif width == 0x02:
2061
+ d.extend(self.WORD_pack(mask))
2062
+ d.extend(self.WORD_pack(data))
2063
+ elif width == 0x04:
2064
+ d.extend(self.DWORD_pack(mask))
2065
+ d.extend(self.DWORD_pack(data))
2066
+ elif width == 0x08:
2067
+ d.extend(self.DLONG_pack(mask))
2068
+ d.extend(self.DLONG_pack(data))
2069
+ response = self.transport.request(types.Command.DBG_READ_MODIFY_WRITE, *d)
2070
+ return types.DbgReadModifyWriteResponse.parse(response, byteOrder=self.slaveProperties.byteOrder, width=width)
2071
+
2072
+ @wrapped
2073
+ def dbgWrite(self, tri: int, width: int, address: int, data):
2074
+ """"""
2075
+ d = bytearray()
2076
+ d.extend(b"\x00")
2077
+ d.append(tri)
2078
+ self._dbg_width = width
2079
+ d.append(width)
2080
+ d.extend(self.WORD_pack(len(data)))
2081
+ d.extend(self.DLONG_pack(address))
2082
+ for da in data:
2083
+ if width == 0x01:
2084
+ d.extend(self.BYTE_pack(da))
2085
+ elif width == 0x02:
2086
+ d.extend(self.WORD_pack(da))
2087
+ elif width == 0x04:
2088
+ d.extend(self.DWORD_pack(da))
2089
+ elif width == 0x08:
2090
+ d.extend(self.DLONG_pack(da))
2091
+ return self.transport.request(types.Command.DBG_WRITE, *d)
2092
+
2093
+ @wrapped
2094
+ def dbgWriteNext(self, num: int, data: int):
2095
+ """"""
2096
+ d = bytearray()
2097
+ d.extend(b"\x00")
2098
+ d.extend(self.WORD_pack(num))
2099
+ d.extend(b"\x00\x00")
2100
+ for i in range(num):
2101
+ if self._dbg_width == 0x01:
2102
+ d.extend(self.BYTE_pack(data[i]))
2103
+ elif self._dbg_width == 0x02:
2104
+ d.extend(self.WORD_pack(data[i]))
2105
+ elif self._dbg_width == 0x04:
2106
+ d.extend(self.DWORD_pack(data[i]))
2107
+ elif self._dbg_width == 0x08:
2108
+ d.extend(self.DLONG_pack(data[i]))
2109
+ return self.transport.request(types.Command.DBG_WRITE_NEXT, *d)
2110
+
2111
+ @wrapped
2112
+ def dbgWriteCan1(self, tri: int, address: int):
2113
+ """"""
2114
+ d = bytearray()
2115
+ d.extend(self.BYTE_pack(tri))
2116
+ d.extend(self.DWORD_pack(address))
2117
+ return self.transport.request(types.Command.DBG_WRITE_CAN1, *d)
2118
+
2119
+ @wrapped
2120
+ def dbgWriteCan2(self, width: int, num: int):
2121
+ """"""
2122
+ d = bytearray()
2123
+ self._dbg_width = width
2124
+ d.append(width)
2125
+ d.extend(self.BYTE_pack(num))
2126
+ return self.transport.request(types.Command.DBG_WRITE_CAN2, *d)
2127
+
2128
+ @wrapped
2129
+ def dbgWriteCanNext(self, num: int, data: int):
2130
+ """"""
2131
+ d = bytearray()
2132
+ d.extend(self.BYTE_pack(num))
2133
+ for i in range(num):
2134
+ if self._dbg_width == 0x01:
2135
+ d.extend(self.BYTE_pack(data[i]))
2136
+ elif self._dbg_width == 0x02:
2137
+ d.extend(self.WORD_pack(data[i]))
2138
+ elif self._dbg_width == 0x04:
2139
+ d.extend(self.DWORD_pack(data[i]))
2140
+ elif self._dbg_width == 0x08:
2141
+ d.extend(self.DLONG_pack(data[i]))
2142
+ return self.transport.request(types.Command.DBG_WRITE_CAN_NEXT, *d)
2143
+
2144
+ @wrapped
2145
+ def dbgRead(self, tri: int, width: int, num: int, address: int):
2146
+ """"""
2147
+ d = bytearray()
2148
+ d.extend(b"\x00")
2149
+ d.extend(self.BYTE_pack(tri))
2150
+ self._dbg_width = width
2151
+ d.extend(self.BYTE_pack(width))
2152
+ d.extend(self.WORD_pack(num))
2153
+ d.extend(self.DLONG_pack(address))
2154
+ response = self.transport.request(types.Command.DBG_READ, *d)
2155
+ return types.DbgReadResponse.parse(response, byteOrder=self.slaveProperties.byteOrder, width=width)
2156
+
2157
+ @wrapped
2158
+ def dbgReadCan1(self, tri: int, address: int):
2159
+ """"""
2160
+ d = bytearray()
2161
+ d.extend(self.BYTE_pack(tri))
2162
+ d.extend(self.DWORD_pack(address))
2163
+ return self.transport.request(types.Command.DBG_READ_CAN1, *d)
2164
+
2165
+ @wrapped
2166
+ def dbgReadCan2(self, width: int, num: int):
2167
+ """"""
2168
+ d = bytearray()
2169
+ self._dbg_width = width
2170
+ d.extend(self.BYTE_pack(width))
2171
+ d.extend(self.BYTE_pack(num))
2172
+ return self.transport.request(types.Command.DBG_READ_CAN2, *d)
2173
+
2174
+ @wrapped
2175
+ def dbgGetTriDescTbl(self):
2176
+ """"""
2177
+ response = self.transport.request(types.Command.DBG_GET_TRI_DESC_TBL, b"\x00\x00\x00\x00\x00")
2178
+ return types.DbgGetTriDescTblResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
2179
+
2180
+ @wrapped
2181
+ def dbgLlbt(self, data):
2182
+ """"""
2183
+ d = bytearray()
2184
+ d.extend(b"\x00")
2185
+ d.extend(self.WORD_pack(len(data)))
2186
+ for b in data:
2187
+ d.extend(self.BYTE_pack(b))
2188
+ response = self.transport.request(types.Command.DBG_LLBT, d)
2189
+ return types.DbgLlbtResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
2190
+
2191
+ @wrapped
2192
+ def timeCorrelationProperties(self, set_properties: int, get_properties_request: int, cluster_id: int):
2193
+ response = self.transport.request(
2194
+ types.Command.TIME_CORRELATION_PROPERTIES, set_properties, get_properties_request, 0, *self.WORD_pack(cluster_id)
2195
+ )
2196
+ return types.TimeCorrelationPropertiesResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
2197
+
2198
+ # Transport layer commands / CAN.
2199
+
2200
+ @broadcasted
2201
+ @wrapped
2202
+ def getSlaveID(self, mode: int):
2203
+ response = self.transportLayerCmd(types.TransportLayerCommands.GET_SLAVE_ID, ord("X"), ord("C"), ord("P"), mode)
2204
+ return types.GetSlaveIdResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
2205
+
2206
+ def getDaqId(self, daq_list_number: int):
2207
+ response = self.transportLayerCmd(types.TransportLayerCommands.GET_DAQ_ID, *self.WORD_pack(daq_list_number))
2208
+ # if response:
2209
+ return types.GetDaqIdResponse.parse(response, byteOrder=self.slaveProperties.byteOrder)
2210
+
2211
+ def setDaqId(self, daq_list_number: int, identifier: int):
2212
+ response = self.transportLayerCmd(
2213
+ types.TransportLayerCommands.SET_DAQ_ID, *self.WORD_pack(daq_list_number), *self.DWORD_pack(identifier)
2214
+ )
2215
+ return response
2216
+
2217
+ # Convenience Functions.
2218
+ def verify(self, addr, length):
2219
+ """Convenience function for verification of a data-transfer from slave
2220
+ to master (Not part of the XCP Specification).
2221
+
2222
+ Parameters
2223
+ ----------
2224
+ addr : int
2225
+ length : int
2226
+
2227
+ Returns
2228
+ -------
2229
+ bool
2230
+ """
2231
+ self.setMta(addr)
2232
+ cs = self.buildChecksum(length)
2233
+ self.logger.debug(f"BuildChecksum return'd: 0x{cs.checksum:08X} [{cs.checksumType}]")
2234
+ self.setMta(addr)
2235
+ data = self.fetch(length)
2236
+ cc = checksum.check(data, cs.checksumType)
2237
+ self.logger.debug(f"Our checksum : 0x{cc:08X}")
2238
+ return cs.checksum == cc
2239
+
2240
+ def getDaqInfo(self, include_event_lists=True):
2241
+ """Get DAQ information: processor, resolution, events."""
2242
+ result = {}
2243
+ dpi = self.getDaqProcessorInfo()
2244
+ processorInfo = {
2245
+ "minDaq": dpi["minDaq"],
2246
+ "maxDaq": dpi["maxDaq"],
2247
+ "properties": {
2248
+ "configType": dpi["daqProperties"]["daqConfigType"],
2249
+ "overloadEvent": dpi["daqProperties"]["overloadEvent"],
2250
+ "overloadMsb": dpi["daqProperties"]["overloadMsb"],
2251
+ "prescalerSupported": dpi["daqProperties"]["prescalerSupported"],
2252
+ "pidOffSupported": dpi["daqProperties"]["pidOffSupported"],
2253
+ "timestampSupported": dpi["daqProperties"]["timestampSupported"],
2254
+ "bitStimSupported": dpi["daqProperties"]["bitStimSupported"],
2255
+ "resumeSupported": dpi["daqProperties"]["resumeSupported"],
2256
+ },
2257
+ "keyByte": {
2258
+ "identificationField": dpi["daqKeyByte"]["Identification_Field"],
2259
+ "addressExtension": dpi["daqKeyByte"]["Address_Extension"],
2260
+ "optimisationType": dpi["daqKeyByte"]["Optimisation_Type"],
2261
+ },
2262
+ }
2263
+ result["processor"] = processorInfo
2264
+
2265
+ dri = self.getDaqResolutionInfo()
2266
+ resolutionInfo = {
2267
+ "timestampTicks": dri["timestampTicks"],
2268
+ "maxOdtEntrySizeDaq": dri["maxOdtEntrySizeDaq"],
2269
+ "maxOdtEntrySizeStim": dri["maxOdtEntrySizeStim"],
2270
+ "granularityOdtEntrySizeDaq": dri["granularityOdtEntrySizeDaq"],
2271
+ "granularityOdtEntrySizeStim": dri["granularityOdtEntrySizeStim"],
2272
+ "timestampMode": {
2273
+ "unit": dri["timestampMode"]["unit"],
2274
+ "fixed": dri["timestampMode"]["fixed"],
2275
+ "size": dri["timestampMode"]["size"],
2276
+ },
2277
+ }
2278
+ result["resolution"] = resolutionInfo
2279
+ channels = []
2280
+ daq_events = []
2281
+ if include_event_lists:
2282
+ for ecn in range(dpi.maxEventChannel):
2283
+ eci = self.getDaqEventInfo(ecn)
2284
+ cycle = eci["eventChannelTimeCycle"]
2285
+ maxDaqList = eci["maxDaqList"]
2286
+ priority = eci["eventChannelPriority"]
2287
+ time_unit = eci["eventChannelTimeUnit"]
2288
+ consistency = eci["daqEventProperties"]["consistency"]
2289
+ daq_supported = eci["daqEventProperties"]["daq"]
2290
+ stim_supported = eci["daqEventProperties"]["stim"]
2291
+ packed_supported = eci["daqEventProperties"]["packed"]
2292
+ name = self.fetch(eci.eventChannelNameLength)
2293
+ if name:
2294
+ name = decode_bytes(name)
2295
+ channel = {
2296
+ "name": name,
2297
+ "priority": eci["eventChannelPriority"],
2298
+ "unit": eci["eventChannelTimeUnit"],
2299
+ "cycle": eci["eventChannelTimeCycle"],
2300
+ "maxDaqList": eci["maxDaqList"],
2301
+ "properties": {
2302
+ "consistency": consistency,
2303
+ "daq": daq_supported,
2304
+ "stim": stim_supported,
2305
+ "packed": packed_supported,
2306
+ },
2307
+ }
2308
+ daq_event_info = DaqEventInfo(
2309
+ name,
2310
+ types.EVENT_CHANNEL_TIME_UNIT_TO_EXP[time_unit],
2311
+ cycle,
2312
+ maxDaqList,
2313
+ priority,
2314
+ consistency,
2315
+ daq_supported,
2316
+ stim_supported,
2317
+ packed_supported,
2318
+ )
2319
+ daq_events.append(daq_event_info)
2320
+ channels.append(channel)
2321
+ result["channels"] = channels
2322
+ self.stim.setDaqEventInfo(daq_events)
2323
+ return result
2324
+
2325
+ def getCurrentProtectionStatus(self):
2326
+ """"""
2327
+ if self.currentProtectionStatus is None:
2328
+ try:
2329
+ status = self.getStatus()
2330
+ except Exception: # may temporary ERR_OUT_OF_RANGE
2331
+ return {"dbg": None, "pgm": None, "stim": None, "daq": None, "calpag": None}
2332
+ self._setProtectionStatus(status.resourceProtectionStatus)
2333
+ return self.currentProtectionStatus
2334
+
2335
+ def _setProtectionStatus(self, protection):
2336
+ """"""
2337
+ self.currentProtectionStatus = {
2338
+ "dbg": protection.dbg,
2339
+ "pgm": protection.pgm,
2340
+ "stim": protection.stim,
2341
+ "daq": protection.daq,
2342
+ "calpag": protection.calpag,
2343
+ }
2344
+
2345
+ def cond_unlock(self, resources=None):
2346
+ """Conditionally unlock resources, i.e. only unlock locked resources.
2347
+
2348
+ Precondition: Parameter "SEED_N_KEY_DLL" must be present and point to a valid DLL/SO.
2349
+
2350
+ Parameters
2351
+ ----------
2352
+ resources: str
2353
+ Comma or space separated list of resources, e.g. "DAQ, CALPAG".
2354
+ The names are not case-sensitive.
2355
+ Valid identifiers are: "calpag", "daq", "dbg", "pgm", "stim".
2356
+
2357
+ If omitted, try to unlock every available resource.
2358
+
2359
+ Raises
2360
+ ------
2361
+ ValueError
2362
+ Invalid resource name.
2363
+
2364
+ `dllif.SeedNKeyError`
2365
+ In case of DLL related issues.
2366
+ """
2367
+ import re
2368
+
2369
+ from pyxcp.dllif import SeedNKeyError, SeedNKeyResult, getKey
2370
+
2371
+ MAX_PAYLOAD = self.slaveProperties["maxCto"] - 2
2372
+
2373
+ protection_status = self.getCurrentProtectionStatus()
2374
+ if any(protection_status.values()) and (not (self.seed_n_key_dll or self.seed_n_key_function)):
2375
+ raise RuntimeError("Neither seed-and-key DLL nor function specified, cannot proceed.") # TODO: ConfigurationError
2376
+ if resources is None:
2377
+ result = []
2378
+ if self.slaveProperties["supportsCalpag"]:
2379
+ result.append("calpag")
2380
+ if self.slaveProperties["supportsDaq"]:
2381
+ result.append("daq")
2382
+ if self.slaveProperties["supportsStim"]:
2383
+ result.append("stim")
2384
+ if self.slaveProperties["supportsPgm"]:
2385
+ result.append("pgm")
2386
+ resources = ",".join(result)
2387
+ resource_names = [r.lower() for r in re.split(r"[ ,]", resources) if r]
2388
+ for name in resource_names:
2389
+ if name not in types.RESOURCE_VALUES:
2390
+ raise ValueError(f"Invalid resource name {name!r}.")
2391
+ if not protection_status[name]:
2392
+ continue
2393
+ resource_value = types.RESOURCE_VALUES[name]
2394
+ result = self.getSeed(types.XcpGetSeedMode.FIRST_PART, resource_value)
2395
+ seed = list(result.seed)
2396
+ length = result.length
2397
+ if length == 0:
2398
+ continue
2399
+ if length > MAX_PAYLOAD:
2400
+ remaining = length - len(seed)
2401
+ while remaining > 0:
2402
+ result = self.getSeed(types.XcpGetSeedMode.REMAINING, resource_value)
2403
+ seed.extend(list(result.seed))
2404
+ remaining -= result.length
2405
+ self.logger.debug(f"Got seed {seed!r} for resource {resource_value!r}.")
2406
+ if self.seed_n_key_function:
2407
+ key = self.seed_n_key_function(resource_value, bytes(seed))
2408
+ self.logger.debug(f"Using seed and key function {self.seed_n_key_function.__name__!r}().")
2409
+ result = SeedNKeyResult.ACK
2410
+ elif self.seed_n_key_dll:
2411
+ self.logger.debug(f"Using seed and key DLL {self.seed_n_key_dll!r}.")
2412
+ result, key = getKey(
2413
+ self.logger,
2414
+ self.config.custom_dll_loader,
2415
+ self.seed_n_key_dll,
2416
+ resource_value,
2417
+ bytes(seed),
2418
+ self.seed_n_key_dll_same_bit_width,
2419
+ )
2420
+ if result == SeedNKeyResult.ACK:
2421
+ key = list(key)
2422
+ self.logger.debug(f"Unlocking resource {resource_value!r} with key {key!r}.")
2423
+ remaining = len(key)
2424
+ while key:
2425
+ data = key[:MAX_PAYLOAD]
2426
+ key_len = len(data)
2427
+ self.unlock(remaining, data)
2428
+ key = key[MAX_PAYLOAD:]
2429
+ remaining -= key_len
2430
+ else:
2431
+ raise SeedNKeyError(f"SeedAndKey DLL returned: {SeedNKeyResult(result).name!r}")
2432
+
2433
+ def identifier(self, id_value: int) -> str:
2434
+ """Return the identifier for the given value.
2435
+ Use this method instead of calling `getId()` directly.
2436
+
2437
+ Parameters
2438
+ ----------
2439
+ id_value: int
2440
+ For standard identifiers, use the constants from `pyxcp.types.XcpGetIdType`.
2441
+
2442
+ Returns
2443
+ -------
2444
+ str
2445
+ """
2446
+ gid = self.getId(id_value)
2447
+ if (gid.mode & 0x01) == 0x01:
2448
+ value = bytes(gid.identification or b"")
2449
+ else:
2450
+ value = self.fetch(gid.length)
2451
+ return decode_bytes(value)
2452
+
2453
+ def id_scanner(self, scan_ranges: Collection[Collection[int]] | None = None) -> dict[str, str]:
2454
+ """Scan for available standard identification types (GET_ID).
2455
+
2456
+ Parameters
2457
+ ----------
2458
+ scan_ranges: Optional[Collection[Collection[int]]]
2459
+
2460
+ - If parameter is omitted or `None` test every standard identification type (s. GET_ID service)
2461
+ plus extensions by Vector Informatik.
2462
+ - Else `scan_ranges` must be a list-of-list.
2463
+ e.g: [[12, 80], [123], [240, 16, 35]]
2464
+ - The first list is a range (closed interval).
2465
+ - The second is a single value.
2466
+ - The third is a value list.
2467
+
2468
+ Returns
2469
+ -------
2470
+ Dict[str, str]
2471
+
2472
+ """
2473
+ result = {}
2474
+
2475
+ def make_generator(sr):
2476
+ STD_IDS = {int(v): k for k, v in types.XcpGetIdType.__members__.items()}
2477
+ if sr is None:
2478
+ scan_range = STD_IDS.keys()
2479
+ else:
2480
+ scan_range = []
2481
+ if not isinstance(sr, Collection):
2482
+ raise TypeError("scan_ranges must be of type `Collection`")
2483
+ for element in sr:
2484
+ if not isinstance(element, Collection):
2485
+ raise TypeError("scan_ranges elements must be of type `Collection`")
2486
+ if not element:
2487
+ raise ValueError("scan_ranges elements cannot be empty")
2488
+ if len(element) == 1:
2489
+ scan_range.append(element[0]) # Single value
2490
+ elif len(element) == 2:
2491
+ start, stop = element # Value range
2492
+ scan_range.extend(list(range(start, stop + 1)))
2493
+ else:
2494
+ scan_range.extend(element) # Value list.
2495
+ scan_range = sorted(frozenset(scan_range))
2496
+
2497
+ def generate():
2498
+ for idx, id_value in enumerate(scan_range):
2499
+ if id_value in STD_IDS:
2500
+ name = STD_IDS[id_value]
2501
+ else:
2502
+ name = f"USER_{idx}"
2503
+ yield (
2504
+ id_value,
2505
+ name,
2506
+ )
2507
+
2508
+ return generate()
2509
+
2510
+ gen = make_generator(scan_ranges)
2511
+ for id_value, name in gen:
2512
+ # Avoid noisy warnings while probing
2513
+ status, response = self.try_command(self.identifier, id_value, silent=True)
2514
+ if status == types.TryCommandResult.OK and response:
2515
+ result[name] = response
2516
+ continue
2517
+ if status == types.TryCommandResult.NOT_IMPLEMENTED:
2518
+ # GET_ID not supported by the slave at all → stop scanning
2519
+ break
2520
+ if status == types.TryCommandResult.XCP_ERROR:
2521
+ # Some IDs may not be supported; ignore typical probe errors
2522
+ try:
2523
+ err = response.error_code
2524
+ except Exception:
2525
+ err = None
2526
+ if err in (types.XcpError.ERR_OUT_OF_RANGE, types.XcpError.ERR_CMD_SYNTAX):
2527
+ continue
2528
+ # For any other XCP error, keep scanning (best-effort) instead of aborting
2529
+ continue
2530
+ if status == types.TryCommandResult.OTHER_ERROR:
2531
+ raise RuntimeError(f"Error while scanning for ID {id_value}: {response!r}")
2532
+ return result
2533
+
2534
+ @property
2535
+ def start_datetime(self) -> int:
2536
+ """"""
2537
+ return self.transport.start_datetime
2538
+
2539
+ def try_command(self, cmd: Callable, *args, **kws) -> tuple[types.TryCommandResult, Any]:
2540
+ """Call master functions and handle XCP errors more gracefuly.
2541
+
2542
+ Parameter
2543
+ ---------
2544
+ cmd: Callable
2545
+ args: list
2546
+ variable length arguments to `cmd`.
2547
+ kws: dict
2548
+ keyword arguments to `cmd`.
2549
+
2550
+ `extra_msg`: str
2551
+ Additional info to log message (not passed to `cmd`).
2552
+
2553
+ Returns
2554
+ -------
2555
+
2556
+ Note
2557
+ ----
2558
+ Mainly used for plug-and-play applications, e.g. `id_scanner` may confronted with `ERR_OUT_OF_RANGE` errors, which
2559
+ is normal for this kind of applications -- or to test for optional commands.
2560
+ Use carefuly not to hide serious error causes.
2561
+ """
2562
+ # Suppress logging of expected XCP negative responses during try_command
2563
+ _prev_suppress = is_suppress_xcp_error_log()
2564
+ set_suppress_xcp_error_log(True)
2565
+ try:
2566
+ extra_msg: str | None = kws.get("extra_msg")
2567
+ if extra_msg:
2568
+ kws.pop("extra_msg")
2569
+ else:
2570
+ extra_msg = ""
2571
+ silent: bool | None = kws.get("silent")
2572
+ if silent:
2573
+ kws.pop("silent")
2574
+ else:
2575
+ silent = False
2576
+ res = cmd(*args, **kws)
2577
+ except SystemExit as e:
2578
+ # restore suppression flag before handling
2579
+ set_suppress_xcp_error_log(_prev_suppress)
2580
+ # print(f"\tUnexpected error while executing command {cmd.__name__!r}: {e!r}")
2581
+ if e.error_code == types.XcpError.ERR_CMD_UNKNOWN:
2582
+ # This is a rather common use-case, so let the user know that there is some functionality missing.
2583
+ if not silent:
2584
+ if extra_msg:
2585
+ self.logger.warning(f"Optional command {cmd.__name__!r} not implemented -- {extra_msg!r}")
2586
+ else:
2587
+ self.logger.warning(f"Optional command {cmd.__name__!r} not implemented.")
2588
+ return (types.TryCommandResult.NOT_IMPLEMENTED, e)
2589
+ else:
2590
+ return (types.TryCommandResult.XCP_ERROR, e)
2591
+ except Exception as e:
2592
+ return (types.TryCommandResult.OTHER_ERROR, e)
2593
+ else:
2594
+ return (types.TryCommandResult.OK, res)
2595
+ finally:
2596
+ # Ensure suppression flag is restored even on success/other exceptions
2597
+ try:
2598
+ set_suppress_xcp_error_log(_prev_suppress)
2599
+ except Exception:
2600
+ pass # nosec B110
2601
+
2602
+
2603
+ def ticks_to_seconds(ticks, resolution):
2604
+ """Convert DAQ timestamp/tick value to seconds.
2605
+
2606
+ Parameters
2607
+ ----------
2608
+ ticks: int
2609
+
2610
+ unit: `GetDaqResolutionInfoResponse` as returned by :meth:`getDaqResolutionInfo`
2611
+ """
2612
+ warnings.warn(
2613
+ "ticks_to_seconds() deprecated, use factory :func:`make_tick_converter` instead.",
2614
+ Warning,
2615
+ stacklevel=1,
2616
+ )
2617
+ return (10 ** types.DAQ_TIMESTAMP_UNIT_TO_EXP[resolution.timestampMode.unit]) * resolution.timestampTicks * ticks
2618
+
2619
+
2620
+ def make_tick_converter(resolution):
2621
+ """Make a function that converts tick count from XCP slave to seconds.
2622
+
2623
+ Parameters
2624
+ ----------
2625
+ resolution: `GetDaqResolutionInfoResponse` as returned by :meth:`getDaqResolutionInfo`
2626
+
2627
+ """
2628
+ exponent = types.DAQ_TIMESTAMP_UNIT_TO_EXP[resolution.timestampMode.unit]
2629
+ tick_resolution = resolution.timestampTicks
2630
+ base = (10**exponent) * tick_resolution
2631
+
2632
+ def ticks_to_seconds(ticks):
2633
+ """Convert DAQ timestamp/tick value to seconds.
2634
+
2635
+ Parameters
2636
+ ----------
2637
+ ticks: int
2638
+
2639
+ Returns
2640
+ -------
2641
+ float
2642
+ """
2643
+ return base * ticks
2644
+
2645
+ return ticks_to_seconds