pyxcp 0.23.3__cp311-cp311-win_arm64.whl → 0.23.6__cp311-cp311-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 +1 -1
- pyxcp/cmdline.py +1 -1
- pyxcp/config/__init__.py +74 -20
- pyxcp/cpp_ext/cpp_ext.cp310-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/cpp_ext.cp311-win_arm64.pyd +0 -0
- pyxcp/daq_stim/__init__.py +61 -2
- pyxcp/daq_stim/stim.cp310-win_arm64.pyd +0 -0
- pyxcp/daq_stim/stim.cp311-win_arm64.pyd +0 -0
- pyxcp/examples/run_daq.py +1 -1
- pyxcp/master/errorhandler.py +116 -11
- pyxcp/master/master.py +18 -1
- pyxcp/recorder/__init__.py +3 -6
- pyxcp/recorder/rekorder.cp310-win_arm64.pyd +0 -0
- pyxcp/recorder/rekorder.cp311-win_arm64.pyd +0 -0
- pyxcp/recorder/unfolder.hpp +67 -55
- pyxcp/recorder/wrap.cpp +0 -6
- pyxcp/transport/base.py +166 -12
- pyxcp/transport/can.py +59 -6
- pyxcp/transport/eth.py +23 -5
- pyxcp/utils.py +2 -2
- pyxcp-0.23.6.dist-info/METADATA +339 -0
- {pyxcp-0.23.3.dist-info → pyxcp-0.23.6.dist-info}/RECORD +25 -25
- pyxcp-0.23.3.dist-info/METADATA +0 -219
- {pyxcp-0.23.3.dist-info → pyxcp-0.23.6.dist-info}/LICENSE +0 -0
- {pyxcp-0.23.3.dist-info → pyxcp-0.23.6.dist-info}/WHEEL +0 -0
- {pyxcp-0.23.3.dist-info → pyxcp-0.23.6.dist-info}/entry_points.txt +0 -0
pyxcp/recorder/unfolder.hpp
CHANGED
|
@@ -1139,6 +1139,50 @@ class DaqRecorderPolicy : public DAQPolicyBase {
|
|
|
1139
1139
|
bool m_initialized{ false };
|
|
1140
1140
|
};
|
|
1141
1141
|
|
|
1142
|
+
class DaqTimeTracker {
|
|
1143
|
+
public:
|
|
1144
|
+
|
|
1145
|
+
DaqTimeTracker(std::uint64_t overflow_value) : m_overflow_value(overflow_value), m_overflow_counter(0ULL), m_previous_timestamp(0ULL) { m_ts_base_set=false; m_ts0_base=0ULL; m_ts1_base=0ULL; }
|
|
1146
|
+
|
|
1147
|
+
auto get_previous_timestamp() const noexcept {
|
|
1148
|
+
return m_previous_timestamp;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
void set_previous_timestamp(std::uint64_t timestamp) noexcept {
|
|
1152
|
+
m_previous_timestamp = timestamp;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
void inc_overflow_counter() noexcept {
|
|
1156
|
+
m_overflow_counter++;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
auto get_overflow_counter() const noexcept {
|
|
1160
|
+
return m_overflow_counter;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
auto get_value() const noexcept {
|
|
1164
|
+
return m_overflow_value * m_overflow_counter;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
public:
|
|
1168
|
+
std::pair<std::uint64_t,std::uint64_t> normalize(std::uint64_t ts0, std::uint64_t ts1) noexcept {
|
|
1169
|
+
if (!m_ts_base_set) {
|
|
1170
|
+
m_ts0_base = ts0;
|
|
1171
|
+
m_ts1_base = ts1;
|
|
1172
|
+
m_ts_base_set = true;
|
|
1173
|
+
}
|
|
1174
|
+
return {ts0 - m_ts0_base, ts1 - m_ts1_base};
|
|
1175
|
+
}
|
|
1176
|
+
private:
|
|
1177
|
+
std::uint64_t m_overflow_value{};
|
|
1178
|
+
std::uint64_t m_overflow_counter{};
|
|
1179
|
+
std::uint64_t m_previous_timestamp{};
|
|
1180
|
+
bool m_ts_base_set{false};
|
|
1181
|
+
std::uint64_t m_ts0_base{0ULL};
|
|
1182
|
+
std::uint64_t m_ts1_base{0ULL};
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
|
|
1142
1186
|
class DaqOnlinePolicy : public DAQPolicyBase {
|
|
1143
1187
|
public:
|
|
1144
1188
|
|
|
@@ -1149,6 +1193,9 @@ class DaqOnlinePolicy : public DAQPolicyBase {
|
|
|
1149
1193
|
|
|
1150
1194
|
void set_parameters(const MeasurementParameters& params) noexcept {
|
|
1151
1195
|
m_decoder = std::make_unique<DAQProcessor>(params);
|
|
1196
|
+
for (auto idx=0; idx < params.get_daq_lists().size(); ++idx) {
|
|
1197
|
+
m_overflows.emplace_back(DaqTimeTracker(params.get_overflow_value()));
|
|
1198
|
+
}
|
|
1152
1199
|
DAQPolicyBase::set_parameters(params);
|
|
1153
1200
|
}
|
|
1154
1201
|
|
|
@@ -1164,71 +1211,34 @@ class DaqOnlinePolicy : public DAQPolicyBase {
|
|
|
1164
1211
|
auto result = m_decoder->feed(timestamp, payload);
|
|
1165
1212
|
if (result) {
|
|
1166
1213
|
const auto& [daq_list, ts0, ts1, meas] = *result;
|
|
1167
|
-
on_daq_list(daq_list, ts0, ts1, meas);
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
1214
|
|
|
1171
|
-
|
|
1172
|
-
}
|
|
1215
|
+
auto& overflow = m_overflows[daq_list];
|
|
1173
1216
|
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1217
|
+
auto [norm_ts0, norm_ts1] = overflow.normalize(ts0, ts1);
|
|
1176
1218
|
|
|
1177
|
-
|
|
1219
|
+
if (overflow.get_previous_timestamp() > norm_ts1) {
|
|
1220
|
+
overflow.inc_overflow_counter();
|
|
1221
|
+
}
|
|
1178
1222
|
|
|
1179
|
-
|
|
1180
|
-
};
|
|
1223
|
+
on_daq_list(daq_list, norm_ts0, norm_ts1 + overflow.get_value(), meas);
|
|
1181
1224
|
|
|
1182
|
-
|
|
1183
|
-
ValueHolder() = delete;
|
|
1184
|
-
ValueHolder(const ValueHolder&) = default;
|
|
1225
|
+
overflow.set_previous_timestamp(norm_ts1);
|
|
1185
1226
|
|
|
1186
|
-
|
|
1227
|
+
}
|
|
1187
1228
|
}
|
|
1188
1229
|
|
|
1189
|
-
|
|
1230
|
+
virtual void initialize() {
|
|
1190
1231
|
}
|
|
1191
1232
|
|
|
1192
|
-
|
|
1193
|
-
return m_value;
|
|
1233
|
+
virtual void finalize() {
|
|
1194
1234
|
}
|
|
1195
1235
|
|
|
1196
1236
|
private:
|
|
1197
1237
|
|
|
1198
|
-
std::
|
|
1238
|
+
std::unique_ptr<DAQProcessor> m_decoder;
|
|
1239
|
+
std::vector<DaqTimeTracker> m_overflows;
|
|
1199
1240
|
};
|
|
1200
1241
|
|
|
1201
|
-
class Overflow {
|
|
1202
|
-
public:
|
|
1203
|
-
|
|
1204
|
-
Overflow(std::uint64_t overflow_value) : m_overflow_value(overflow_value), m_overflow_counter(0ULL), m_previous_timestamp(0ULL) {
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
auto get_previous_timestamp() const noexcept {
|
|
1208
|
-
return m_previous_timestamp;
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
void set_previous_timestamp(std::uint64_t timestamp) noexcept {
|
|
1212
|
-
m_previous_timestamp = timestamp;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
void inc_overflow_counter() noexcept {
|
|
1216
|
-
m_overflow_counter++;
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
auto get_overflow_counter() const noexcept {
|
|
1220
|
-
return m_overflow_counter;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
auto get_value() const noexcept {
|
|
1224
|
-
return m_overflow_value * m_overflow_counter;
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
private:
|
|
1228
|
-
std::uint64_t m_overflow_value{};
|
|
1229
|
-
std::uint64_t m_overflow_counter{};
|
|
1230
|
-
std::uint64_t m_previous_timestamp{};
|
|
1231
|
-
};
|
|
1232
1242
|
|
|
1233
1243
|
class XcpLogFileDecoder {
|
|
1234
1244
|
public:
|
|
@@ -1240,12 +1250,12 @@ class XcpLogFileDecoder {
|
|
|
1240
1250
|
m_params = des.run();
|
|
1241
1251
|
|
|
1242
1252
|
for (auto idx=0; idx < m_params.get_daq_lists().size(); ++idx) {
|
|
1243
|
-
m_overflows.emplace_back(
|
|
1253
|
+
m_overflows.emplace_back(DaqTimeTracker(m_params.get_overflow_value()));
|
|
1244
1254
|
}
|
|
1245
1255
|
|
|
1246
1256
|
m_decoder = std::make_unique<DAQProcessor>(m_params);
|
|
1247
1257
|
} else {
|
|
1248
|
-
|
|
1258
|
+
throw std::runtime_error("XcpLogFileDecoder: missing metadata.");
|
|
1249
1259
|
}
|
|
1250
1260
|
}
|
|
1251
1261
|
|
|
@@ -1289,15 +1299,17 @@ class XcpLogFileDecoder {
|
|
|
1289
1299
|
|
|
1290
1300
|
auto& overflow = m_overflows[daq_list];
|
|
1291
1301
|
|
|
1292
|
-
|
|
1302
|
+
auto [norm_ts0, norm_ts1] = overflow.normalize(ts0, ts1);
|
|
1303
|
+
|
|
1304
|
+
if (overflow.get_previous_timestamp() > norm_ts1) {
|
|
1293
1305
|
overflow.inc_overflow_counter();
|
|
1294
1306
|
// Maybe on debug-level?
|
|
1295
1307
|
// std::cout << "Overflow detected, counter: " << overflow.get_overflow_counter() << " " << overflow.get_previous_timestamp() << " " << ts1 << std::endl;
|
|
1296
1308
|
}
|
|
1297
1309
|
|
|
1298
|
-
on_daq_list(daq_list,
|
|
1310
|
+
on_daq_list(daq_list, norm_ts0, norm_ts1 + overflow.get_value(), meas);
|
|
1299
1311
|
|
|
1300
|
-
overflow.set_previous_timestamp(
|
|
1312
|
+
overflow.set_previous_timestamp(norm_ts1);
|
|
1301
1313
|
}
|
|
1302
1314
|
}
|
|
1303
1315
|
}
|
|
@@ -1326,7 +1338,7 @@ class XcpLogFileDecoder {
|
|
|
1326
1338
|
XcpLogFileReader m_reader;
|
|
1327
1339
|
std::unique_ptr<DAQProcessor> m_decoder;
|
|
1328
1340
|
MeasurementParameters m_params;
|
|
1329
|
-
std::vector<
|
|
1341
|
+
std::vector<DaqTimeTracker> m_overflows;
|
|
1330
1342
|
};
|
|
1331
1343
|
|
|
1332
1344
|
#endif // RECORDER_UNFOLDER_HPP
|
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()
|
|
@@ -222,6 +229,10 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
222
229
|
self.finish_listener()
|
|
223
230
|
self.listener.join()
|
|
224
231
|
|
|
232
|
+
# Ensure the close event is cleared before starting a new listener thread.
|
|
233
|
+
if hasattr(self, "closeEvent"):
|
|
234
|
+
self.closeEvent.clear()
|
|
235
|
+
|
|
225
236
|
self.listener = threading.Thread(target=self.listen)
|
|
226
237
|
self.listener.start()
|
|
227
238
|
|
|
@@ -235,6 +246,8 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
235
246
|
self.timing.start()
|
|
236
247
|
with self.policy_lock:
|
|
237
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)
|
|
238
251
|
self.send(frame)
|
|
239
252
|
try:
|
|
240
253
|
xcpPDU = self.get()
|
|
@@ -243,7 +256,10 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
243
256
|
MSG = f"Response timed out (timeout={self.timeout / 1_000_000_000}s)"
|
|
244
257
|
with self.policy_lock:
|
|
245
258
|
self.policy.feed(types.FrameCategory.METADATA, self.counter_send, self.timestamp.value, bytes(MSG, "ascii"))
|
|
246
|
-
|
|
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
|
|
247
263
|
else:
|
|
248
264
|
self.timing.stop()
|
|
249
265
|
return
|
|
@@ -299,16 +315,30 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
299
315
|
self.parent._setService(cmd)
|
|
300
316
|
|
|
301
317
|
cmd_len = cmd.bit_length() // 8 # calculate bytes needed for cmd
|
|
302
|
-
|
|
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))
|
|
303
329
|
|
|
304
330
|
header = self.HEADER.pack(len(packet), self.counter_send)
|
|
305
331
|
self.counter_send = (self.counter_send + 1) & 0xFFFF
|
|
306
332
|
|
|
307
333
|
frame = header + packet
|
|
308
334
|
|
|
309
|
-
|
|
310
|
-
if
|
|
311
|
-
frame
|
|
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)
|
|
312
342
|
|
|
313
343
|
if self._debug:
|
|
314
344
|
self.logger.debug(f"-> {hexDump(frame)}")
|
|
@@ -341,7 +371,14 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
341
371
|
block_response += partial_response[1:]
|
|
342
372
|
else:
|
|
343
373
|
if self.timestamp.value - start > self.timeout:
|
|
344
|
-
|
|
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
|
|
345
382
|
short_sleep()
|
|
346
383
|
return block_response
|
|
347
384
|
|
|
@@ -368,16 +405,29 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
368
405
|
self.timer_restart_event.set()
|
|
369
406
|
|
|
370
407
|
def process_response(self, response: bytes, length: int, counter: int, recv_timestamp: int) -> None:
|
|
371
|
-
|
|
372
|
-
self.logger.warning(f"Duplicate message counter {counter} received from the XCP slave")
|
|
373
|
-
if self._debug:
|
|
374
|
-
self.logger.debug(f"<- L{length} C{counter} {hexDump(response[:512])}")
|
|
375
|
-
return
|
|
376
|
-
self.counter_received = counter
|
|
408
|
+
# Important: determine PID first so duplicate counter handling can be applied selectively.
|
|
377
409
|
pid = response[0]
|
|
410
|
+
|
|
378
411
|
if pid >= 0xFC:
|
|
412
|
+
# Do not drop RESPONSE/EVENT/SERV frames even if the transport counter repeats.
|
|
413
|
+
# Some slaves may reuse the counter while DAQ traffic is active, and we must not lose
|
|
414
|
+
# command responses; otherwise request() can stall until timeout.
|
|
379
415
|
if self._debug:
|
|
380
416
|
self.logger.debug(f"<- L{length} C{counter} {hexDump(response)}")
|
|
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
|
+
)
|
|
381
431
|
if pid >= 0xFE:
|
|
382
432
|
self.resQueue.append(response)
|
|
383
433
|
with self.policy_lock:
|
|
@@ -391,6 +441,15 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
391
441
|
with self.policy_lock:
|
|
392
442
|
self.policy.feed(types.FrameCategory.SERV, self.counter_received, self.timestamp.value, response)
|
|
393
443
|
else:
|
|
444
|
+
# DAQ traffic: Some transports reuse or do not advance the counter for DAQ frames.
|
|
445
|
+
# Do not drop DAQ frames on duplicate counters to avoid losing measurements.
|
|
446
|
+
if counter == self.counter_received:
|
|
447
|
+
self.logger.debug(f"Duplicate message counter {counter} received (DAQ) - not dropping")
|
|
448
|
+
# DAQ still flowing – reset request timeout window to avoid false timeouts while
|
|
449
|
+
# the slave is busy but has not yet responded to a command.
|
|
450
|
+
self.timer_restart_event.set()
|
|
451
|
+
# Fall through and process the frame as usual.
|
|
452
|
+
self.counter_received = counter
|
|
394
453
|
if self._debug:
|
|
395
454
|
self.logger.debug(f"<- L{length} C{counter} ODT_Data[0:8] {hexDump(response[:8])}")
|
|
396
455
|
if self.first_daq_timestamp is None:
|
|
@@ -399,9 +458,104 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
399
458
|
timestamp = recv_timestamp
|
|
400
459
|
else:
|
|
401
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)
|
|
463
|
+
# DAQ activity indicates the slave is alive/busy; keep extending the wait window for any
|
|
464
|
+
# outstanding request, similar to EV_CMD_PENDING behavior on stacks that don't emit it.
|
|
465
|
+
self.timer_restart_event.set()
|
|
402
466
|
with self.policy_lock:
|
|
403
467
|
self.policy.feed(types.FrameCategory.DAQ, self.counter_received, timestamp, response)
|
|
404
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
|
+
|
|
405
559
|
# @abc.abstractproperty
|
|
406
560
|
# @property
|
|
407
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
463
|
-
|
|
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(
|
|
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
|
-
|
|
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
|