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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python
2
- """Implements error-handling according to XCP spec.
3
- """
2
+ """Implements error-handling according to XCP spec."""
3
+
4
4
  import functools
5
5
  import logging
6
6
  import threading
@@ -17,6 +17,25 @@ from pyxcp.types import COMMAND_CATEGORIES, XcpError, XcpResponseError, XcpTimeo
17
17
 
18
18
  handle_errors = True # enable/disable XCP error-handling.
19
19
 
20
+ # Thread-local flag to suppress logging for expected XCP negative responses
21
+
22
+
23
+ _thread_flags = threading.local()
24
+
25
+
26
+ def set_suppress_xcp_error_log(value: bool) -> None:
27
+ try:
28
+ _thread_flags.suppress_xcp_error_log = bool(value)
29
+ except Exception:
30
+ pass
31
+
32
+
33
+ def is_suppress_xcp_error_log() -> bool:
34
+ try:
35
+ return bool(getattr(_thread_flags, "suppress_xcp_error_log", False))
36
+ except Exception:
37
+ return False
38
+
20
39
 
21
40
  class SingletonBase:
22
41
  _lock = threading.Lock()
@@ -196,6 +215,166 @@ class Handler:
196
215
  self._repeater = None
197
216
  self.logger = logging.getLogger("PyXCP")
198
217
 
218
+ def _diagnostics_enabled(self) -> bool:
219
+ try:
220
+ app = getattr(self.instance, "config", None)
221
+ if app is None:
222
+ return True
223
+ general = getattr(app, "general", None)
224
+ if general is None:
225
+ return True
226
+ return bool(getattr(general, "diagnostics_on_failure", True))
227
+ except Exception:
228
+ return True
229
+
230
+ def _build_transport_diagnostics(self) -> str:
231
+ try:
232
+ transport = getattr(self.instance, "transport", None)
233
+ if transport is None:
234
+ return ""
235
+ if hasattr(transport, "_build_diagnostics_dump"):
236
+ return transport._build_diagnostics_dump() # type: ignore[attr-defined]
237
+ except Exception:
238
+ pass
239
+ return ""
240
+
241
+ def _append_diag(self, msg: str) -> str:
242
+ # Suppress diagnostics entirely when XCP error logging is suppressed (e.g., try_command probing)
243
+ if is_suppress_xcp_error_log():
244
+ return msg
245
+ if not self._diagnostics_enabled():
246
+ return msg
247
+ diag = self._build_transport_diagnostics()
248
+ if not diag:
249
+ return msg
250
+ # Prefer a Rich-formatted table for compact, readable diagnostics.
251
+ try:
252
+ header = "--- Diagnostics (for troubleshooting) ---"
253
+ body = diag
254
+ if "\n" in diag and diag.startswith("--- Diagnostics"):
255
+ header, body = diag.split("\n", 1)
256
+
257
+ # Try to parse the structured JSON body produced by transports
258
+ import json as _json # Local import to avoid hard dependency at module import time
259
+
260
+ payload = _json.loads(body)
261
+ transport_params = payload.get("transport_params") or {}
262
+ last_pdus = payload.get("last_pdus") or []
263
+
264
+ # Try to use rich if available
265
+ try:
266
+ from rich.console import Console
267
+ from rich.table import Table
268
+ from rich.panel import Panel
269
+ from textwrap import shorten
270
+
271
+ console = Console(file=None, force_terminal=False, width=120, record=True, markup=False)
272
+
273
+ # Transport parameters table
274
+ tp_table = Table(title="Transport Parameters", title_style="bold", show_header=True, header_style="bold magenta")
275
+ tp_table.add_column("Key", style="cyan", no_wrap=True)
276
+ tp_table.add_column("Value", style="white")
277
+ from rich.markup import escape as _escape
278
+
279
+ for k, v in (transport_params or {}).items():
280
+ # Convert complex values to compact repr
281
+ sv = repr(v)
282
+ sv = shorten(sv, width=80, placeholder="…")
283
+ tp_table.add_row(_escape(str(k)), _escape(sv))
284
+
285
+ # Last PDUs table (most recent last)
286
+ pdu_table = Table(
287
+ title="Last PDUs (most recent last)", title_style="bold", show_header=True, header_style="bold magenta"
288
+ )
289
+ for col in ("dir", "cat", "ctr", "ts", "len", "data"):
290
+ pdu_table.add_column(col, no_wrap=(col in {"dir", "cat", "ctr", "len"}), style="white")
291
+ for pdu in last_pdus:
292
+ try:
293
+ dir_ = str(pdu.get("dir", ""))
294
+ cat = str(pdu.get("cat", ""))
295
+ ctr = str(pdu.get("ctr", ""))
296
+ # Format timestamp: convert ns -> s with 5 decimals if numeric
297
+ ts_val = pdu.get("ts", "")
298
+ try:
299
+ ts_num = int(ts_val)
300
+ ts = f"{ts_num / 1_000_000_000:.5f}"
301
+ except Exception:
302
+ ts = str(ts_val)
303
+ ln = str(pdu.get("len", ""))
304
+ # Prefer showing actual data content; avoid repr quotes
305
+ data_val = pdu.get("data", "")
306
+ try:
307
+ if isinstance(data_val, (bytes, bytearray, list, tuple)):
308
+ # Lazily import to avoid hard dependency
309
+ from pyxcp.utils import hexDump as _hexDump
310
+
311
+ data_str = _hexDump(data_val)
312
+ else:
313
+ data_str = str(data_val)
314
+ except Exception:
315
+ data_str = str(data_val)
316
+ # Shorten potentially huge values to keep table compact
317
+ from textwrap import shorten as _shorten
318
+
319
+ ts = _shorten(ts, width=20, placeholder="…")
320
+ data = _shorten(data_str, width=40, placeholder="…")
321
+ # Escape strings to avoid Rich markup interpretation (e.g., '[' ']' in hex dumps)
322
+ dir_e = _escape(dir_)
323
+ cat_e = _escape(cat)
324
+ ctr_e = _escape(ctr)
325
+ ts_e = _escape(ts)
326
+ ln_e = _escape(ln)
327
+ data_e = _escape(data)
328
+ pdu_table.add_row(dir_e, cat_e, ctr_e, ts_e, ln_e, data_e)
329
+ except Exception:
330
+ # If anything odd in structure, add a single-cell row with repr
331
+ from textwrap import shorten as _shorten
332
+
333
+ pdu_table.add_row(_shorten(repr(pdu), width=80, placeholder="…"), "", "", "", "", "")
334
+
335
+ # Combine into a single panel and capture as text
336
+ console.print(Panel.fit(tp_table, title=header))
337
+ if last_pdus:
338
+ console.print(pdu_table)
339
+ rendered = console.export_text(clear=False)
340
+
341
+ except Exception:
342
+ # Rich not available or rendering failed; fallback to compact logger lines
343
+ self.logger.error(header)
344
+ if transport_params:
345
+ self.logger.error("transport_params: %s", transport_params)
346
+ if last_pdus:
347
+ self.logger.error("last_pdus (most recent last):")
348
+ for pdu in last_pdus:
349
+ try:
350
+ ts_val = pdu.get("ts", "")
351
+ try:
352
+ ts_num = int(ts_val)
353
+ ts_fmt = f"{ts_num / 1_000_000_000:.5f}"
354
+ except Exception:
355
+ ts_fmt = str(ts_val)
356
+ data_val = pdu.get("data", "")
357
+ if isinstance(data_val, (bytes, bytearray, list, tuple)):
358
+ from pyxcp.utils import hexDump as _hexDump
359
+
360
+ data_str = _hexDump(data_val)
361
+ else:
362
+ data_str = str(data_val)
363
+ pdu_copy = dict(pdu)
364
+ pdu_copy["ts"] = ts_fmt
365
+ pdu_copy["data"] = data_str
366
+ self.logger.error("%s", pdu_copy)
367
+ except Exception:
368
+ self.logger.error("%s", pdu)
369
+ except Exception:
370
+ # As a last resort, emit the whole diagnostics blob verbatim
371
+ try:
372
+ for line in diag.splitlines():
373
+ self.logger.error(line)
374
+ except Exception:
375
+ pass
376
+ return msg
377
+
199
378
  def __str__(self):
200
379
  return f"Handler(func = {func_name(self.func)} -- {self.arguments} service = {self.service} error_code = {self.error_code})"
201
380
 
@@ -268,16 +447,16 @@ class Handler:
268
447
  if item == Action.NONE:
269
448
  pass
270
449
  elif item == Action.DISPLAY_ERROR:
271
- raise SystemExit("Could not proceed due to unhandled error (DISPLAY_ERROR).", self.error_code)
450
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (DISPLAY_ERROR)."), self.error_code)
272
451
  elif item == Action.RETRY_SYNTAX:
273
- raise SystemExit("Could not proceed due to unhandled error (RETRY_SYNTAX).", self.error_code)
452
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (RETRY_SYNTAX)."), self.error_code)
274
453
  elif item == Action.RETRY_PARAM:
275
- raise SystemExit("Could not proceed due to unhandled error (RETRY_PARAM).", self.error_code)
454
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (RETRY_PARAM)."), self.error_code)
276
455
  elif item == Action.USE_A2L:
277
- raise SystemExit("Could not proceed due to unhandled error (USE_A2L).", self.error_code)
456
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (USE_A2L)."), self.error_code)
278
457
  elif item == Action.USE_ALTERATIVE:
279
458
  raise SystemExit(
280
- "Could not proceed due to unhandled error (USE_ALTERATIVE).", self.error_code
459
+ self._append_diag("Could not proceed due to unhandled error (USE_ALTERATIVE)."), self.error_code
281
460
  ) # TODO: check alternatives.
282
461
  elif item == Action.REPEAT:
283
462
  repetitionCount = Repeater.REPEAT
@@ -286,13 +465,15 @@ class Handler:
286
465
  elif item == Action.REPEAT_INF_TIMES:
287
466
  repetitionCount = Repeater.INFINITE
288
467
  elif item == Action.RESTART_SESSION:
289
- raise SystemExit("Could not proceed due to unhandled error (RESTART_SESSION).", self.error_code)
468
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (RESTART_SESSION)."), self.error_code)
290
469
  elif item == Action.TERMINATE_SESSION:
291
- raise SystemExit("Could not proceed due to unhandled error (TERMINATE_SESSION).", self.error_code)
470
+ raise SystemExit(
471
+ self._append_diag("Could not proceed due to unhandled error (TERMINATE_SESSION)."), self.error_code
472
+ )
292
473
  elif item == Action.SKIP:
293
474
  pass
294
475
  elif item == Action.NEW_FLASH_WARE:
295
- raise SystemExit("Could not proceed due to unhandled error (NEW_FLASH_WARE)", self.error_code)
476
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (NEW_FLASH_WARE)"), self.error_code)
296
477
  return result_pre_actions, result_actions, Repeater(repetitionCount)
297
478
 
298
479
 
@@ -366,6 +547,48 @@ class Executor(SingletonBase):
366
547
  # self.logger.critical(f"XcpResponseError [{e.get_error_code()}]")
367
548
  self.error_code = e.get_error_code()
368
549
  handler.error_code = self.error_code
550
+ try:
551
+ svc = getattr(inst.service, "name", None)
552
+ # Derive a human-friendly error name if available
553
+ try:
554
+ err_name = (
555
+ getattr(XcpError, int(self.error_code)).name
556
+ if hasattr(XcpError, "__members__")
557
+ else str(self.error_code)
558
+ )
559
+ except Exception:
560
+ # Fallbacks: try enum-style .name or string conversion
561
+ err_name = getattr(self.error_code, "name", None) or str(self.error_code)
562
+ try:
563
+ err_code_int = int(self.error_code)
564
+ except Exception:
565
+ err_code_int = self.error_code # best effort
566
+ msg = f"XCP negative response: {err_name} (0x{err_code_int:02X})"
567
+ if svc:
568
+ msg += f" on service {svc}"
569
+ # Suppress noisy ERROR log if requested by caller context
570
+ if is_suppress_xcp_error_log():
571
+ self.logger.debug(
572
+ msg,
573
+ extra={
574
+ "event": "xcp_error_suppressed",
575
+ "service": svc,
576
+ "error_code": err_code_int,
577
+ "error_name": err_name,
578
+ },
579
+ )
580
+ else:
581
+ self.logger.error(
582
+ msg,
583
+ extra={
584
+ "event": "xcp_error",
585
+ "service": svc,
586
+ "error_code": err_code_int,
587
+ "error_name": err_name,
588
+ },
589
+ )
590
+ except Exception:
591
+ pass
369
592
  except XcpTimeoutError:
370
593
  is_connect = func.__name__ == "connect"
371
594
  self.logger.warning(f"XcpTimeoutError -- Service: {func.__name__!r}")
@@ -405,9 +628,21 @@ class Executor(SingletonBase):
405
628
  if handler.repeater.repeat():
406
629
  continue
407
630
  else:
408
- raise UnrecoverableError(
409
- f"Max. repetition count reached while trying to execute service {handler.func.__name__!r}."
410
- )
631
+ msg = f"Max. repetition count reached while trying to execute service {handler.func.__name__!r}."
632
+ # Try to append diagnostics from the transport
633
+ try:
634
+ if hasattr(handler, "_append_diag"):
635
+ msg = handler._append_diag(msg)
636
+ except Exception:
637
+ pass
638
+ try:
639
+ self.logger.error(
640
+ "XCP unrecoverable",
641
+ extra={"event": "xcp_unrecoverable", "service": getattr(inst.service, "name", None)},
642
+ )
643
+ except Exception:
644
+ pass
645
+ raise UnrecoverableError(msg)
411
646
  finally:
412
647
  # cleanup of class variables
413
648
  self.previous_error_code = None