pyxcp 0.23.4__cp312-cp312-macosx_11_0_arm64.whl → 0.23.7__cp312-cp312-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.

Potentially problematic release.


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

pyxcp/recorder/wrap.cpp CHANGED
@@ -11,7 +11,6 @@
11
11
  namespace py = pybind11;
12
12
  using namespace pybind11::literals;
13
13
 
14
- PYBIND11_MAKE_OPAQUE(ValueHolder);
15
14
 
16
15
  class PyDaqOnlinePolicy : public DaqOnlinePolicy {
17
16
  public:
@@ -181,9 +180,4 @@ PYBIND11_MODULE(rekorder, m) {
181
180
  .def("get_header", &XcpLogFileDecoder::get_header)
182
181
  .def("initialize", &XcpLogFileDecoder::initialize)
183
182
  .def("finalize", &XcpLogFileDecoder::finalize);
184
-
185
- py::class_<ValueHolder>(m, "ValueHolder")
186
- //.def(py::init<const ValueHolder&>())
187
- .def(py::init<const std::any&>())
188
- .def_property_readonly("value", &ValueHolder::get_value);
189
183
  }
pyxcp/transport/base.py CHANGED
@@ -172,6 +172,13 @@ class BaseTransport(metaclass=abc.ABCMeta):
172
172
  self.pre_send_timestamp: int = self.timestamp.value
173
173
  self.post_send_timestamp: int = self.timestamp.value
174
174
  self.recv_timestamp: int = self.timestamp.value
175
+ # Ring buffer for last PDUs to aid diagnostics on failures
176
+ try:
177
+ from collections import deque as _dq
178
+
179
+ self._last_pdus = _dq(maxlen=200)
180
+ except Exception:
181
+ self._last_pdus = []
175
182
 
176
183
  def __del__(self) -> None:
177
184
  self.finish_listener()
@@ -239,6 +246,8 @@ class BaseTransport(metaclass=abc.ABCMeta):
239
246
  self.timing.start()
240
247
  with self.policy_lock:
241
248
  self.policy.feed(types.FrameCategory.CMD, self.counter_send, self.timestamp.value, frame)
249
+ # Record outgoing CMD for diagnostics
250
+ self._record_pdu("out", types.FrameCategory.CMD, self.counter_send, self.timestamp.value, frame)
242
251
  self.send(frame)
243
252
  try:
244
253
  xcpPDU = self.get()
@@ -247,7 +256,10 @@ class BaseTransport(metaclass=abc.ABCMeta):
247
256
  MSG = f"Response timed out (timeout={self.timeout / 1_000_000_000}s)"
248
257
  with self.policy_lock:
249
258
  self.policy.feed(types.FrameCategory.METADATA, self.counter_send, self.timestamp.value, bytes(MSG, "ascii"))
250
- raise types.XcpTimeoutError(MSG) from None
259
+ # Build diagnostics and include in exception
260
+ diag = self._build_diagnostics_dump() if self._diagnostics_enabled() else ""
261
+ self.logger.debug("XCP request timeout", extra={"event": "timeout", "command": cmd.name})
262
+ raise types.XcpTimeoutError(MSG + ("\n" + diag if diag else "")) from None
251
263
  else:
252
264
  self.timing.stop()
253
265
  return
@@ -303,16 +315,30 @@ class BaseTransport(metaclass=abc.ABCMeta):
303
315
  self.parent._setService(cmd)
304
316
 
305
317
  cmd_len = cmd.bit_length() // 8 # calculate bytes needed for cmd
306
- packet = bytes(flatten(cmd.to_bytes(cmd_len, "big"), data))
318
+ cmd_bytes = cmd.to_bytes(cmd_len, "big")
319
+ try:
320
+ from pyxcp.cpp_ext import accel as _accel
321
+ except Exception:
322
+ _accel = None # type: ignore
323
+
324
+ # Build payload (command + data) using optional accelerator
325
+ if _accel is not None and hasattr(_accel, "build_packet"):
326
+ packet = _accel.build_packet(cmd_bytes, data)
327
+ else:
328
+ packet = bytes(flatten(cmd_bytes, data))
307
329
 
308
330
  header = self.HEADER.pack(len(packet), self.counter_send)
309
331
  self.counter_send = (self.counter_send + 1) & 0xFFFF
310
332
 
311
333
  frame = header + packet
312
334
 
313
- remainder = len(frame) % self.alignment
314
- if remainder:
315
- frame += b"\0" * (self.alignment - remainder)
335
+ # Align using optional accelerator
336
+ if _accel is not None and hasattr(_accel, "add_alignment"):
337
+ frame = _accel.add_alignment(frame, self.alignment)
338
+ else:
339
+ remainder = len(frame) % self.alignment
340
+ if remainder:
341
+ frame += b"\0" * (self.alignment - remainder)
316
342
 
317
343
  if self._debug:
318
344
  self.logger.debug(f"-> {hexDump(frame)}")
@@ -345,7 +371,14 @@ class BaseTransport(metaclass=abc.ABCMeta):
345
371
  block_response += partial_response[1:]
346
372
  else:
347
373
  if self.timestamp.value - start > self.timeout:
348
- raise types.XcpTimeoutError("Response timed out [block_receive].") from None
374
+ waited = (self.timestamp.value - start) / 1e9 if hasattr(self.timestamp, "value") else None
375
+ msg = f"Response timed out [block_receive]: received {len(block_response)} of {length_required} bytes"
376
+ if waited is not None:
377
+ msg += f" after {waited:.3f}s"
378
+ # Attach diagnostics
379
+ diag = self._build_diagnostics_dump() if self._diagnostics_enabled() else ""
380
+ self.logger.debug("XCP block_receive timeout", extra={"event": "timeout"})
381
+ raise types.XcpTimeoutError(msg + ("\n" + diag if diag else "")) from None
349
382
  short_sleep()
350
383
  return block_response
351
384
 
@@ -382,6 +415,19 @@ class BaseTransport(metaclass=abc.ABCMeta):
382
415
  if self._debug:
383
416
  self.logger.debug(f"<- L{length} C{counter} {hexDump(response)}")
384
417
  self.counter_received = counter
418
+ # Record incoming non-DAQ frames for diagnostics
419
+ self._record_pdu(
420
+ "in",
421
+ (
422
+ types.FrameCategory.RESPONSE
423
+ if pid >= 0xFE
424
+ else types.FrameCategory.SERV if pid == 0xFC else types.FrameCategory.EVENT
425
+ ),
426
+ counter,
427
+ recv_timestamp,
428
+ response,
429
+ length,
430
+ )
385
431
  if pid >= 0xFE:
386
432
  self.resQueue.append(response)
387
433
  with self.policy_lock:
@@ -412,12 +458,104 @@ class BaseTransport(metaclass=abc.ABCMeta):
412
458
  timestamp = recv_timestamp
413
459
  else:
414
460
  timestamp = 0
461
+ # Record DAQ frame (only keep small prefix in payload string later)
462
+ self._record_pdu("in", types.FrameCategory.DAQ, counter, timestamp, response, length)
415
463
  # DAQ activity indicates the slave is alive/busy; keep extending the wait window for any
416
464
  # outstanding request, similar to EV_CMD_PENDING behavior on stacks that don't emit it.
417
465
  self.timer_restart_event.set()
418
466
  with self.policy_lock:
419
467
  self.policy.feed(types.FrameCategory.DAQ, self.counter_received, timestamp, response)
420
468
 
469
+ def _record_pdu(
470
+ self,
471
+ direction: str,
472
+ category: types.FrameCategory,
473
+ counter: int,
474
+ timestamp: int,
475
+ payload: bytes,
476
+ length: Optional[int] = None,
477
+ ) -> None:
478
+ try:
479
+ entry = {
480
+ "dir": direction,
481
+ "cat": category.name,
482
+ "ctr": int(counter),
483
+ "ts": int(timestamp),
484
+ "len": int(length if length is not None else len(payload)),
485
+ "data": hexDump(payload if category != types.FrameCategory.DAQ else payload[:8])[:512],
486
+ }
487
+ self._last_pdus.append(entry)
488
+ except Exception:
489
+ pass
490
+
491
+ def _build_diagnostics_dump(self) -> str:
492
+ import json as _json
493
+
494
+ # transport params
495
+ tp = {"transport": self.__class__.__name__}
496
+ cfg = getattr(self, "config", None)
497
+ # Extract common Eth/Can fields when available
498
+ for key in (
499
+ "host",
500
+ "port",
501
+ "protocol",
502
+ "ipv6",
503
+ "bind_to_address",
504
+ "bind_to_port",
505
+ "fd",
506
+ "bitrate",
507
+ "data_bitrate",
508
+ "can_id_master",
509
+ "can_id_slave",
510
+ ):
511
+ if cfg is not None and hasattr(cfg, key):
512
+ try:
513
+ tp[key] = getattr(cfg, key)
514
+ except Exception:
515
+ pass
516
+ # negotiated properties
517
+ negotiated = None
518
+ try:
519
+ master = getattr(self, "parent", None)
520
+ if master is not None and hasattr(master, "slaveProperties"):
521
+ sp = getattr(master, "slaveProperties")
522
+ negotiated = getattr(sp, "__dict__", None) or str(sp)
523
+ except Exception:
524
+ negotiated = None
525
+ # last PDUs
526
+ general = None
527
+ last_n = 20
528
+ try:
529
+ app = getattr(self.config, "parent", None)
530
+ app = getattr(app, "parent", None)
531
+ if app is not None and hasattr(app, "general") and hasattr(app.general, "diagnostics_last_pdus"):
532
+ last_n = int(app.general.diagnostics_last_pdus or last_n)
533
+ general = app.general
534
+ except Exception:
535
+ pass
536
+ pdus = list(self._last_pdus)[-last_n:]
537
+ payload = {
538
+ "transport_params": tp,
539
+ "last_pdus": pdus,
540
+ }
541
+ try:
542
+ body = _json.dumps(payload, ensure_ascii=False, default=str, indent=2)
543
+ except Exception:
544
+ body = str(payload)
545
+ # Add a small header to explain what follows
546
+ header = "--- Diagnostics (for troubleshooting) ---"
547
+ return f"{header}\n{body}"
548
+
549
+ def _diagnostics_enabled(self) -> bool:
550
+ try:
551
+ app = getattr(self.config, "parent", None)
552
+ app = getattr(app, "parent", None)
553
+ if app is not None and hasattr(app, "general"):
554
+ return bool(getattr(app.general, "diagnostics_on_failure", True))
555
+ except Exception:
556
+ return True
557
+ return True
558
+
421
559
  # @abc.abstractproperty
422
560
  # @property
423
561
  # def transport_layer_interface(self) -> Any:
pyxcp/transport/can.py CHANGED
@@ -292,7 +292,13 @@ class PythonCanWrapper:
292
292
  self.timeout: int = timeout
293
293
  self.parameters = parameters
294
294
  if not self.parent.has_user_supplied_interface:
295
- self.can_interface_class = _get_class_for_interface(self.interface_name)
295
+ try:
296
+ self.can_interface_class = _get_class_for_interface(self.interface_name)
297
+ except Exception as ex:
298
+ # Provide clearer message if interface not supported by python-can on this platform
299
+ raise CanInitializationError(
300
+ f"Unsupported or unavailable CAN interface {self.interface_name!r}: {ex.__class__.__name__}: {ex}"
301
+ ) from ex
296
302
  else:
297
303
  self.can_interface_class = None
298
304
  self.can_interface: BusABC
@@ -320,7 +326,15 @@ class PythonCanWrapper:
320
326
  self.can_interface.set_filters(merged_filters)
321
327
  self.software_filter.set_filters(can_filters) # Filter unwanted traffic.
322
328
  else:
323
- self.can_interface = self.can_interface_class(interface=self.interface_name, can_filters=can_filters, **self.parameters)
329
+ try:
330
+ self.can_interface = self.can_interface_class(
331
+ interface=self.interface_name, can_filters=can_filters, **self.parameters
332
+ )
333
+ except OSError as ex:
334
+ # Typical when selecting socketcan on unsupported OS (e.g., Windows)
335
+ raise CanInitializationError(
336
+ f"OS error while creating CAN interface {self.interface_name!r}: {ex.__class__.__name__}: {ex}"
337
+ ) from ex
324
338
  self.software_filter.accept_all()
325
339
  self.parent.logger.info(f"XCPonCAN - Using Interface: '{self.can_interface!s}'")
326
340
  self.parent.logger.info(f"XCPonCAN - Filters used: {self.can_interface.filters}")
@@ -401,13 +415,33 @@ class Can(BaseTransport):
401
415
  self.padding_value = self.config.padding_value
402
416
  if transport_layer_interface is None:
403
417
  self.interface_name = self.config.interface
404
- self.interface_configuration = detect_available_configs(interfaces=[self.interface_name])
418
+ # On platforms that do not support certain backends (e.g., SocketCAN on Windows),
419
+ # python-can may raise OSError deep inside interface initialization. We want to
420
+ # fail fast with a clearer hint and avoid unhandled low-level errors.
421
+ try:
422
+ self.interface_configuration = detect_available_configs(interfaces=[self.interface_name])
423
+ except Exception as ex:
424
+ # Best-effort graceful message; keep original exception context
425
+ self.logger.critical(
426
+ f"XCPonCAN - Failed to query available configs for interface {self.interface_name!r}: {ex.__class__.__name__}: {ex}"
427
+ )
428
+ self.interface_configuration = []
405
429
  parameters = self.get_interface_parameters()
406
430
  else:
407
431
  self.interface_name = "custom"
408
432
  # print("TRY GET PARAMs", self.get_interface_parameters())
409
433
  parameters = {}
410
- self.can_interface = PythonCanWrapper(self, self.interface_name, config.timeout, **parameters)
434
+ try:
435
+ self.can_interface = PythonCanWrapper(self, self.interface_name, config.timeout, **parameters)
436
+ except OSError as ex:
437
+ # Catch platform-specific socket errors early (e.g., SocketCAN on Windows)
438
+ msg = (
439
+ f"XCPonCAN - Failed to initialize CAN interface {self.interface_name!r}: "
440
+ f"{ex.__class__.__name__}: {ex}.\n"
441
+ f"Hint: Interface may be unsupported on this OS or missing drivers."
442
+ )
443
+ self.logger.critical(msg)
444
+ raise CanInitializationError(msg) from ex
411
445
  self.logger.info(f"XCPonCAN - Interface-Type: {self.interface_name!r} Parameters: {list(parameters.items())}")
412
446
  self.logger.info(
413
447
  f"XCPonCAN - Master-ID (Tx): 0x{self.can_id_master.id:08X}{self.can_id_master.type_str} -- "
@@ -459,15 +493,34 @@ class Can(BaseTransport):
459
493
  self.data_received(frame.data, frame.timestamp)
460
494
 
461
495
  def connect(self):
462
- if self.useDefaultListener:
463
- self.start_listener()
496
+ # Start listener lazily after a successful interface connection to avoid a dangling
497
+ # thread waiting on a not-yet-connected interface if initialization fails.
464
498
  try:
465
499
  self.can_interface.connect()
466
500
  except CanInitializationError:
501
+ # Ensure any previously-started listener is stopped to prevent hangs.
502
+ self.finish_listener()
467
503
  console.print("[red]\nThere may be a problem with the configuration of your CAN-interface.\n")
468
504
  console.print(f"[grey]Current configuration of interface {self.interface_name!r}:")
469
505
  console.print(self.interface_configuration)
470
506
  raise
507
+ except OSError as ex:
508
+ # Ensure any previously-started listener is stopped to prevent hangs.
509
+ self.finish_listener()
510
+ # E.g., attempting to instantiate SocketCAN on Windows raises an OSError from socket layer.
511
+ # Provide a clearer, actionable message and keep the original exception.
512
+ msg = (
513
+ f"XCPonCAN - OS error while initializing interface {self.interface_name!r}: "
514
+ f"{ex.__class__.__name__}: {ex}.\n"
515
+ f"Hint: This interface may not be supported on your platform. "
516
+ f"On Windows, use e.g. 'vector', 'kvaser', 'pcan', or other vendor backends instead of 'socketcan'."
517
+ )
518
+ self.logger.critical(msg)
519
+ raise CanInitializationError(msg) from ex
520
+ else:
521
+ # Only now start the default listener if requested.
522
+ if self.useDefaultListener:
523
+ self.start_listener()
471
524
  self.status = 1 # connected
472
525
 
473
526
  def send(self, frame: bytes) -> None:
pyxcp/transport/eth.py CHANGED
@@ -69,8 +69,8 @@ class Eth(BaseTransport):
69
69
  self.sockaddr,
70
70
  ) = addrinfo[0]
71
71
  except BaseException as ex: # noqa: B036
72
- msg = f"XCPonEth - Failed to resolve address {self.host}:{self.port}"
73
- self.logger.critical(msg)
72
+ msg = f"XCPonEth - Failed to resolve address {self.host}:{self.port} ({self.protocol}, ipv6={self.ipv6}): {ex.__class__.__name__}: {ex}"
73
+ self.logger.critical(msg, extra={"transport": "eth", "host": self.host, "port": self.port, "protocol": self.protocol})
74
74
  raise Exception(msg) from ex
75
75
  self.status: int = 0
76
76
  self.sock = socket.socket(self.address_family, self.socktype, self.proto)
@@ -87,8 +87,10 @@ class Eth(BaseTransport):
87
87
  try:
88
88
  self.sock.bind(self._local_address)
89
89
  except BaseException as ex: # noqa: B036
90
- msg = f"XCPonEth - Failed to bind socket to given address {self._local_address}"
91
- self.logger.critical(msg)
90
+ msg = f"XCPonEth - Failed to bind socket to given address {self._local_address}: {ex.__class__.__name__}: {ex}"
91
+ self.logger.critical(
92
+ msg, extra={"transport": "eth", "host": self.host, "port": self.port, "protocol": self.protocol}
93
+ )
92
94
  raise Exception(msg) from ex
93
95
  self._packet_listener = threading.Thread(
94
96
  target=self._packet_listen,
@@ -194,7 +196,23 @@ class Eth(BaseTransport):
194
196
  else:
195
197
  if current_size >= length:
196
198
  response = data[current_position : current_position + length]
197
- process_response(response, length, counter, timestamp)
199
+ try:
200
+ process_response(response, length, counter, timestamp)
201
+ except BaseException as ex: # Guard listener against unhandled exceptions (e.g., disk full in policy)
202
+ try:
203
+ self.logger.critical(
204
+ f"Listener error in process_response: {ex.__class__.__name__}: {ex}. Stopping listener.",
205
+ extra={"event": "listener_error"},
206
+ )
207
+ except Exception:
208
+ pass
209
+ try:
210
+ # Signal all loops to stop
211
+ if hasattr(self, "closeEvent"):
212
+ self.closeEvent.set()
213
+ except Exception:
214
+ pass
215
+ return
198
216
  current_size -= length
199
217
  current_position += length
200
218
  length = None
pyxcp/utils.py CHANGED
@@ -113,8 +113,8 @@ def enum_from_str(enum_class: IntEnum, enumerator: str) -> IntEnum:
113
113
 
114
114
  enumerator: str
115
115
 
116
- Example
117
- -------
116
+ Examples
117
+ --------
118
118
 
119
119
  class Color(enum.IntEnum):
120
120
  RED = 0