pyxcp 0.23.4__cp313-cp313-win_arm64.whl → 0.23.7__cp313-cp313-win_arm64.whl

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

Potentially problematic release.


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

pyxcp/__init__.py CHANGED
@@ -17,4 +17,4 @@ tb_install(show_locals=True, max_frames=3) # Install custom exception handler.
17
17
 
18
18
  # if you update this manually, do not forget to update
19
19
  # .bumpversion.cfg and pyproject.toml.
20
- __version__ = "0.23.4"
20
+ __version__ = "0.23.7"
pyxcp/cmdline.py CHANGED
@@ -55,7 +55,7 @@ class ArgumentParser:
55
55
  self._description = description
56
56
 
57
57
  def run(self, policy=None, transport_layer_interface=None):
58
- """Create and configure a master instance.
58
+ """Create and configure a synchronous master instance.
59
59
 
60
60
  Args:
61
61
  policy: Optional policy to use for the master
pyxcp/config/__init__.py CHANGED
@@ -913,6 +913,10 @@ class General(Configurable):
913
913
  connect_retries = Integer(help="Number of CONNECT retries (None for infinite retries).", allow_none=True, default_value=3).tag(
914
914
  config=True
915
915
  )
916
+ # Structured diagnostics dump options
917
+ diagnostics_on_failure = Bool(True, help="Append a structured diagnostics dump to timeout errors.").tag(config=True)
918
+ diagnostics_last_pdus = Integer(20, help="How many recent PDUs to include in diagnostics dump.").tag(config=True)
919
+
916
920
  seed_n_key_dll = Unicode("", allow_none=False, help="Dynamic library used for slave resource unlocking.").tag(config=True)
917
921
  seed_n_key_dll_same_bit_width = Bool(False, help="").tag(config=True)
918
922
  custom_dll_loader = Unicode(allow_none=True, default_value=None, help="Use an custom seed and key DLL loader.").tag(config=True)
@@ -1010,6 +1014,11 @@ class PyXCP(Application):
1010
1014
  config=True
1011
1015
  )
1012
1016
 
1017
+ # Logging options
1018
+ structured_logging = Bool(False, help="Emit one-line JSON logs instead of rich text.").tag(config=True)
1019
+ # Use log_output_format to avoid clashing with traitlets.Application.log_format (a %-style template)
1020
+ log_output_format = Enum(values=["rich", "json"], default_value="rich", help="Select logging output format.").tag(config=True)
1021
+
1013
1022
  classes = List([General, Transport, CustomArgs])
1014
1023
 
1015
1024
  subcommands = dict(
@@ -1026,34 +1035,79 @@ class PyXCP(Application):
1026
1035
  self.subapp.start()
1027
1036
  exit(2)
1028
1037
  else:
1029
- has_handlers = logging.getLogger().hasHandlers()
1030
- if has_handlers:
1031
- self.log = logging.getLogger()
1032
- self._read_configuration(self.config_file)
1033
- else:
1034
- self._read_configuration(self.config_file)
1035
- self._setup_logger()
1038
+ # Always read configuration and then set up our logger explicitly to avoid
1039
+ # traitlets.Application default logging using an incompatible 'log_format'.
1040
+ self._read_configuration(self.config_file)
1041
+ try:
1042
+ # Ensure base Application.log_format is a valid %-style template
1043
+ # (Users might set c.PyXCP.log_format = "json" which clashes with traitlets behavior.)
1044
+ self.log_format = "%(message)s" # type: ignore[assignment]
1045
+ except Exception:
1046
+ pass
1047
+ self._setup_logger()
1036
1048
  self.log.debug(f"pyxcp version: {self.version}")
1037
1049
 
1038
1050
  def _setup_logger(self):
1039
1051
  from pyxcp.types import Command
1040
1052
 
1041
1053
  # Remove any handlers installed by `traitlets`.
1042
- for hdl in self.log.handlers:
1054
+ for hdl in list(self.log.handlers):
1043
1055
  self.log.removeHandler(hdl)
1044
1056
 
1045
- # formatter = logging.Formatter(fmt=self.log_format, datefmt=self.log_datefmt)
1046
-
1047
- keywords = list(Command.__members__.keys()) + ["ARGS", "KWS"] # Syntax highlight XCP commands and other stuff.
1048
- rich_handler = RichHandler(
1049
- rich_tracebacks=True,
1050
- tracebacks_show_locals=True,
1051
- log_time_format=self.log_datefmt,
1052
- level=self.log_level,
1053
- keywords=keywords,
1054
- )
1055
- # rich_handler.setFormatter(formatter)
1056
- self.log.addHandler(rich_handler)
1057
+ # Decide formatter/handler based on config
1058
+ use_json = False
1059
+ try:
1060
+ # Prefer explicit log_output_format; fallback to structured_logging for compatibility
1061
+ use_json = getattr(self, "log_output_format", "rich") == "json" or getattr(self, "structured_logging", False)
1062
+ # Backward-compat: if someone set PyXCP.log_format="json" in config, honor it here too
1063
+ if not use_json:
1064
+ lf = getattr(self, "log_format", None)
1065
+ if isinstance(lf, str) and lf.lower() == "json":
1066
+ use_json = True
1067
+ except Exception:
1068
+ use_json = False
1069
+
1070
+ if use_json:
1071
+
1072
+ class JSONFormatter(logging.Formatter):
1073
+ def format(self, record: logging.LogRecord) -> str:
1074
+ # Build a minimal structured payload
1075
+ payload = {
1076
+ "time": self.formatTime(record, self.datefmt),
1077
+ "level": record.levelname,
1078
+ "logger": record.name,
1079
+ "message": record.getMessage(),
1080
+ }
1081
+ # Include extras if present
1082
+ for key in ("transport", "host", "port", "protocol", "event", "command"):
1083
+ if hasattr(record, key):
1084
+ payload[key] = getattr(record, key)
1085
+ # Exceptions
1086
+ if record.exc_info:
1087
+ payload["exc_type"] = record.exc_info[0].__name__ if record.exc_info[0] else None
1088
+ payload["exc_text"] = self.formatException(record.exc_info)
1089
+ try:
1090
+ import json as _json
1091
+
1092
+ return _json.dumps(payload, ensure_ascii=False)
1093
+ except Exception:
1094
+ return f"{payload}"
1095
+
1096
+ handler = logging.StreamHandler()
1097
+ formatter = JSONFormatter(datefmt=self.log_datefmt)
1098
+ handler.setFormatter(formatter)
1099
+ handler.setLevel(self.log_level)
1100
+ self.log.addHandler(handler)
1101
+ else:
1102
+ keywords = list(Command.__members__.keys()) + ["ARGS", "KWS"] # Syntax highlight XCP commands and other stuff.
1103
+ rich_handler = RichHandler(
1104
+ rich_tracebacks=True,
1105
+ tracebacks_show_locals=True,
1106
+ log_time_format=self.log_datefmt,
1107
+ level=self.log_level,
1108
+ keywords=keywords,
1109
+ )
1110
+ self.log.addHandler(rich_handler)
1057
1111
 
1058
1112
  def initialize(self, argv=None):
1059
1113
  from pyxcp import __version__ as pyxcp_version
Binary file
Binary file
Binary file
Binary file
@@ -33,6 +33,8 @@ class DaqProcessor:
33
33
  def __init__(self, daq_lists: List[DaqList]):
34
34
  self.daq_lists = daq_lists
35
35
  self.log = get_application().log
36
+ # Flag indicating a fatal OS-level error occurred during DAQ (e.g., disk full, out-of-memory)
37
+ self._fatal_os_error: bool = False
36
38
 
37
39
  def setup(self, start_datetime: Optional[CurrentDatetime] = None, write_multiple: bool = True):
38
40
  if not self.xcp_master.slaveProperties.supportsDaq:
@@ -163,6 +165,31 @@ class DaqProcessor:
163
165
  self.xcp_master.startStopSynch(0x01)
164
166
 
165
167
  def stop(self):
168
+ # If a fatal OS error occurred during acquisition, skip sending stop to the slave to avoid
169
+ # cascading timeouts/unrecoverable errors and shut down transport gracefully instead.
170
+ if getattr(self, "_fatal_os_error", False):
171
+ try:
172
+ self.log.error(
173
+ "DAQ stop skipped due to previous fatal OS error (e.g., disk full or out-of-memory). Closing transport."
174
+ )
175
+ except Exception:
176
+ pass
177
+ try:
178
+ # Best-effort: stop listener and close transport so threads finish cleanly.
179
+ if hasattr(self.xcp_master, "transport") and self.xcp_master.transport is not None:
180
+ # Signal listeners to stop
181
+ try:
182
+ if hasattr(self.xcp_master.transport, "closeEvent"):
183
+ self.xcp_master.transport.closeEvent.set()
184
+ except Exception:
185
+ pass
186
+ # Close transport connection
187
+ try:
188
+ self.xcp_master.transport.close()
189
+ except Exception:
190
+ pass
191
+ finally:
192
+ return
166
193
  self.xcp_master.startStopSynch(0x00)
167
194
 
168
195
  def first_pids(self):
@@ -218,7 +245,40 @@ class DaqToCsv(DaqOnlinePolicy):
218
245
  out_file.write(f"{hdr}\n")
219
246
 
220
247
  def on_daq_list(self, daq_list: int, timestamp0: int, timestamp1: int, payload: list):
221
- self.files[daq_list].write(f"{timestamp0},{timestamp1},{', '.join([str(x) for x in payload])}\n")
248
+ # Guard against hard OS errors (e.g., disk full) during file writes.
249
+ if getattr(self, "_fatal_os_error", False):
250
+ return
251
+ try:
252
+ self.files[daq_list].write(f"{timestamp0},{timestamp1},{', '.join([str(x) for x in payload])}\n")
253
+ except (OSError, MemoryError) as ex:
254
+ # Mark fatal condition to alter shutdown path and avoid further writes/commands.
255
+ self._fatal_os_error = True
256
+ try:
257
+ self.log.critical(f"DAQ file write failed: {ex.__class__.__name__}: {ex}. Initiating graceful shutdown.")
258
+ except Exception:
259
+ pass
260
+ # Stop listener to prevent more DAQ traffic and avoid thread crashes.
261
+ try:
262
+ if hasattr(self.xcp_master, "transport") and self.xcp_master.transport is not None:
263
+ if hasattr(self.xcp_master.transport, "closeEvent"):
264
+ self.xcp_master.transport.closeEvent.set()
265
+ except Exception:
266
+ pass
267
+ # Best-effort: close any opened files to flush buffers and release resources.
268
+ try:
269
+ for f in getattr(self, "files", {}).values():
270
+ try:
271
+ f.flush()
272
+ except Exception:
273
+ pass
274
+ try:
275
+ f.close()
276
+ except Exception:
277
+ pass
278
+ except Exception:
279
+ pass
280
+ # Do not re-raise; allow the system to continue to a controlled shutdown.
281
+ return
222
282
 
223
283
  def finalize(self):
224
284
  self.log.debug("DaqCsv::finalize()")
Binary file
Binary file
Binary file
Binary file
pyxcp/examples/run_daq.py CHANGED
@@ -149,7 +149,7 @@ with ap.run(policy=daq_parser) as x:
149
149
  print("start DAQ lists.")
150
150
  daq_parser.start() # Start DAQ lists.
151
151
 
152
- time.sleep(5.0 * 60.0) # Run for 15 minutes.
152
+ time.sleep(2.0 * 60.0 * 60.0) # Run for 15 minutes.
153
153
 
154
154
  print("Stop DAQ....")
155
155
  daq_parser.stop() # Stop DAQ lists.
@@ -17,6 +17,26 @@ 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
+ import threading
22
+
23
+
24
+ _thread_flags = threading.local()
25
+
26
+
27
+ def set_suppress_xcp_error_log(value: bool) -> None:
28
+ try:
29
+ _thread_flags.suppress_xcp_error_log = bool(value)
30
+ except Exception:
31
+ pass
32
+
33
+
34
+ def is_suppress_xcp_error_log() -> bool:
35
+ try:
36
+ return bool(getattr(_thread_flags, "suppress_xcp_error_log", False))
37
+ except Exception:
38
+ return False
39
+
20
40
 
21
41
  class SingletonBase:
22
42
  _lock = threading.Lock()
@@ -196,6 +216,35 @@ class Handler:
196
216
  self._repeater = None
197
217
  self.logger = logging.getLogger("PyXCP")
198
218
 
219
+ def _diagnostics_enabled(self) -> bool:
220
+ try:
221
+ app = getattr(self.instance, "config", None)
222
+ if app is None:
223
+ return True
224
+ general = getattr(app, "general", None)
225
+ if general is None:
226
+ return True
227
+ return bool(getattr(general, "diagnostics_on_failure", True))
228
+ except Exception:
229
+ return True
230
+
231
+ def _build_transport_diagnostics(self) -> str:
232
+ try:
233
+ transport = getattr(self.instance, "transport", None)
234
+ if transport is None:
235
+ return ""
236
+ if hasattr(transport, "_build_diagnostics_dump"):
237
+ return transport._build_diagnostics_dump() # type: ignore[attr-defined]
238
+ except Exception:
239
+ pass
240
+ return ""
241
+
242
+ def _append_diag(self, msg: str) -> str:
243
+ if not self._diagnostics_enabled():
244
+ return msg
245
+ diag = self._build_transport_diagnostics()
246
+ return msg + ("\n" + diag if diag else "")
247
+
199
248
  def __str__(self):
200
249
  return f"Handler(func = {func_name(self.func)} -- {self.arguments} service = {self.service} error_code = {self.error_code})"
201
250
 
@@ -268,16 +317,16 @@ class Handler:
268
317
  if item == Action.NONE:
269
318
  pass
270
319
  elif item == Action.DISPLAY_ERROR:
271
- raise SystemExit("Could not proceed due to unhandled error (DISPLAY_ERROR).", self.error_code)
320
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (DISPLAY_ERROR)."), self.error_code)
272
321
  elif item == Action.RETRY_SYNTAX:
273
- raise SystemExit("Could not proceed due to unhandled error (RETRY_SYNTAX).", self.error_code)
322
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (RETRY_SYNTAX)."), self.error_code)
274
323
  elif item == Action.RETRY_PARAM:
275
- raise SystemExit("Could not proceed due to unhandled error (RETRY_PARAM).", self.error_code)
324
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (RETRY_PARAM)."), self.error_code)
276
325
  elif item == Action.USE_A2L:
277
- raise SystemExit("Could not proceed due to unhandled error (USE_A2L).", self.error_code)
326
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (USE_A2L)."), self.error_code)
278
327
  elif item == Action.USE_ALTERATIVE:
279
328
  raise SystemExit(
280
- "Could not proceed due to unhandled error (USE_ALTERATIVE).", self.error_code
329
+ self._append_diag("Could not proceed due to unhandled error (USE_ALTERATIVE)."), self.error_code
281
330
  ) # TODO: check alternatives.
282
331
  elif item == Action.REPEAT:
283
332
  repetitionCount = Repeater.REPEAT
@@ -286,13 +335,15 @@ class Handler:
286
335
  elif item == Action.REPEAT_INF_TIMES:
287
336
  repetitionCount = Repeater.INFINITE
288
337
  elif item == Action.RESTART_SESSION:
289
- raise SystemExit("Could not proceed due to unhandled error (RESTART_SESSION).", self.error_code)
338
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (RESTART_SESSION)."), self.error_code)
290
339
  elif item == Action.TERMINATE_SESSION:
291
- raise SystemExit("Could not proceed due to unhandled error (TERMINATE_SESSION).", self.error_code)
340
+ raise SystemExit(
341
+ self._append_diag("Could not proceed due to unhandled error (TERMINATE_SESSION)."), self.error_code
342
+ )
292
343
  elif item == Action.SKIP:
293
344
  pass
294
345
  elif item == Action.NEW_FLASH_WARE:
295
- raise SystemExit("Could not proceed due to unhandled error (NEW_FLASH_WARE)", self.error_code)
346
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (NEW_FLASH_WARE)"), self.error_code)
296
347
  return result_pre_actions, result_actions, Repeater(repetitionCount)
297
348
 
298
349
 
@@ -366,6 +417,48 @@ class Executor(SingletonBase):
366
417
  # self.logger.critical(f"XcpResponseError [{e.get_error_code()}]")
367
418
  self.error_code = e.get_error_code()
368
419
  handler.error_code = self.error_code
420
+ try:
421
+ svc = getattr(inst.service, "name", None)
422
+ # Derive a human-friendly error name if available
423
+ try:
424
+ err_name = (
425
+ getattr(XcpError, int(self.error_code)).name
426
+ if hasattr(XcpError, "__members__")
427
+ else str(self.error_code)
428
+ )
429
+ except Exception:
430
+ # Fallbacks: try enum-style .name or string conversion
431
+ err_name = getattr(self.error_code, "name", None) or str(self.error_code)
432
+ try:
433
+ err_code_int = int(self.error_code)
434
+ except Exception:
435
+ err_code_int = self.error_code # best effort
436
+ msg = f"XCP negative response: {err_name} (0x{err_code_int:02X})"
437
+ if svc:
438
+ msg += f" on service {svc}"
439
+ # Suppress noisy ERROR log if requested by caller context
440
+ if is_suppress_xcp_error_log():
441
+ self.logger.debug(
442
+ msg,
443
+ extra={
444
+ "event": "xcp_error_suppressed",
445
+ "service": svc,
446
+ "error_code": err_code_int,
447
+ "error_name": err_name,
448
+ },
449
+ )
450
+ else:
451
+ self.logger.error(
452
+ msg,
453
+ extra={
454
+ "event": "xcp_error",
455
+ "service": svc,
456
+ "error_code": err_code_int,
457
+ "error_name": err_name,
458
+ },
459
+ )
460
+ except Exception:
461
+ pass
369
462
  except XcpTimeoutError:
370
463
  is_connect = func.__name__ == "connect"
371
464
  self.logger.warning(f"XcpTimeoutError -- Service: {func.__name__!r}")
@@ -405,9 +498,21 @@ class Executor(SingletonBase):
405
498
  if handler.repeater.repeat():
406
499
  continue
407
500
  else:
408
- raise UnrecoverableError(
409
- f"Max. repetition count reached while trying to execute service {handler.func.__name__!r}."
410
- )
501
+ msg = f"Max. repetition count reached while trying to execute service {handler.func.__name__!r}."
502
+ # Try to append diagnostics from the transport
503
+ try:
504
+ if hasattr(handler, "_append_diag"):
505
+ msg = handler._append_diag(msg)
506
+ except Exception:
507
+ pass
508
+ try:
509
+ self.logger.error(
510
+ "XCP unrecoverable",
511
+ extra={"event": "xcp_unrecoverable", "service": getattr(inst.service, "name", None)},
512
+ )
513
+ except Exception:
514
+ pass
515
+ raise UnrecoverableError(msg)
411
516
  finally:
412
517
  # cleanup of class variables
413
518
  self.previous_error_code = None
pyxcp/master/master.py CHANGED
@@ -25,7 +25,13 @@ from pyxcp.constants import (
25
25
  makeWordUnpacker,
26
26
  )
27
27
  from pyxcp.daq_stim.stim import DaqEventInfo, Stim
28
- from pyxcp.master.errorhandler import SystemExit, disable_error_handling, wrapped
28
+ from pyxcp.master.errorhandler import (
29
+ SystemExit,
30
+ disable_error_handling,
31
+ is_suppress_xcp_error_log,
32
+ set_suppress_xcp_error_log,
33
+ wrapped,
34
+ )
29
35
  from pyxcp.transport.base import create_transport
30
36
  from pyxcp.utils import decode_bytes, delay, short_sleep
31
37
 
@@ -1972,6 +1978,9 @@ class Master:
1972
1978
  is normal for this kind of applications -- or to test for optional commands.
1973
1979
  Use carefuly not to hide serious error causes.
1974
1980
  """
1981
+ # Suppress logging of expected XCP negative responses during try_command
1982
+ _prev_suppress = is_suppress_xcp_error_log()
1983
+ set_suppress_xcp_error_log(True)
1975
1984
  try:
1976
1985
  extra_msg: Optional[str] = kws.get("extra_msg")
1977
1986
  if extra_msg:
@@ -1985,6 +1994,8 @@ class Master:
1985
1994
  silent = False
1986
1995
  res = cmd(*args, **kws)
1987
1996
  except SystemExit as e:
1997
+ # restore suppression flag before handling
1998
+ set_suppress_xcp_error_log(_prev_suppress)
1988
1999
  # print(f"\tUnexpected error while executing command {cmd.__name__!r}: {e!r}")
1989
2000
  if e.error_code == types.XcpError.ERR_CMD_UNKNOWN:
1990
2001
  # This is a rather common use-case, so let the user know that there is some functionality missing.
@@ -2000,6 +2011,12 @@ class Master:
2000
2011
  return (types.TryCommandResult.OTHER_ERROR, e)
2001
2012
  else:
2002
2013
  return (types.TryCommandResult.OK, res)
2014
+ finally:
2015
+ # Ensure suppression flag is restored even on success/other exceptions
2016
+ try:
2017
+ set_suppress_xcp_error_log(_prev_suppress)
2018
+ except Exception:
2019
+ pass
2003
2020
 
2004
2021
 
2005
2022
  def ticks_to_seconds(ticks, resolution):
@@ -16,12 +16,9 @@ else:
16
16
  HAS_PANDAS = True
17
17
 
18
18
  from pyxcp.recorder.rekorder import DaqOnlinePolicy # noqa: F401
19
- from pyxcp.recorder.rekorder import (
20
- DaqRecorderPolicy, # noqa: F401
21
- Deserializer, # noqa: F401
22
- MeasurementParameters, # noqa: F401
23
- ValueHolder, # noqa: F401
24
- )
19
+ from pyxcp.recorder.rekorder import DaqRecorderPolicy # noqa: F401
20
+ from pyxcp.recorder.rekorder import Deserializer # noqa: F401
21
+ from pyxcp.recorder.rekorder import MeasurementParameters # noqa: F401
25
22
  from pyxcp.recorder.rekorder import XcpLogFileDecoder as _XcpLogFileDecoder
26
23
  from pyxcp.recorder.rekorder import _PyXcpLogFileReader, _PyXcpLogFileWriter, data_types
27
24
 
Binary file
Binary file
Binary file
Binary file