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