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