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.
- pyxcp/__init__.py +1 -1
- pyxcp/cmdline.py +14 -29
- pyxcp/config/__init__.py +1257 -1258
- pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
- pyxcp/cpp_ext/bin.hpp +7 -6
- pyxcp/cpp_ext/cpp_ext.cpython-310-darwin.so +0 -0
- pyxcp/cpp_ext/cpp_ext.cpython-311-darwin.so +0 -0
- pyxcp/cpp_ext/cpp_ext.cpython-312-darwin.so +0 -0
- pyxcp/cpp_ext/cpp_ext.cpython-313-darwin.so +0 -0
- pyxcp/cpp_ext/daqlist.hpp +241 -73
- pyxcp/cpp_ext/extension_wrapper.cpp +123 -15
- pyxcp/cpp_ext/framing.hpp +360 -0
- pyxcp/cpp_ext/helper.hpp +280 -280
- pyxcp/cpp_ext/mcobject.hpp +248 -246
- pyxcp/cpp_ext/sxi_framing.hpp +332 -0
- pyxcp/daq_stim/__init__.py +145 -67
- pyxcp/daq_stim/optimize/binpacking.py +2 -2
- pyxcp/daq_stim/scheduler.cpp +8 -8
- pyxcp/errormatrix.py +2 -2
- pyxcp/examples/run_daq.py +5 -4
- pyxcp/examples/xcp_policy.py +6 -6
- pyxcp/examples/xcp_read_benchmark.py +2 -2
- pyxcp/examples/xcp_skel.py +1 -2
- pyxcp/examples/xcp_unlock.py +10 -12
- pyxcp/examples/xcp_user_supplied_driver.py +1 -2
- pyxcp/examples/xcphello.py +2 -15
- pyxcp/examples/xcphello_recorder.py +2 -2
- pyxcp/master/__init__.py +1 -0
- pyxcp/master/errorhandler.py +134 -4
- pyxcp/master/master.py +823 -252
- pyxcp/recorder/.idea/.gitignore +8 -0
- pyxcp/recorder/.idea/misc.xml +4 -0
- pyxcp/recorder/.idea/modules.xml +8 -0
- pyxcp/recorder/.idea/recorder.iml +6 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +7 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/index.pb +7 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/index.pb +7 -0
- pyxcp/recorder/.idea/vcs.xml +10 -0
- pyxcp/recorder/__init__.py +96 -98
- pyxcp/recorder/converter/__init__.py +4 -10
- pyxcp/recorder/reader.hpp +138 -139
- pyxcp/recorder/reco.py +1 -0
- pyxcp/recorder/rekorder.cpython-310-darwin.so +0 -0
- pyxcp/recorder/rekorder.cpython-311-darwin.so +0 -0
- pyxcp/recorder/rekorder.cpython-312-darwin.so +0 -0
- pyxcp/recorder/rekorder.cpython-313-darwin.so +0 -0
- pyxcp/recorder/rekorder.hpp +274 -274
- pyxcp/recorder/unfolder.hpp +1354 -1319
- pyxcp/recorder/wrap.cpp +184 -183
- pyxcp/recorder/writer.hpp +302 -302
- pyxcp/scripts/xcp_daq_recorder.py +54 -0
- pyxcp/scripts/xcp_fetch_a2l.py +2 -2
- pyxcp/scripts/xcp_id_scanner.py +1 -2
- pyxcp/scripts/xcp_info.py +66 -51
- pyxcp/scripts/xcp_profile.py +1 -2
- pyxcp/tests/test_daq.py +1 -1
- pyxcp/tests/test_framing.py +262 -0
- pyxcp/tests/test_master.py +210 -100
- pyxcp/tests/test_transport.py +138 -42
- pyxcp/timing.py +1 -1
- pyxcp/transport/__init__.py +8 -5
- pyxcp/transport/base.py +70 -180
- pyxcp/transport/can.py +58 -7
- pyxcp/transport/eth.py +32 -15
- pyxcp/transport/hdf5_policy.py +167 -0
- pyxcp/transport/sxi.py +126 -52
- pyxcp/transport/transport_ext.cpython-310-darwin.so +0 -0
- pyxcp/transport/transport_ext.cpython-311-darwin.so +0 -0
- pyxcp/transport/transport_ext.cpython-312-darwin.so +0 -0
- pyxcp/transport/transport_ext.cpython-313-darwin.so +0 -0
- pyxcp/transport/transport_ext.hpp +214 -0
- pyxcp/transport/transport_wrapper.cpp +249 -0
- pyxcp/transport/usb_transport.py +47 -31
- pyxcp/types.py +0 -13
- pyxcp/{utils.py → utils/__init__.py} +1 -2
- pyxcp/utils/cli.py +78 -0
- {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info}/METADATA +4 -2
- pyxcp-0.25.7.dist-info/RECORD +158 -0
- {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info}/WHEEL +1 -1
- pyxcp/examples/conf_sxi.json +0 -9
- pyxcp/examples/conf_sxi.toml +0 -7
- pyxcp-0.23.8.dist-info/RECORD +0 -135
- {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info}/entry_points.txt +0 -0
- {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#if !defined (__SXI_FRAMING_HPP)
|
|
2
|
+
#define __SXI_FRAMING_HPP
|
|
3
|
+
|
|
4
|
+
#include <array>
|
|
5
|
+
#include <bit>
|
|
6
|
+
#include <chrono>
|
|
7
|
+
#include <condition_variable>
|
|
8
|
+
#include <cstdint>
|
|
9
|
+
#include <functional>
|
|
10
|
+
#include <memory>
|
|
11
|
+
#include <mutex>
|
|
12
|
+
#include <thread>
|
|
13
|
+
#include <vector>
|
|
14
|
+
#include <iomanip>
|
|
15
|
+
#include <iostream>
|
|
16
|
+
|
|
17
|
+
// Header format options
|
|
18
|
+
enum class SxiHeaderFormat {
|
|
19
|
+
LenByte,
|
|
20
|
+
LenCtrByte,
|
|
21
|
+
LenFillByte,
|
|
22
|
+
LenWord,
|
|
23
|
+
LenCtrWord,
|
|
24
|
+
LenFillWord,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Checksum type options
|
|
28
|
+
enum class SxiChecksumType {
|
|
29
|
+
None,
|
|
30
|
+
Sum8,
|
|
31
|
+
Sum16
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
namespace detail {
|
|
35
|
+
inline uint16_t make_word_le(const uint8_t* p) {
|
|
36
|
+
return p[0] | p[1] << 8;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
inline void put_word_le(uint8_t* p, uint16_t v) {
|
|
40
|
+
if constexpr (std::endian::native == std::endian::big) {
|
|
41
|
+
p[0] = static_cast<uint8_t>(v & 0xFF);
|
|
42
|
+
p[1] = static_cast<uint8_t>((v >> 8) & 0xFF);
|
|
43
|
+
} else {
|
|
44
|
+
std::memcpy(p, &v, sizeof(v));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} // namespace detail
|
|
48
|
+
|
|
49
|
+
class RestartableTimer {
|
|
50
|
+
public:
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @brief Constructs the timer.
|
|
54
|
+
* @param timeout The duration after which the timer expires.
|
|
55
|
+
* @param on_timeout The function to call upon timeout.
|
|
56
|
+
*/
|
|
57
|
+
RestartableTimer(std::chrono::milliseconds timeout, std::function<void()> on_timeout) :
|
|
58
|
+
m_timeout(timeout), m_on_timeout(std::move(on_timeout)), m_running(false) {
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
~RestartableTimer() {
|
|
62
|
+
stop();
|
|
63
|
+
if (m_thread.joinable()) {
|
|
64
|
+
m_thread.join();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Disable copy and move semantics
|
|
69
|
+
RestartableTimer(const RestartableTimer&) = delete;
|
|
70
|
+
RestartableTimer& operator=(const RestartableTimer&) = delete;
|
|
71
|
+
RestartableTimer(RestartableTimer&&) = delete;
|
|
72
|
+
RestartableTimer& operator=(RestartableTimer&&) = delete;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @brief Starts the timer. If already running, it resets the countdown.
|
|
76
|
+
*/
|
|
77
|
+
void start() {
|
|
78
|
+
if (m_timeout == std::chrono::milliseconds(0)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
std::unique_lock<std::mutex> lock(m_mutex);
|
|
82
|
+
if (!m_running) {
|
|
83
|
+
m_running = true;
|
|
84
|
+
if (m_thread.joinable()) {
|
|
85
|
+
m_thread.join(); // Ensure previous thread is finished
|
|
86
|
+
}
|
|
87
|
+
m_thread = std::thread(&RestartableTimer::run, this);
|
|
88
|
+
} else {
|
|
89
|
+
// Already running, just signal a reset
|
|
90
|
+
m_cv.notify_one();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @brief Stops the timer.
|
|
96
|
+
*/
|
|
97
|
+
void stop() {
|
|
98
|
+
if (m_timeout == std::chrono::milliseconds(0)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
std::unique_lock<std::mutex> lock(m_mutex);
|
|
102
|
+
if (!m_running) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
m_running = false;
|
|
106
|
+
m_cv.notify_one();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @brief Resets the timer's countdown.
|
|
111
|
+
*/
|
|
112
|
+
void reset_timeout() {
|
|
113
|
+
if (m_timeout == std::chrono::milliseconds(0)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
std::unique_lock<std::mutex> lock(m_mutex);
|
|
117
|
+
if (m_running) {
|
|
118
|
+
m_cv.notify_one();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private:
|
|
123
|
+
|
|
124
|
+
void run() {
|
|
125
|
+
std::unique_lock<std::mutex> lock(m_mutex);
|
|
126
|
+
while (m_running) {
|
|
127
|
+
// wait_for returns cv_status::timeout if the time expires without a notification
|
|
128
|
+
if (m_cv.wait_for(lock, m_timeout) == std::cv_status::timeout) {
|
|
129
|
+
// Timeout occurred. Check m_running again in case stop() was called
|
|
130
|
+
// while we were waiting for the lock.
|
|
131
|
+
if (m_running) {
|
|
132
|
+
m_on_timeout();
|
|
133
|
+
m_running = false; // Stop the timer thread after firing
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
std::thread m_thread;
|
|
140
|
+
std::mutex m_mutex;
|
|
141
|
+
std::condition_variable m_cv;
|
|
142
|
+
std::chrono::milliseconds m_timeout;
|
|
143
|
+
std::function<void()> m_on_timeout;
|
|
144
|
+
std::atomic<bool> m_running;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
template<SxiHeaderFormat Format, SxiChecksumType Checksum>
|
|
149
|
+
class SxiReceiver {
|
|
150
|
+
public:
|
|
151
|
+
|
|
152
|
+
explicit SxiReceiver(
|
|
153
|
+
std::function<void(const std::vector<uint8_t>&, uint16_t, uint16_t)> dispatch_handler,
|
|
154
|
+
std::chrono::milliseconds /*timeout*/ = std::chrono::milliseconds(0)
|
|
155
|
+
) :
|
|
156
|
+
dispatch_(std::move(dispatch_handler)) {
|
|
157
|
+
reset();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
void feed_bytes(const std::string& data) {
|
|
161
|
+
for (const auto& c : data) {
|
|
162
|
+
//feed(static_cast<uint8_t>(c));
|
|
163
|
+
feed(c);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
void feed(uint8_t octet) {
|
|
168
|
+
if (index_ >= buffer_.size()) {
|
|
169
|
+
reset();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
buffer_[index_] = octet;
|
|
173
|
+
if (state_ == State::Idle) {
|
|
174
|
+
state_ = State::UntilLength;
|
|
175
|
+
fill_ = 0;
|
|
176
|
+
}
|
|
177
|
+
if (state_ == State::UntilLength) {
|
|
178
|
+
bool header_complete = false;
|
|
179
|
+
if constexpr (Format == SxiHeaderFormat::LenByte) {
|
|
180
|
+
if (index_ == 0) {
|
|
181
|
+
dlc_ = buffer_[0];
|
|
182
|
+
remaining_ = dlc_;
|
|
183
|
+
header_complete = true;
|
|
184
|
+
}
|
|
185
|
+
} else if constexpr (Format == SxiHeaderFormat::LenCtrByte || Format == SxiHeaderFormat::LenFillByte) {
|
|
186
|
+
if (index_ == 1) {
|
|
187
|
+
dlc_ = buffer_[0];
|
|
188
|
+
if constexpr (Format == SxiHeaderFormat::LenCtrByte) {
|
|
189
|
+
ctr_ = buffer_[1];
|
|
190
|
+
}
|
|
191
|
+
remaining_ = dlc_;
|
|
192
|
+
header_complete = true;
|
|
193
|
+
}
|
|
194
|
+
} else if constexpr (Format == SxiHeaderFormat::LenWord) {
|
|
195
|
+
if (index_ == 1) {
|
|
196
|
+
dlc_ = detail::make_word_le(&buffer_[0]);
|
|
197
|
+
remaining_ = dlc_;
|
|
198
|
+
header_complete = true;
|
|
199
|
+
}
|
|
200
|
+
} else if constexpr (Format == SxiHeaderFormat::LenCtrWord || Format == SxiHeaderFormat::LenFillWord) {
|
|
201
|
+
if (index_ == 3) {
|
|
202
|
+
dlc_ = detail::make_word_le(&buffer_[0]);
|
|
203
|
+
if constexpr (Format == SxiHeaderFormat::LenCtrWord) {
|
|
204
|
+
ctr_ = detail::make_word_le(&buffer_[2]);
|
|
205
|
+
}
|
|
206
|
+
remaining_ = dlc_;
|
|
207
|
+
header_complete = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (header_complete) {
|
|
211
|
+
if constexpr (Checksum == SxiChecksumType::Sum8) {
|
|
212
|
+
remaining_ += 1;
|
|
213
|
+
} else if constexpr (Checksum == SxiChecksumType::Sum16) {
|
|
214
|
+
uint16_t header_size = 0;
|
|
215
|
+
if constexpr (Format == SxiHeaderFormat::LenByte) header_size = 1;
|
|
216
|
+
else if constexpr (Format == SxiHeaderFormat::LenCtrByte || Format == SxiHeaderFormat::LenFillByte) header_size = 2;
|
|
217
|
+
else if constexpr (Format == SxiHeaderFormat::LenWord) header_size = 2;
|
|
218
|
+
else if constexpr (Format == SxiHeaderFormat::LenCtrWord || Format == SxiHeaderFormat::LenFillWord) header_size = 4;
|
|
219
|
+
|
|
220
|
+
fill_ = ((header_size + dlc_) % 2 != 0) ? 1u : 0u;
|
|
221
|
+
remaining_ += (2 + fill_);
|
|
222
|
+
}
|
|
223
|
+
state_ = State::Remaining;
|
|
224
|
+
if (remaining_ != 0) {
|
|
225
|
+
index_++;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (state_ == State::Remaining) {
|
|
231
|
+
if (remaining_ > 0) {
|
|
232
|
+
remaining_--;
|
|
233
|
+
}
|
|
234
|
+
if (remaining_ == 0) {
|
|
235
|
+
uint16_t payload_off = 0;
|
|
236
|
+
if constexpr (Format == SxiHeaderFormat::LenByte) {
|
|
237
|
+
payload_off = 1;
|
|
238
|
+
} else if constexpr (Format == SxiHeaderFormat::LenCtrByte || Format == SxiHeaderFormat::LenFillByte) {
|
|
239
|
+
payload_off = 2;
|
|
240
|
+
} else if constexpr (Format == SxiHeaderFormat::LenWord) {
|
|
241
|
+
payload_off = 2;
|
|
242
|
+
} else if constexpr (Format == SxiHeaderFormat::LenCtrWord || Format == SxiHeaderFormat::LenFillWord) {
|
|
243
|
+
payload_off = 4;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// verify checksum
|
|
247
|
+
if constexpr (Checksum == SxiChecksumType::Sum8) {
|
|
248
|
+
uint8_t sum = 0;
|
|
249
|
+
for (uint16_t i = 0; i < (payload_off + dlc_ + fill_); ++i) {
|
|
250
|
+
sum += buffer_[i];
|
|
251
|
+
}
|
|
252
|
+
uint8_t rx = buffer_[payload_off + dlc_];
|
|
253
|
+
if (sum != rx) {
|
|
254
|
+
log_checksum_error(sum, rx, payload_off + dlc_ + 1);
|
|
255
|
+
reset();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
} else if constexpr (Checksum == SxiChecksumType::Sum16) {
|
|
259
|
+
uint16_t count = (payload_off + dlc_ + fill_);
|
|
260
|
+
uint16_t sum = 0;
|
|
261
|
+
|
|
262
|
+
for (uint16_t idx = 0; idx < count; idx += 2) {
|
|
263
|
+
sum = static_cast<uint16_t>(sum + detail::make_word_le(&buffer_[idx]));
|
|
264
|
+
}
|
|
265
|
+
uint16_t rx = detail::make_word_le(&buffer_[payload_off + dlc_ + fill_]);
|
|
266
|
+
if (sum != rx) {
|
|
267
|
+
log_checksum_error(sum, rx, payload_off + dlc_ + fill_ + 2);
|
|
268
|
+
reset();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (dispatch_) {
|
|
273
|
+
dispatch_({ buffer_.data() + payload_off, buffer_.data() + payload_off + dlc_ }, dlc_, ctr_);
|
|
274
|
+
#if defined(XCP_TL_TEST_HOOKS)
|
|
275
|
+
std::fill(buffer_.begin(), buffer_.end(), 0xcc);
|
|
276
|
+
#endif
|
|
277
|
+
}
|
|
278
|
+
reset();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
index_++;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private:
|
|
286
|
+
|
|
287
|
+
enum class State {
|
|
288
|
+
Idle,
|
|
289
|
+
UntilLength,
|
|
290
|
+
Remaining
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
template<typename T>
|
|
294
|
+
void log_checksum_error(T calculated, T received, uint16_t packet_len) {
|
|
295
|
+
std::cerr << "SXI checksum error: Calculated " << std::hex << "0x" << static_cast<int>(calculated)
|
|
296
|
+
<< ", but received " << "0x" << static_cast<int>(received) << "." << std::dec << std::endl;
|
|
297
|
+
std::cerr << "Packet dump (" << packet_len << " bytes):" << std::endl;
|
|
298
|
+
std::cerr << "[";
|
|
299
|
+
std::ios_base::fmtflags flags(std::cerr.flags()); // save flags
|
|
300
|
+
for (uint16_t i = 0; i < packet_len; ++i) {
|
|
301
|
+
std::cerr << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(buffer_[i]) << " ";
|
|
302
|
+
if ((i + 1) % 16 == 0) {
|
|
303
|
+
std::cerr << std::endl;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
std::cerr << "]" << std::endl;
|
|
307
|
+
std::cerr.flags(flags); // restore flags
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
void reset() {
|
|
311
|
+
state_ = State::Idle;
|
|
312
|
+
index_ = 0;
|
|
313
|
+
dlc_ = 0;
|
|
314
|
+
remaining_ = 0;
|
|
315
|
+
ctr_ = 0;
|
|
316
|
+
fill_ = 0;
|
|
317
|
+
#if defined(XCP_TL_TEST_HOOKS)
|
|
318
|
+
std::fill(buffer_.begin(), buffer_.end(), 0xcc);
|
|
319
|
+
#endif
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
std::array<uint8_t, 1024> buffer_{};
|
|
323
|
+
State state_{ State::Idle };
|
|
324
|
+
uint32_t index_{ 0 };
|
|
325
|
+
uint16_t dlc_{ 0 };
|
|
326
|
+
uint16_t ctr_{ 0 };
|
|
327
|
+
uint32_t remaining_{ 0 };
|
|
328
|
+
uint16_t fill_ {0};
|
|
329
|
+
std::function<void(const std::vector<uint8_t>&, uint16_t, uint16_t)> dispatch_;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
#endif // __SXI_FRAMING_HPP
|
pyxcp/daq_stim/__init__.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
import json
|
|
4
5
|
from time import time_ns
|
|
5
|
-
from typing import Dict, List, Optional, TextIO
|
|
6
|
+
from typing import Any, Dict, List, Optional, TextIO, Tuple, Union
|
|
7
|
+
|
|
8
|
+
from pyxcp.cpp_ext.cpp_ext import DaqList, PredefinedDaqList
|
|
6
9
|
|
|
7
10
|
from pyxcp import types
|
|
8
11
|
from pyxcp.config import get_application
|
|
9
|
-
from pyxcp.cpp_ext.cpp_ext import DaqList
|
|
10
12
|
from pyxcp.daq_stim.optimize import make_continuous_blocks
|
|
11
13
|
from pyxcp.daq_stim.optimize.binpacking import first_fit_decreasing
|
|
12
14
|
from pyxcp.recorder import DaqOnlinePolicy as _DaqOnlinePolicy
|
|
@@ -14,7 +16,6 @@ from pyxcp.recorder import DaqRecorderPolicy as _DaqRecorderPolicy
|
|
|
14
16
|
from pyxcp.recorder import MeasurementParameters
|
|
15
17
|
from pyxcp.utils import CurrentDatetime
|
|
16
18
|
|
|
17
|
-
|
|
18
19
|
DAQ_ID_FIELD_SIZE = {
|
|
19
20
|
"IDF_ABS_ODT_NUMBER": 1,
|
|
20
21
|
"IDF_REL_ODT_NUMBER_ABS_DAQ_LIST_NUMBER_BYTE": 2,
|
|
@@ -29,9 +30,87 @@ DAQ_TIMESTAMP_SIZE = {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
def load_daq_lists_from_json(file_path: str) -> List[DaqList]:
|
|
34
|
+
"""Load and validate DAQ-list from JSON file."""
|
|
35
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
36
|
+
config = json.load(f)
|
|
37
|
+
|
|
38
|
+
if not isinstance(config, list):
|
|
39
|
+
raise ValueError("DAQ configuration must be a JSON array (list)")
|
|
40
|
+
|
|
41
|
+
daq_lists: List[DaqList] = []
|
|
42
|
+
for idx, entry in enumerate(config):
|
|
43
|
+
if not isinstance(entry, dict):
|
|
44
|
+
raise TypeError(f"Entry {idx} must be an object/dict")
|
|
45
|
+
|
|
46
|
+
required = {"name", "event_num", "stim", "enable_timestamps", "measurements", "priority", "prescaler"}
|
|
47
|
+
missing = required - set(entry.keys())
|
|
48
|
+
if missing:
|
|
49
|
+
raise ValueError(f"Entry {idx} missing required keys: {missing}")
|
|
50
|
+
|
|
51
|
+
# Basic type conversions / checks
|
|
52
|
+
name = entry["name"]
|
|
53
|
+
if not isinstance(name, str):
|
|
54
|
+
raise TypeError(f"Entry {idx} 'name' must be a string")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
event_num = int(entry["event_num"])
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise TypeError(f"Entry {idx} 'event_num' must be an integer") from e
|
|
60
|
+
|
|
61
|
+
stim = bool(entry["stim"])
|
|
62
|
+
enable_timestamps = bool(entry["enable_timestamps"])
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
priority = int(entry["priority"])
|
|
66
|
+
prescaler = int(entry["prescaler"])
|
|
67
|
+
except Exception as e:
|
|
68
|
+
raise TypeError(f"Entry {idx} 'priority' and 'prescaler' must be integers") from e
|
|
69
|
+
|
|
70
|
+
measurements_raw = entry["measurements"]
|
|
71
|
+
if not isinstance(measurements_raw, list):
|
|
72
|
+
raise TypeError(f"Entry {idx} 'measurements' must be a list")
|
|
73
|
+
|
|
74
|
+
measurements: List[Tuple[str, int, int, str]] = []
|
|
75
|
+
for m_idx, m in enumerate(measurements_raw):
|
|
76
|
+
if not (isinstance(m, (list, tuple)) and len(m) == 4):
|
|
77
|
+
raise ValueError(f"Entry {idx} measurement {m_idx} must be a 4-element list/tuple")
|
|
78
|
+
m_name, m_addr, m_offset, m_type = m
|
|
79
|
+
|
|
80
|
+
if not isinstance(m_name, str):
|
|
81
|
+
raise TypeError(f"Entry {idx} measurement {m_idx} name must be a string")
|
|
82
|
+
try:
|
|
83
|
+
m_addr = int(m_addr)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
raise TypeError(f"Entry {idx} measurement {m_idx} address must be an integer") from e
|
|
86
|
+
try:
|
|
87
|
+
m_offset = int(m_offset)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
raise TypeError(f"Entry {idx} measurement {m_idx} offset must be an integer") from e
|
|
90
|
+
if not isinstance(m_type, str):
|
|
91
|
+
raise TypeError(f"Entry {idx} measurement {m_idx} type must be a string")
|
|
92
|
+
|
|
93
|
+
measurements.append((m_name, m_addr, m_offset, m_type))
|
|
94
|
+
|
|
95
|
+
daq_kwargs: Dict[str, Any] = {
|
|
96
|
+
"name": name,
|
|
97
|
+
"event_num": event_num,
|
|
98
|
+
"stim": stim,
|
|
99
|
+
"enable_timestamps": enable_timestamps,
|
|
100
|
+
"measurements": measurements,
|
|
101
|
+
"priority": priority,
|
|
102
|
+
"prescaler": prescaler,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
daq_lists.append(DaqList(**daq_kwargs))
|
|
106
|
+
|
|
107
|
+
return daq_lists
|
|
108
|
+
|
|
109
|
+
|
|
32
110
|
class DaqProcessor:
|
|
33
|
-
def __init__(self, daq_lists: List[DaqList]):
|
|
111
|
+
def __init__(self, daq_lists: List[Union[DaqList, PredefinedDaqList]]):
|
|
34
112
|
self.daq_lists = daq_lists
|
|
113
|
+
self.is_predefined = [isinstance(d, PredefinedDaqList) for d in daq_lists]
|
|
35
114
|
self.log = get_application().log
|
|
36
115
|
# Flag indicating a fatal OS-level error occurred during DAQ (e.g., disk full, out-of-memory)
|
|
37
116
|
self._fatal_os_error: bool = False
|
|
@@ -43,13 +122,14 @@ class DaqProcessor:
|
|
|
43
122
|
if start_datetime is None:
|
|
44
123
|
start_datetime = CurrentDatetime(time_ns())
|
|
45
124
|
self.start_datetime = start_datetime
|
|
46
|
-
# print(self.start_datetime)
|
|
47
125
|
try:
|
|
48
126
|
processor = self.daq_info.get("processor")
|
|
49
127
|
properties = processor.get("properties")
|
|
50
128
|
resolution = self.daq_info.get("resolution")
|
|
51
|
-
if properties["configType"] == "STATIC":
|
|
52
|
-
raise TypeError(
|
|
129
|
+
if properties["configType"] == "STATIC" and not all(self.is_predefined):
|
|
130
|
+
raise TypeError(
|
|
131
|
+
"DAQ configuration is static, but in your configuration are only dynamic DAQ lists -- cannot proceed."
|
|
132
|
+
)
|
|
53
133
|
self.supports_timestampes = properties["timestampSupported"]
|
|
54
134
|
self.supports_prescaler = properties["prescalerSupported"]
|
|
55
135
|
self.supports_pid_off = properties["pidOffSupported"]
|
|
@@ -72,22 +152,24 @@ class DaqProcessor:
|
|
|
72
152
|
max_payload_size = min(max_odt_entry_size, max_dto - header_len)
|
|
73
153
|
# First ODT may contain timestamp.
|
|
74
154
|
self.selectable_timestamps = False
|
|
155
|
+
max_payload_size_first = max_payload_size
|
|
75
156
|
if not self.supports_timestampes:
|
|
76
|
-
|
|
77
|
-
# print("NO TIMESTAMP SUPPORT")
|
|
157
|
+
self.log.info("No timestamp support")
|
|
78
158
|
else:
|
|
79
159
|
if self.ts_fixed:
|
|
80
|
-
|
|
160
|
+
self.log.debug("Fixed timestamps")
|
|
81
161
|
max_payload_size_first = max_payload_size - self.ts_size
|
|
82
162
|
else:
|
|
83
|
-
|
|
163
|
+
self.log.debug("Variable timestamps.")
|
|
84
164
|
self.selectable_timestamps = True
|
|
85
|
-
|
|
86
165
|
except Exception as e:
|
|
87
166
|
raise TypeError(f"DAQ_INFO corrupted: {e}") from e
|
|
88
167
|
|
|
89
168
|
# DAQ optimization.
|
|
90
|
-
|
|
169
|
+
# For dynamic DaqList instances, compute physical layout; skip for PredefinedDaqList.
|
|
170
|
+
for idx, daq_list in enumerate(self.daq_lists):
|
|
171
|
+
if isinstance(daq_list, PredefinedDaqList):
|
|
172
|
+
continue
|
|
91
173
|
if self.selectable_timestamps:
|
|
92
174
|
if daq_list.enable_timestamps:
|
|
93
175
|
max_payload_size_first = max_payload_size - self.ts_size
|
|
@@ -98,34 +180,47 @@ class DaqProcessor:
|
|
|
98
180
|
byte_order = 0 if self.xcp_master.slaveProperties.byteOrder == "INTEL" else 1
|
|
99
181
|
self._first_pids = []
|
|
100
182
|
daq_count = len(self.daq_lists)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
183
|
+
|
|
184
|
+
# Decide whether DAQ allocation must be performed.
|
|
185
|
+
config_static = self.daq_info.get("processor", {}).get("properties", {}).get("configType") == "STATIC"
|
|
186
|
+
|
|
187
|
+
if not config_static:
|
|
188
|
+
# For dynamic configuration, program only dynamic (non-predefined) DAQ lists.
|
|
189
|
+
self.xcp_master.freeDaq()
|
|
190
|
+
# Allocate the number of DAQ lists required.
|
|
191
|
+
self.xcp_master.allocDaq(daq_count)
|
|
192
|
+
measurement_list = []
|
|
193
|
+
for i, daq_list in enumerate(self.daq_lists, self.min_daq):
|
|
194
|
+
if isinstance(daq_list, PredefinedDaqList):
|
|
195
|
+
# Skip allocation for predefined DAQ lists.
|
|
196
|
+
continue
|
|
197
|
+
measurements = daq_list.measurements_opt
|
|
198
|
+
measurement_list.append((i, measurements))
|
|
199
|
+
odt_count = len(measurements)
|
|
200
|
+
self.xcp_master.allocOdt(i, odt_count)
|
|
201
|
+
# Iterate again over ODT entries -- we need to respect sequencing requirements.
|
|
202
|
+
for i, measurements in measurement_list:
|
|
203
|
+
for j, measurement in enumerate(measurements):
|
|
204
|
+
entry_count = len(measurement.entries)
|
|
205
|
+
self.xcp_master.allocOdtEntry(i, j, entry_count)
|
|
206
|
+
# Write DAQs (only for dynamic lists)
|
|
207
|
+
for i, daq_list in enumerate(self.daq_lists, self.min_daq):
|
|
208
|
+
if isinstance(daq_list, PredefinedDaqList):
|
|
209
|
+
continue
|
|
210
|
+
measurements = daq_list.measurements_opt
|
|
211
|
+
for j, measurement in enumerate(measurements):
|
|
212
|
+
if len(measurement.entries) == 0:
|
|
213
|
+
continue # CAN special case: No room for data in first ODT.
|
|
214
|
+
self.xcp_master.setDaqPtr(i, j, 0)
|
|
215
|
+
for entry in measurement.entries:
|
|
216
|
+
self.xcp_master.writeDaq(0xFF, entry.length, entry.ext, entry.address)
|
|
217
|
+
else:
|
|
218
|
+
# STATIC configuration on the slave: skip allocation and programming; lists/ODTs are predefined.
|
|
219
|
+
pass
|
|
125
220
|
|
|
126
221
|
# arm DAQ lists -- this is technically a function on its own.
|
|
127
|
-
|
|
128
|
-
|
|
222
|
+
first_daq_list = 0 if config_static else self.min_daq
|
|
223
|
+
for i, daq_list in enumerate(self.daq_lists, first_daq_list):
|
|
129
224
|
mode = 0x00
|
|
130
225
|
if self.supports_timestampes and (self.ts_fixed or (self.selectable_timestamps and daq_list.enable_timestamps)):
|
|
131
226
|
mode = 0x10
|
|
@@ -143,8 +238,6 @@ class DaqProcessor:
|
|
|
143
238
|
)
|
|
144
239
|
res = self.xcp_master.startStopDaqList(0x02, i)
|
|
145
240
|
self._first_pids.append(res.firstPid)
|
|
146
|
-
if start_datetime:
|
|
147
|
-
pass
|
|
148
241
|
self.measurement_params = MeasurementParameters(
|
|
149
242
|
byte_order,
|
|
150
243
|
header_len,
|
|
@@ -168,26 +261,21 @@ class DaqProcessor:
|
|
|
168
261
|
# If a fatal OS error occurred during acquisition, skip sending stop to the slave to avoid
|
|
169
262
|
# cascading timeouts/unrecoverable errors and shut down transport gracefully instead.
|
|
170
263
|
if getattr(self, "_fatal_os_error", False):
|
|
171
|
-
|
|
264
|
+
with suppress(Exception):
|
|
172
265
|
self.log.error(
|
|
173
266
|
"DAQ stop skipped due to previous fatal OS error (e.g., disk full or out-of-memory). Closing transport."
|
|
174
267
|
)
|
|
175
|
-
except Exception:
|
|
176
|
-
pass
|
|
177
268
|
try:
|
|
178
269
|
# Best-effort: stop listener and close transport so threads finish cleanly.
|
|
179
270
|
if hasattr(self.xcp_master, "transport") and self.xcp_master.transport is not None:
|
|
180
271
|
# Signal listeners to stop
|
|
181
|
-
|
|
272
|
+
with suppress(Exception):
|
|
182
273
|
if hasattr(self.xcp_master.transport, "closeEvent"):
|
|
183
274
|
self.xcp_master.transport.closeEvent.set()
|
|
184
|
-
|
|
185
|
-
pass
|
|
275
|
+
|
|
186
276
|
# Close transport connection
|
|
187
|
-
|
|
277
|
+
with suppress(Exception):
|
|
188
278
|
self.xcp_master.transport.close()
|
|
189
|
-
except Exception:
|
|
190
|
-
pass
|
|
191
279
|
finally:
|
|
192
280
|
return
|
|
193
281
|
self.xcp_master.startStopSynch(0x00)
|
|
@@ -197,7 +285,6 @@ class DaqProcessor:
|
|
|
197
285
|
|
|
198
286
|
|
|
199
287
|
class DaqRecorder(DaqProcessor, _DaqRecorderPolicy):
|
|
200
|
-
|
|
201
288
|
def __init__(self, daq_lists: List[DaqList], file_name: str, prealloc: int = 200, chunk_size: int = 1):
|
|
202
289
|
DaqProcessor.__init__(self, daq_lists)
|
|
203
290
|
_DaqRecorderPolicy.__init__(self)
|
|
@@ -253,30 +340,21 @@ class DaqToCsv(DaqOnlinePolicy):
|
|
|
253
340
|
except (OSError, MemoryError) as ex:
|
|
254
341
|
# Mark fatal condition to alter shutdown path and avoid further writes/commands.
|
|
255
342
|
self._fatal_os_error = True
|
|
256
|
-
|
|
343
|
+
with suppress(Exception):
|
|
257
344
|
self.log.critical(f"DAQ file write failed: {ex.__class__.__name__}: {ex}. Initiating graceful shutdown.")
|
|
258
|
-
|
|
259
|
-
pass
|
|
345
|
+
|
|
260
346
|
# Stop listener to prevent more DAQ traffic and avoid thread crashes.
|
|
261
|
-
|
|
347
|
+
with suppress(Exception):
|
|
262
348
|
if hasattr(self.xcp_master, "transport") and self.xcp_master.transport is not None:
|
|
263
349
|
if hasattr(self.xcp_master.transport, "closeEvent"):
|
|
264
350
|
self.xcp_master.transport.closeEvent.set()
|
|
265
|
-
except Exception:
|
|
266
|
-
pass
|
|
267
351
|
# Best-effort: close any opened files to flush buffers and release resources.
|
|
268
|
-
|
|
352
|
+
with suppress(Exception):
|
|
269
353
|
for f in getattr(self, "files", {}).values():
|
|
270
|
-
|
|
354
|
+
with suppress(Exception):
|
|
271
355
|
f.flush()
|
|
272
|
-
|
|
273
|
-
pass
|
|
274
|
-
try:
|
|
356
|
+
with suppress(Exception):
|
|
275
357
|
f.close()
|
|
276
|
-
except Exception:
|
|
277
|
-
pass
|
|
278
|
-
except Exception:
|
|
279
|
-
pass
|
|
280
358
|
# Do not re-raise; allow the system to continue to a controlled shutdown.
|
|
281
359
|
return
|
|
282
360
|
|