pyxcp 0.25.0__cp313-cp313-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyxcp might be problematic. Click here for more details.

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