govnovod 5

This commit is contained in:
kamil
2026-04-09 12:04:50 +03:00
parent 8710ed8b39
commit 887e2f6730
35 changed files with 200516 additions and 1000059 deletions

135
main.cpp
View File

@ -40,6 +40,12 @@ enum class StopMode {
DiSyn2Fall
};
enum class Di1Mode {
ZeroOnChange,
Trace,
Ignore
};
struct Config {
std::string serial;
std::optional<uint32_t> ip_addr;
@ -60,6 +66,7 @@ struct Config {
uint32_t sync_mode = X502_SYNC_DI_SYN1_RISE;
uint32_t sync_start_mode = X502_SYNC_DI_SYN2_RISE;
StopMode stop_mode = StopMode::DiSyn2Fall;
Di1Mode di1_mode = Di1Mode::ZeroOnChange;
uint32_t recv_block_words = 32768;
uint32_t recv_timeout_ms = 50;
@ -307,6 +314,33 @@ std::string stop_mode_to_string(StopMode mode) {
}
}
Di1Mode parse_di1_mode(const std::string& text) {
const std::string value = trim_copy(text);
if ((value == "zero") || (value == "zero_on_change") || (value == "mark")) {
return Di1Mode::ZeroOnChange;
}
if ((value == "trace") || (value == "plot") || (value == "stream")) {
return Di1Mode::Trace;
}
if ((value == "ignore") || (value == "off") || (value == "none")) {
return Di1Mode::Ignore;
}
fail("Unsupported di1 mode: " + text);
}
std::string di1_mode_to_string(Di1Mode mode) {
switch (mode) {
case Di1Mode::ZeroOnChange:
return "zero";
case Di1Mode::Trace:
return "trace";
case Di1Mode::Ignore:
return "ignore";
default:
return "unknown";
}
}
bool sync_uses_di_syn1(uint32_t mode) {
return (mode == X502_SYNC_DI_SYN1_RISE) || (mode == X502_SYNC_DI_SYN1_FALL);
}
@ -335,6 +369,7 @@ void print_help(const char* exe_name) {
<< " [mode:diff|comm] [range:0.2] [clock:di_syn1_rise]\n"
<< " [start:di_syn2_rise] [stop:di_syn2_fall] [sample_clock_hz:125000|max]\n"
<< " [internal_ref_hz:2000000]\n"
<< " [di1:zero|trace|ignore]\n"
<< " [duration_ms:100] [packet_limit:0] [csv:capture.csv] [svg:capture.svg]\n"
<< " [live_html:live_plot.html] [live_json:live_plot.json]\n"
<< " [recv_block:32768] [stats_period_ms:1000] [live_update_period_ms:1000] [svg_history_packets:50] [start_wait_ms:10000]\n"
@ -355,6 +390,9 @@ void print_help(const char* exe_name) {
<< " sample_clock_hz:125000 -> requested ADC sample rate; for external clock it is the expected rate\n"
<< " sample_clock_hz:max -> with clock:internal, use the maximum ADC speed\n"
<< " internal_ref_hz:2000000 -> internal base clock for clock:internal (1500000 or 2000000)\n"
<< " di1:zero -> write ADC sample as 0 on each DI1 level change\n"
<< " di1:trace -> keep ADC unchanged and store DI1 as a separate synchronous trace\n"
<< " di1:ignore -> ignore DI1 for both zeroing and plotting\n"
<< " stats_period_ms:1000 -> print online transfer/capture statistics every 1000 ms (0 disables)\n"
<< " recv_block:32768 -> request up to 32768 raw 32-bit words per X502_Recv() call\n"
<< " live_update_period_ms:1000 -> refresh live HTML/JSON no more than once per second\n"
@ -379,8 +417,8 @@ void print_help(const char* exe_name) {
<< " stop: frames | di_syn2_rise | di_syn2_fall\n"
<< "\n"
<< "This build enables synchronous DIN together with ADC. DI_SYN2 stop edges are detected\n"
<< "inside the same input stream, packets are split continuously by DI_SYN2 edges, and if\n"
<< "digital input 1 changes state the corresponding ADC sample is written to the buffer as 0.\n"
<< "inside the same input stream, packets are split continuously by DI_SYN2 edges, and DI1\n"
<< "can either zero ADC samples on change, be exported as a separate synchronous trace, or be ignored.\n"
<< "Open live_plot.html in a browser to see the live graph update over time.\n"
<< "The live HTML supports X/Y zoom buttons, mouse-wheel zoom, and reset.\n"
<< "\n"
@ -460,6 +498,10 @@ Config parse_args(int argc, char** argv) {
cfg.stop_mode = parse_stop_mode(arg.substr(5));
continue;
}
if (starts_with(arg, "di1:")) {
cfg.di1_mode = parse_di1_mode(arg.substr(4));
continue;
}
if (starts_with(arg, "sample_clock_hz:")) {
const std::string value = trim_copy(arg.substr(16));
cfg.sample_clock_specified = true;
@ -784,8 +826,11 @@ bool matches_stop_edge(StopMode mode, bool prev_level, bool current_level) {
struct PacketAccumulator {
std::array<std::vector<double>, 2> channels;
std::vector<uint8_t> di1;
std::size_t zeroed_samples = 0;
std::size_t stored_samples = 0;
bool pending_frame_di1_valid = false;
uint8_t pending_frame_di1 = 0;
void reset(std::size_t reserve_frames, std::size_t channel_count) {
for (std::size_t i = 0; i < channels.size(); ++i) {
@ -795,12 +840,20 @@ struct PacketAccumulator {
channel.reserve(reserve_frames);
}
}
di1.clear();
di1.reserve(reserve_frames);
zeroed_samples = 0;
stored_samples = 0;
pending_frame_di1_valid = false;
pending_frame_di1 = 0;
}
std::size_t frame_count(std::size_t channel_count) const {
return (channel_count <= 1U) ? channels[0].size() : std::min(channels[0].size(), channels[1].size());
std::size_t frames = (channel_count <= 1U) ? channels[0].size() : std::min(channels[0].size(), channels[1].size());
if (!di1.empty()) {
frames = std::min(frames, di1.size());
}
return frames;
}
};
@ -982,12 +1035,13 @@ int run(const Config& cfg) {
<< " channel 1: " << phy_channel_name(cfg.mode, cfg.ch1) << "\n"
<< " channel 2: "
<< ((cfg.channel_count >= 2U) ? phy_channel_name(cfg.mode, cfg.ch2) : std::string("disabled")) << "\n"
<< " DI1 handling: " << di1_mode_to_string(cfg.di1_mode) << "\n"
<< " ADC range: +/-" << range_to_volts(cfg.range) << " V\n"
<< " max frames per packet per channel: " << target_frames << "\n";
CaptureFileWriter writer(cfg.csv_path, cfg.svg_path, cfg.live_html_path, cfg.live_json_path);
writer.initialize_live_plot();
writer.initialize_csv(cfg.channel_count);
writer.initialize_csv(cfg.channel_count, cfg.di1_mode == Di1Mode::Trace);
std::cout << " live plot html: " << writer.live_html_path() << "\n"
<< " live plot data: " << writer.live_json_path() << "\n"
<< " recv block words: " << cfg.recv_block_words << "\n"
@ -1064,9 +1118,14 @@ int run(const Config& cfg) {
<< ", ADC samples/s=" << adc_samples_per_s
<< ", DIN samples/s=" << din_samples_per_s
<< ", frames/s per channel=" << frames_per_ch_per_s
<< ", packets/s=" << packets_per_s
<< ", zeroed on DI1 change=" << zeroed_fraction << "% ("
<< stats_zeroed_samples << "/" << stats_stored_adc_samples << ")\n";
<< ", packets/s=" << packets_per_s;
if (cfg.di1_mode == Di1Mode::ZeroOnChange) {
std::cout << ", zeroed on DI1 change=" << zeroed_fraction << "% ("
<< stats_zeroed_samples << "/" << stats_stored_adc_samples << ")";
} else if (cfg.di1_mode == Di1Mode::Trace) {
std::cout << ", DI1 trace=enabled";
}
std::cout << "\n";
if (!final_report) {
stats_window_start = now;
@ -1098,12 +1157,19 @@ int run(const Config& cfg) {
} else {
current_packet.channels[1].clear();
}
if (cfg.di1_mode == Di1Mode::Trace) {
current_packet.di1.resize(frames);
} else {
current_packet.di1.clear();
}
CapturePacket packet;
packet.packet_index = static_cast<std::size_t>(total_completed_packets + 1U);
packet.channel_count = cfg.channel_count;
packet.has_di1_trace = (cfg.di1_mode == Di1Mode::Trace);
packet.ch1 = std::move(current_packet.channels[0]);
packet.ch2 = std::move(current_packet.channels[1]);
packet.di1 = std::move(current_packet.di1);
const double packet_duration_ms = (1000.0 * static_cast<double>(frames)) / actual_frame_freq_hz;
const double zeroed_fraction = (current_packet.stored_samples == 0U)
@ -1115,9 +1181,14 @@ int run(const Config& cfg) {
<< "Packet " << packet.packet_index
<< ": frames/ch=" << frames
<< ", duration_ms=" << packet_duration_ms
<< ", close_reason=" << packet_close_reason_to_string(reason)
<< ", zeroed_on_DI1_change=" << zeroed_fraction << "% ("
<< current_packet.zeroed_samples << "/" << current_packet.stored_samples << ")\n";
<< ", close_reason=" << packet_close_reason_to_string(reason);
if (cfg.di1_mode == Di1Mode::ZeroOnChange) {
std::cout << ", zeroed_on_DI1_change=" << zeroed_fraction << "% ("
<< current_packet.zeroed_samples << "/" << current_packet.stored_samples << ")";
} else if (cfg.di1_mode == Di1Mode::Trace) {
std::cout << ", di1_trace_frames=" << packet.di1.size();
}
std::cout << "\n";
writer.append_csv_packet(packet, actual_frame_freq_hz, csv_global_frame_index);
++total_completed_packets;
@ -1268,12 +1339,12 @@ int run(const Config& cfg) {
pending_din.pop_front();
const bool di1_level = (din_value & kE502Digital1Mask) != 0U;
bool zero_on_di1_change = false;
bool di1_changed = false;
if (!di1_initialized) {
di1_prev_level = di1_level;
di1_initialized = true;
} else if (di1_level != di1_prev_level) {
zero_on_di1_change = true;
di1_changed = true;
di1_prev_level = di1_level;
}
@ -1323,8 +1394,13 @@ int run(const Config& cfg) {
const uint32_t lch = next_lch;
next_lch = (next_lch + 1U) % cfg.channel_count;
if ((cfg.di1_mode == Di1Mode::Trace) && ((cfg.channel_count <= 1U) || (lch == 0U))) {
current_packet.pending_frame_di1 = static_cast<uint8_t>(di1_level ? 1U : 0U);
current_packet.pending_frame_di1_valid = true;
}
double stored_value = adc_value;
if (zero_on_di1_change) {
if ((cfg.di1_mode == Di1Mode::ZeroOnChange) && di1_changed) {
stored_value = 0.0;
++total_zeroed_samples;
++stats_zeroed_samples;
@ -1337,6 +1413,12 @@ int run(const Config& cfg) {
++total_stored_adc_samples;
++stats_stored_adc_samples;
if (lch == (cfg.channel_count - 1U)) {
if ((cfg.di1_mode == Di1Mode::Trace) &&
current_packet.pending_frame_di1_valid &&
(current_packet.di1.size() < target_frames)) {
current_packet.di1.push_back(current_packet.pending_frame_di1);
current_packet.pending_frame_di1_valid = false;
}
++total_completed_frames;
++stats_completed_frames;
}
@ -1382,9 +1464,13 @@ int run(const Config& cfg) {
}
std::cout << "Captured " << total_completed_packets << " packet(s), "
<< total_packet_frames << " total frames per channel kept for final SVG\n"
<< "ADC samples forced to 0 on DI1 change: " << total_zeroed_samples << "\n"
<< "Average stats: "
<< total_packet_frames << " total frames per channel kept for final SVG\n";
if (cfg.di1_mode == Di1Mode::ZeroOnChange) {
std::cout << "ADC samples forced to 0 on DI1 change: " << total_zeroed_samples << "\n";
} else if (cfg.di1_mode == Di1Mode::Trace) {
std::cout << "DI1 synchronous trace exported to CSV/live plot/SVG\n";
}
std::cout << "Average stats: "
<< "MB/s=" << std::fixed << std::setprecision(3)
<< ((static_cast<double>(total_raw_words) * sizeof(uint32_t)) /
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0) /
@ -1401,12 +1487,17 @@ int run(const Config& cfg) {
<< ", packets/s="
<< (static_cast<double>(total_completed_packets) /
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0))
<< ", packets captured=" << total_completed_packets
<< ", zeroed on DI1 change="
<< ((total_stored_adc_samples == 0U)
? 0.0
: (100.0 * static_cast<double>(total_zeroed_samples) / static_cast<double>(total_stored_adc_samples)))
<< "% (" << total_zeroed_samples << "/" << total_stored_adc_samples << ")\n"
<< ", packets captured=" << total_completed_packets;
if (cfg.di1_mode == Di1Mode::ZeroOnChange) {
std::cout << ", zeroed on DI1 change="
<< ((total_stored_adc_samples == 0U)
? 0.0
: (100.0 * static_cast<double>(total_zeroed_samples) / static_cast<double>(total_stored_adc_samples)))
<< "% (" << total_zeroed_samples << "/" << total_stored_adc_samples << ")";
} else if (cfg.di1_mode == Di1Mode::Trace) {
std::cout << ", DI1 trace=enabled";
}
std::cout << "\n"
<< "Final SVG packets retained in memory: " << svg_packets.size() << "\n"
<< "CSV: " << cfg.csv_path << "\n"
<< "SVG: " << cfg.svg_path << "\n";