pyxcp 0.23.3__cp312-cp312-win_arm64.whl → 0.25.6__cp312-cp312-win_arm64.whl

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