diff --git a/capture_file_writer.cpp b/capture_file_writer.cpp new file mode 100644 index 0000000..7ded561 --- /dev/null +++ b/capture_file_writer.cpp @@ -0,0 +1,494 @@ +#include "capture_file_writer.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct PlotPoint { + std::size_t sample_index = 0; + double value = 0.0; +}; + +std::size_t packet_frame_count(const CapturePacket& packet) { + return std::min(packet.ch1.size(), packet.ch2.size()); +} + +std::vector build_min_max_trace(const std::vector& data, std::size_t max_columns) { + std::vector result; + if (data.empty()) { + return result; + } + + const std::size_t bucket_size = std::max(1, (data.size() + max_columns - 1) / max_columns); + result.reserve(max_columns * 2); + + for (std::size_t begin = 0; begin < data.size(); begin += bucket_size) { + const std::size_t end = std::min(begin + bucket_size, data.size()); + std::size_t min_index = begin; + std::size_t max_index = begin; + for (std::size_t i = begin + 1; i < end; ++i) { + if (data[i] < data[min_index]) { + min_index = i; + } + if (data[i] > data[max_index]) { + max_index = i; + } + } + + if (min_index <= max_index) { + result.push_back({min_index, data[min_index]}); + if (max_index != min_index) { + result.push_back({max_index, data[max_index]}); + } + } else { + result.push_back({max_index, data[max_index]}); + result.push_back({min_index, data[min_index]}); + } + } + + if (result.back().sample_index != (data.size() - 1)) { + result.push_back({data.size() - 1, data.back()}); + } + + return result; +} + +std::string polyline_points(const std::vector& trace, + std::size_t packet_start_index, + std::size_t total_frames, + double y_min, + double y_max, + double left, + double top, + double width, + double height) { + std::ostringstream out; + out << std::fixed << std::setprecision(3); + const double y_span = std::max(1e-12, y_max - y_min); + const double x_span = (total_frames > 1U) ? static_cast(total_frames - 1U) : 1.0; + + for (const auto& point : trace) { + const double global_index = static_cast(packet_start_index + point.sample_index); + const double x = left + (global_index / x_span) * width; + const double y = top + height - ((point.value - y_min) / y_span) * height; + out << x << "," << y << " "; + } + + return out.str(); +} + +std::string json_points(const std::vector& trace, double frame_freq_hz) { + std::ostringstream out; + out << std::fixed << std::setprecision(9); + out << "["; + bool first = true; + for (const auto& point : trace) { + if (!first) { + out << ","; + } + first = false; + out << "[" << (static_cast(point.sample_index) / frame_freq_hz) << "," << point.value << "]"; + } + out << "]"; + return out.str(); +} + +[[noreturn]] void fail_write(const std::string& message); + +void write_text_file(const std::string& path, const std::string& text) { + std::ofstream file(path, std::ios::binary); + if (!file) { + fail_write("Cannot open file for writing: " + path); + } + file << text; +} + +void write_live_html_document(const std::string& path, const std::string& data_json) { + std::ofstream html(path, std::ios::binary); + if (!html) { + fail_write("Cannot open live HTML for writing: " + path); + } + + html << "\n" + << "\n" + << "\n" + << " \n" + << " \n" + << " E-502 Live Plot\n" + << " \n" + << "\n" + << "\n" + << "
\n" + << "
\n" + << "
\n" + << "
\n" + << "
E-502 Live Packet Plot
\n" + << "
\n" + << " Waiting for packets...\n" + << " \n" + << " \n" + << " \n" + << "
\n" + << "
\n" + << "
\n" + << " Auto-refresh every 500 ms\n" + << "
\n" + << "
\n" + << "
Open this page once and leave it open. It reloads itself to pick up new packets.
\n" + << " \n" + << "
\n" + << " CH1\n" + << " CH2\n" + << "
\n" + << "
\n" + << "
\n" + << " \n" + << "\n" + << "\n"; +} + +[[noreturn]] void fail_write(const std::string& message) { + throw std::runtime_error(message); +} + +} // namespace + +CaptureFileWriter::CaptureFileWriter(std::string csv_path, + std::string svg_path, + std::string live_html_path, + std::string live_json_path) + : csv_path_(std::move(csv_path)), + svg_path_(std::move(svg_path)), + live_html_path_(std::move(live_html_path)), + live_json_path_(std::move(live_json_path)) {} + +const std::string& CaptureFileWriter::live_html_path() const { + return live_html_path_; +} + +const std::string& CaptureFileWriter::live_json_path() const { + return live_json_path_; +} + +void CaptureFileWriter::write(const std::vector& packets, + double frame_freq_hz, + double nominal_range_v) const { + write_csv(packets, frame_freq_hz); + write_svg(packets, frame_freq_hz, nominal_range_v); +} + +void CaptureFileWriter::initialize_live_plot() const { + const std::string waiting_json = + "{\n" + " \"status\": \"waiting\",\n" + " \"message\": \"Waiting for first packet...\"\n" + "}\n"; + write_text_file(live_json_path_, waiting_json); + write_live_html_document(live_html_path_, waiting_json); +} + +void CaptureFileWriter::update_live_plot(const CapturePacket& packet, + std::size_t packets_seen, + double packets_per_second, + double frame_freq_hz, + const std::string& close_reason, + std::size_t zeroed_samples, + std::size_t stored_samples) const { + const std::size_t frames = packet_frame_count(packet); + const double duration_ms = (frames == 0U) ? 0.0 : (1000.0 * static_cast(frames) / frame_freq_hz); + const double zeroed_percent = (stored_samples == 0U) + ? 0.0 + : (100.0 * static_cast(zeroed_samples) / static_cast(stored_samples)); + const auto trace1 = build_min_max_trace(packet.ch1, 1800); + const auto trace2 = build_min_max_trace(packet.ch2, 1800); + + std::ostringstream json; + json << std::fixed << std::setprecision(9); + json << "{\n" + << " \"status\": \"packet\",\n" + << " \"packetIndex\": " << packet.packet_index << ",\n" + << " \"packetsSeen\": " << packets_seen << ",\n" + << " \"packetsPerSecond\": " << packets_per_second << ",\n" + << " \"framesPerChannel\": " << frames << ",\n" + << " \"frameFreqHz\": " << frame_freq_hz << ",\n" + << " \"durationMs\": " << duration_ms << ",\n" + << " \"closeReason\": \"" << close_reason << "\",\n" + << " \"zeroedSamples\": " << zeroed_samples << ",\n" + << " \"storedSamples\": " << stored_samples << ",\n" + << " \"zeroedPercent\": " << zeroed_percent << ",\n" + << " \"updatedAt\": \"packet " << packet.packet_index << "\",\n" + << " \"ch1\": " << json_points(trace1, frame_freq_hz) << ",\n" + << " \"ch2\": " << json_points(trace2, frame_freq_hz) << "\n" + << "}\n"; + + write_text_file(live_json_path_, json.str()); + write_live_html_document(live_html_path_, json.str()); +} + +void CaptureFileWriter::write_csv(const std::vector& packets, + double frame_freq_hz) const { + std::ofstream file(csv_path_, std::ios::binary); + if (!file) { + fail_write("Cannot open CSV for writing: " + csv_path_); + } + + file << "packet_index,frame_index,global_frame_index,time_s,packet_time_s,ch1_v,ch2_v\n"; + file << std::fixed << std::setprecision(9); + + std::size_t global_frame_index = 0; + for (const auto& packet : packets) { + const std::size_t frames = packet_frame_count(packet); + for (std::size_t i = 0; i < frames; ++i) { + const double time_s = static_cast(global_frame_index) / frame_freq_hz; + const double packet_time_s = static_cast(i) / frame_freq_hz; + file << packet.packet_index << "," << i << "," << global_frame_index << "," + << time_s << "," << packet_time_s << "," << packet.ch1[i] << "," << packet.ch2[i] << "\n"; + ++global_frame_index; + } + } +} + +void CaptureFileWriter::write_svg(const std::vector& packets, + double frame_freq_hz, + double nominal_range_v) const { + std::ofstream file(svg_path_, std::ios::binary); + if (!file) { + fail_write("Cannot open SVG for writing: " + svg_path_); + } + + std::size_t total_frames = 0; + double min_y = std::numeric_limits::infinity(); + double max_y = -std::numeric_limits::infinity(); + for (const auto& packet : packets) { + const std::size_t frames = packet_frame_count(packet); + total_frames += frames; + for (std::size_t i = 0; i < frames; ++i) { + min_y = std::min(min_y, packet.ch1[i]); + max_y = std::max(max_y, packet.ch1[i]); + min_y = std::min(min_y, packet.ch2[i]); + max_y = std::max(max_y, packet.ch2[i]); + } + } + + if (!std::isfinite(min_y) || !std::isfinite(max_y) || (min_y == max_y)) { + min_y = -nominal_range_v; + max_y = nominal_range_v; + } else { + const double pad = std::max(0.1, (max_y - min_y) * 0.08); + min_y -= pad; + max_y += pad; + } + + const double total_time_s = (total_frames > 1U) ? (static_cast(total_frames - 1U) / frame_freq_hz) : 0.0; + const double width = 1400.0; + const double height = 800.0; + const double left = 90.0; + const double right = 40.0; + const double top = 40.0; + const double bottom = 80.0; + const double plot_w = width - left - right; + const double plot_h = height - top - bottom; + const double zero_y = top + plot_h - ((0.0 - min_y) / std::max(1e-12, max_y - min_y)) * plot_h; + + file << "\n"; + file << " \n"; + file << " \n"; + + for (int i = 0; i <= 10; ++i) { + const double x = left + (plot_w * i / 10.0); + const double y = top + (plot_h * i / 10.0); + file << " \n"; + file << " \n"; + } + + if ((0.0 >= min_y) && (0.0 <= max_y)) { + file << " \n"; + } + + std::size_t frame_offset = 0; + for (std::size_t packet_idx = 0; packet_idx < packets.size(); ++packet_idx) { + const auto& packet = packets[packet_idx]; + const std::size_t frames = packet_frame_count(packet); + if (frames == 0U) { + continue; + } + + const double x0 = left + ((total_frames > 1U) + ? (static_cast(frame_offset) / static_cast(total_frames - 1U)) * plot_w + : 0.0); + const double x1 = left + ((total_frames > 1U) + ? (static_cast(frame_offset + frames - 1U) / static_cast(total_frames - 1U)) * plot_w + : plot_w); + const char* packet_fill = (packet_idx % 2U == 0U) ? "#f7fafc" : "#fdfefe"; + file << " \n"; + if (packet_idx != 0U) { + file << " \n"; + } + + const auto trace1 = build_min_max_trace(packet.ch1, 1200); + const auto trace2 = build_min_max_trace(packet.ch2, 1200); + file << " \n"; + file << " \n"; + + if (packets.size() <= 24U) { + file << " P" + << packet.packet_index << "\n"; + } + + frame_offset += frames; + } + + file << " E-502 capture: " << packets.size() << " packet(s), CH1 and CH2\n"; + file << " time, s\n"; + file << " V\n"; + + file << std::fixed << std::setprecision(6); + for (int i = 0; i <= 10; ++i) { + const double x = left + (plot_w * i / 10.0); + const double t = total_time_s * i / 10.0; + file << " " << t << "\n"; + + const double y = top + plot_h - (plot_h * i / 10.0); + const double v = min_y + (max_y - min_y) * i / 10.0; + file << " " << v << "\n"; + } + + const double legend_y = height - 48.0; + file << " \n"; + file << " CH1\n"; + file << " \n"; + file << " CH2\n"; + file << "\n"; +} diff --git a/capture_file_writer.h b/capture_file_writer.h new file mode 100644 index 0000000..9fdee52 --- /dev/null +++ b/capture_file_writer.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +struct CapturePacket { + std::size_t packet_index = 0; + std::vector ch1; + std::vector ch2; +}; + +class CaptureFileWriter { +public: + CaptureFileWriter(std::string csv_path, + std::string svg_path, + std::string live_html_path, + std::string live_json_path); + + void write(const std::vector& packets, + double frame_freq_hz, + double nominal_range_v) const; + + void initialize_live_plot() const; + + void update_live_plot(const CapturePacket& packet, + std::size_t packets_seen, + double packets_per_second, + double frame_freq_hz, + const std::string& close_reason, + std::size_t zeroed_samples, + std::size_t stored_samples) const; + + const std::string& live_html_path() const; + const std::string& live_json_path() const; + +private: + void write_csv(const std::vector& packets, + double frame_freq_hz) const; + + void write_svg(const std::vector& packets, + double frame_freq_hz, + double nominal_range_v) const; + + std::string csv_path_; + std::string svg_path_; + std::string live_html_path_; + std::string live_json_path_; +}; diff --git a/main.cpp b/main.cpp index 955ba2f..2aa91d1 100644 --- a/main.cpp +++ b/main.cpp @@ -14,13 +14,15 @@ #pragma warning(pop) #endif +#include "capture_file_writer.h" + #include #include #include #include #include #include -#include +#include #include #include #include @@ -47,15 +49,20 @@ struct Config { uint32_t ch1 = 2; uint32_t ch2 = 3; - double sample_clock_hz = 2000000.0; - double duration_ms = 40.0; + double sample_clock_hz = 125000.0; + bool sample_clock_specified = false; + bool max_internal_clock = false; + double duration_ms = 100.0; + uint32_t packet_limit = 0; + uint32_t internal_ref_freq = X502_REF_FREQ_2000KHZ; - uint32_t sync_mode = X502_SYNC_EXTERNAL_MASTER; - uint32_t sync_start_mode = X502_SYNC_DI_SYN1_RISE; - StopMode stop_mode = StopMode::TargetFrames; + uint32_t sync_mode = X502_SYNC_DI_SYN1_RISE; + uint32_t sync_start_mode = X502_SYNC_DI_SYN2_RISE; + StopMode stop_mode = StopMode::DiSyn2Fall; uint32_t recv_block_words = 8192; uint32_t recv_timeout_ms = 100; + uint32_t stats_period_ms = 1000; uint32_t start_wait_ms = 10000; uint32_t input_buffer_words = 262144; uint32_t input_step_words = 8192; @@ -67,6 +74,8 @@ struct Config { std::string csv_path = "capture.csv"; std::string svg_path = "capture.svg"; + std::string live_html_path = "live_plot.html"; + std::string live_json_path = "live_plot.json"; }; [[noreturn]] void fail(const std::string& message) { @@ -187,6 +196,32 @@ uint32_t parse_mode(const std::string& text) { fail("Unsupported input mode: " + text); } +uint32_t parse_internal_ref_freq(const std::string& text) { + const std::string value = trim_copy(text); + if ((value == "2000000") || (value == "2000khz") || (value == "2mhz")) { + return X502_REF_FREQ_2000KHZ; + } + if ((value == "1500000") || (value == "1500khz") || (value == "1.5mhz")) { + return X502_REF_FREQ_1500KHZ; + } + fail("Unsupported internal_ref_hz value: " + text + ". Use 1500000 or 2000000"); +} + +std::string ref_freq_to_string(uint32_t freq) { + switch (freq) { + case X502_REF_FREQ_2000KHZ: + return "2000000"; + case X502_REF_FREQ_1500KHZ: + return "1500000"; + default: + return "unknown"; + } +} + +bool use_internal_max_clock(const Config& cfg) { + return (cfg.sync_mode == X502_SYNC_INTERNAL) && (cfg.max_internal_clock || !cfg.sample_clock_specified); +} + uint32_t parse_sync_mode(const std::string& text) { const std::string value = trim_copy(text); if ((value == "conv_in") || (value == "start_in") || (value == "external_master")) { @@ -281,20 +316,30 @@ void print_help(const char* exe_name) { std::cout << "Usage:\n" << " " << exe_name << " [serial:SN] [ip:192.168.0.10] [ch1:2] [ch2:3]\n" - << " [mode:diff|comm] [range:5] [clock:conv_in]\n" - << " [start:di_syn1_rise] [stop:frames] [sample_clock_hz:2000000]\n" - << " [duration_ms:40] [csv:capture.csv] [svg:capture.svg]\n" - << " [recv_block:8192] [start_wait_ms:10000]\n" + << " [mode:diff|comm] [range:5] [clock:di_syn1_rise]\n" + << " [start:di_syn2_rise] [stop:di_syn2_fall] [sample_clock_hz:125000|max]\n" + << " [internal_ref_hz:2000000]\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:8192] [stats_period_ms:1000] [start_wait_ms:10000]\n" << " [pullup_syn1] [pullup_syn2] [pulldown_conv_in] [pulldown_start_in]\n" << "\n" << "Defaults for E-502:\n" << " ch1:2, ch2:3 -> X3-Y3 and X4-Y4\n" << " mode:diff -> differential measurement\n" << " range:5 -> +/-5 V range\n" - << " clock:conv_in -> external sample clock on CONV_IN\n" - << " start:di_syn1_rise-> start on DI_SYN1 rising edge\n" - << " stop:frames -> stop after duration_ms worth of frames\n" - << " duration_ms:40 -> capture one 40 ms chirp or max length when stop is external\n" + << " clock:di_syn1_rise-> external sample clock on DI_SYN1 rising edge\n" + << " clock:internal -> module generates its own clock\n" + << " start:di_syn2_rise-> packet starts on DI_SYN2 rising edge\n" + << " stop:di_syn2_fall -> packet stops on DI_SYN2 falling edge\n" + << " 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" + << " stats_period_ms:1000 -> print online transfer/capture statistics every 1000 ms (0 disables)\n" + << " duration_ms:100 -> max packet length if stop edge does not arrive\n" + << " packet_limit:0 -> 0 means continuous until Ctrl+C, N means stop after N packets\n" + << " live_html/live_json -> live graph files updated as packets arrive\n" + << " If sample_clock_hz is omitted together with clock:internal, the maximum ADC speed is used\n" << "\n" << "Differential physical channel mapping:\n" << " 0..15 -> X1-Y1 .. X16-Y16\n" @@ -308,13 +353,20 @@ void print_help(const char* exe_name) { << " start: immediate | start_in | di_syn1_rise | di_syn1_fall | di_syn2_rise | di_syn2_fall\n" << " stop: frames | di_syn2_rise | di_syn2_fall\n" << "\n" - << "Stop on DI_SYN2 is polled asynchronously via X502_AsyncInDig(), so ADC stream stays\n" - << "ADC-only. For reliable stop detection hold DI_SYN2 active until the program stops.\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" + << "Open live_plot.html in a browser to see the live graph update over time.\n" << "\n" << "Recommended working example:\n" << " " << exe_name - << " clock:di_syn1_rise start:start_in stop:di_syn2_rise sample_clock_hz:2000000" - << " duration_ms:40 csv:chirp.csv svg:chirp.svg\n"; + << " clock:di_syn1_rise start:di_syn2_rise stop:di_syn2_fall sample_clock_hz:125000" + << " duration_ms:100 packet_limit:10 csv:chirp.csv svg:chirp.svg\n" + << "\n" + << "Internal-clock example:\n" + << " " << exe_name + << " clock:internal internal_ref_hz:2000000 start:di_syn2_rise stop:di_syn2_fall" + << " sample_clock_hz:max duration_ms:100 packet_limit:0 csv:chirp.csv svg:chirp.svg\n"; } Config parse_args(int argc, char** argv) { @@ -379,13 +431,28 @@ Config parse_args(int argc, char** argv) { continue; } if (starts_with(arg, "sample_clock_hz:")) { - cfg.sample_clock_hz = parse_double(arg.substr(16), "sample_clock_hz"); + const std::string value = trim_copy(arg.substr(16)); + cfg.sample_clock_specified = true; + if ((value == "max") || (value == "maximum")) { + cfg.max_internal_clock = true; + } else { + cfg.sample_clock_hz = parse_double(value, "sample_clock_hz"); + cfg.max_internal_clock = false; + } + continue; + } + if (starts_with(arg, "internal_ref_hz:")) { + cfg.internal_ref_freq = parse_internal_ref_freq(arg.substr(16)); continue; } if (starts_with(arg, "duration_ms:")) { cfg.duration_ms = parse_double(arg.substr(12), "duration_ms"); continue; } + if (starts_with(arg, "packet_limit:")) { + cfg.packet_limit = parse_u32(arg.substr(13), "packet_limit"); + continue; + } if (starts_with(arg, "recv_block:")) { cfg.recv_block_words = parse_u32(arg.substr(11), "recv_block"); continue; @@ -394,6 +461,10 @@ Config parse_args(int argc, char** argv) { cfg.recv_timeout_ms = parse_u32(arg.substr(16), "recv_timeout_ms"); continue; } + if (starts_with(arg, "stats_period_ms:")) { + cfg.stats_period_ms = parse_u32(arg.substr(16), "stats_period_ms"); + continue; + } if (starts_with(arg, "start_wait_ms:")) { cfg.start_wait_ms = parse_u32(arg.substr(14), "start_wait_ms"); continue; @@ -414,15 +485,26 @@ Config parse_args(int argc, char** argv) { cfg.svg_path = arg.substr(4); continue; } + if (starts_with(arg, "live_html:")) { + cfg.live_html_path = arg.substr(10); + continue; + } + if (starts_with(arg, "live_json:")) { + cfg.live_json_path = arg.substr(10); + continue; + } fail("Unknown argument: " + arg); } if (cfg.duration_ms <= 0.0) { fail("duration_ms must be > 0"); } - if (cfg.sample_clock_hz <= 0.0) { + if (!cfg.max_internal_clock && (cfg.sample_clock_hz <= 0.0)) { fail("sample_clock_hz must be > 0"); } + if (cfg.max_internal_clock && (cfg.sync_mode != X502_SYNC_INTERNAL)) { + fail("sample_clock_hz:max is only valid together with clock:internal"); + } if (cfg.recv_block_words == 0) { fail("recv_block must be > 0"); } @@ -441,8 +523,11 @@ Config parse_args(int argc, char** argv) { if ((cfg.stop_mode != StopMode::TargetFrames) && sync_uses_di_syn2(cfg.sync_mode)) { fail("DI_SYN2 cannot be used simultaneously for clock and stop"); } - if ((cfg.stop_mode != StopMode::TargetFrames) && sync_uses_di_syn2(cfg.sync_start_mode)) { - fail("DI_SYN2 cannot be used simultaneously for start and stop"); + if ((cfg.stop_mode == StopMode::DiSyn2Rise) && (cfg.sync_start_mode == X502_SYNC_DI_SYN2_RISE)) { + fail("start and stop cannot both use the same DI_SYN2 rising edge"); + } + if ((cfg.stop_mode == StopMode::DiSyn2Fall) && (cfg.sync_start_mode == X502_SYNC_DI_SYN2_FALL)) { + fail("start and stop cannot both use the same DI_SYN2 falling edge"); } if (cfg.mode == X502_LCH_MODE_DIFF) { @@ -485,6 +570,11 @@ struct Api { decltype(&X502_SetLChannel) SetLChannel = nullptr; decltype(&X502_SetAdcFreqDivider) SetAdcFreqDivider = nullptr; decltype(&X502_SetAdcInterframeDelay) SetAdcInterframeDelay = nullptr; + decltype(&X502_SetDinFreqDivider) SetDinFreqDivider = nullptr; + decltype(&X502_SetAdcFreq) SetAdcFreq = nullptr; + decltype(&X502_GetAdcFreq) GetAdcFreq = nullptr; + decltype(&X502_SetDinFreq) SetDinFreq = nullptr; + decltype(&X502_SetRefFreq) SetRefFreq = nullptr; decltype(&X502_SetStreamBufSize) SetStreamBufSize = nullptr; decltype(&X502_SetStreamStep) SetStreamStep = nullptr; decltype(&X502_SetDigInPullup) SetDigInPullup = nullptr; @@ -492,9 +582,8 @@ struct Api { decltype(&X502_Configure) Configure = nullptr; decltype(&X502_StreamsEnable) StreamsEnable = nullptr; decltype(&X502_StreamsStart) StreamsStart = nullptr; - decltype(&X502_AsyncInDig) AsyncInDig = nullptr; decltype(&X502_Recv) Recv = nullptr; - decltype(&X502_ProcessAdcData) ProcessAdcData = nullptr; + decltype(&X502_ProcessData) ProcessData = nullptr; decltype(&E502_OpenUsb) OpenUsb = nullptr; decltype(&E502_OpenByIpAddr) OpenByIpAddr = nullptr; @@ -523,6 +612,11 @@ struct Api { SetLChannel = load_symbol(x502_module, "X502_SetLChannel"); SetAdcFreqDivider = load_symbol(x502_module, "X502_SetAdcFreqDivider"); SetAdcInterframeDelay = load_symbol(x502_module, "X502_SetAdcInterframeDelay"); + SetDinFreqDivider = load_symbol(x502_module, "X502_SetDinFreqDivider"); + SetAdcFreq = load_symbol(x502_module, "X502_SetAdcFreq"); + GetAdcFreq = load_symbol(x502_module, "X502_GetAdcFreq"); + SetDinFreq = load_symbol(x502_module, "X502_SetDinFreq"); + SetRefFreq = load_symbol(x502_module, "X502_SetRefFreq"); SetStreamBufSize = load_symbol(x502_module, "X502_SetStreamBufSize"); SetStreamStep = load_symbol(x502_module, "X502_SetStreamStep"); SetDigInPullup = load_symbol(x502_module, "X502_SetDigInPullup"); @@ -530,9 +624,8 @@ struct Api { Configure = load_symbol(x502_module, "X502_Configure"); StreamsEnable = load_symbol(x502_module, "X502_StreamsEnable"); StreamsStart = load_symbol(x502_module, "X502_StreamsStart"); - AsyncInDig = load_symbol(x502_module, "X502_AsyncInDig"); Recv = load_symbol(x502_module, "X502_Recv"); - ProcessAdcData = load_symbol(x502_module, "X502_ProcessAdcData"); + ProcessData = load_symbol(x502_module, "X502_ProcessData"); OpenUsb = load_symbol(e502_module, "E502_OpenUsb"); OpenByIpAddr = load_symbol(e502_module, "E502_OpenByIpAddr"); @@ -566,13 +659,96 @@ void expect_ok(const Api& api, int32_t err, const std::string& what) { constexpr uint32_t kE502DiSyn2Mask = (static_cast(1U) << 13U) | (static_cast(1U) << 17U); +constexpr uint32_t kE502Digital1Mask = (static_cast(1U) << 0U); -bool read_di_syn2_level(const Api& api, t_x502_hnd hnd) { - uint32_t din = 0; - expect_ok(api, api.AsyncInDig(hnd, &din), "Read digital inputs"); - return (din & kE502DiSyn2Mask) != 0; +volatile LONG g_console_stop_requested = 0; + +BOOL WINAPI console_ctrl_handler(DWORD ctrl_type) { + if ((ctrl_type == CTRL_C_EVENT) || (ctrl_type == CTRL_BREAK_EVENT) || (ctrl_type == CTRL_CLOSE_EVENT)) { + InterlockedExchange(&g_console_stop_requested, 1); + return TRUE; + } + return FALSE; } +bool console_stop_requested() { + return InterlockedCompareExchange(&g_console_stop_requested, 0, 0) != 0; +} + +enum class PacketCloseReason { + ExternalStopEdge, + DurationLimit, + UserStop +}; + +const char* packet_close_reason_to_string(PacketCloseReason reason) { + switch (reason) { + case PacketCloseReason::ExternalStopEdge: + return "DI_SYN2 edge"; + case PacketCloseReason::DurationLimit: + return "duration limit"; + case PacketCloseReason::UserStop: + return "user stop"; + default: + return "unknown"; + } +} + +bool matches_sync_edge(uint32_t mode, bool prev_level, bool current_level) { + if (mode == X502_SYNC_DI_SYN2_RISE) { + return !prev_level && current_level; + } + if (mode == X502_SYNC_DI_SYN2_FALL) { + return prev_level && !current_level; + } + return false; +} + +bool matches_stop_edge(StopMode mode, bool prev_level, bool current_level) { + if (mode == StopMode::DiSyn2Rise) { + return !prev_level && current_level; + } + if (mode == StopMode::DiSyn2Fall) { + return prev_level && !current_level; + } + return false; +} + +struct PacketAccumulator { + std::array, 2> channels; + std::size_t zeroed_samples = 0; + std::size_t stored_samples = 0; + + void reset(std::size_t reserve_frames) { + for (auto& channel : channels) { + channel.clear(); + channel.reserve(reserve_frames); + } + zeroed_samples = 0; + stored_samples = 0; + } + + std::size_t frame_count() const { + return std::min(channels[0].size(), channels[1].size()); + } +}; + +struct ConsoleCtrlGuard { + bool installed = false; + + ConsoleCtrlGuard() { + InterlockedExchange(&g_console_stop_requested, 0); + installed = SetConsoleCtrlHandler(console_ctrl_handler, TRUE) != 0; + } + + ~ConsoleCtrlGuard() { + if (installed) { + SetConsoleCtrlHandler(console_ctrl_handler, FALSE); + } + InterlockedExchange(&g_console_stop_requested, 0); + } +}; + struct DeviceHandle { const Api& api; t_x502_hnd hnd = nullptr; @@ -608,199 +784,6 @@ void print_device_info(const t_x502_info& info) { << "MCU firmware: " << info.mcu_firmware_ver << "\n"; } -struct PlotPoint { - std::size_t sample_index = 0; - double value = 0.0; -}; - -std::vector build_min_max_trace(const std::vector& data, std::size_t max_columns) { - std::vector result; - if (data.empty()) { - return result; - } - - const std::size_t bucket_size = std::max(1, (data.size() + max_columns - 1) / max_columns); - result.reserve(max_columns * 2); - - for (std::size_t begin = 0; begin < data.size(); begin += bucket_size) { - const std::size_t end = std::min(begin + bucket_size, data.size()); - std::size_t min_index = begin; - std::size_t max_index = begin; - for (std::size_t i = begin + 1; i < end; ++i) { - if (data[i] < data[min_index]) { - min_index = i; - } - if (data[i] > data[max_index]) { - max_index = i; - } - } - - if (min_index <= max_index) { - result.push_back({min_index, data[min_index]}); - if (max_index != min_index) { - result.push_back({max_index, data[max_index]}); - } - } else { - result.push_back({max_index, data[max_index]}); - result.push_back({min_index, data[min_index]}); - } - } - - if (result.back().sample_index != (data.size() - 1)) { - result.push_back({data.size() - 1, data.back()}); - } - return result; -} - -std::string polyline_points(const std::vector& trace, - double max_time_s, - double y_min, - double y_max, - double left, - double top, - double width, - double height) { - std::ostringstream out; - out << std::fixed << std::setprecision(3); - const double y_span = std::max(1e-12, y_max - y_min); - const std::size_t max_index = trace.empty() ? 1 : trace.back().sample_index; - const double time_scale = (max_time_s > 0.0) ? max_time_s : 1.0; - - for (const auto& point : trace) { - const double time_s = (max_index == 0) - ? 0.0 - : (static_cast(point.sample_index) / static_cast(max_index)) * time_scale; - const double x = left + (time_s / time_scale) * width; - const double y = top + height - ((point.value - y_min) / y_span) * height; - out << x << "," << y << " "; - } - return out.str(); -} - -void write_csv(const std::string& path, - const std::vector& ch1, - const std::vector& ch2, - double frame_freq_hz) { - std::ofstream file(path, std::ios::binary); - if (!file) { - fail("Cannot open CSV for writing: " + path); - } - - file << "frame_index,time_s,ch1_v,ch2_v\n"; - file << std::fixed << std::setprecision(9); - const std::size_t frames = std::min(ch1.size(), ch2.size()); - for (std::size_t i = 0; i < frames; ++i) { - const double time_s = static_cast(i) / frame_freq_hz; - file << i << "," << time_s << "," << ch1[i] << "," << ch2[i] << "\n"; - } -} - -void write_svg(const std::string& path, - const std::vector& ch1, - const std::vector& ch2, - double frame_freq_hz, - double nominal_range_v) { - std::ofstream file(path, std::ios::binary); - if (!file) { - fail("Cannot open SVG for writing: " + path); - } - - const std::size_t frames = std::min(ch1.size(), ch2.size()); - const double total_time_s = (frames > 1) ? (static_cast(frames - 1) / frame_freq_hz) : 0.0; - - double min_y = std::numeric_limits::infinity(); - double max_y = -std::numeric_limits::infinity(); - for (double v : ch1) { - min_y = std::min(min_y, v); - max_y = std::max(max_y, v); - } - for (double v : ch2) { - min_y = std::min(min_y, v); - max_y = std::max(max_y, v); - } - if (!std::isfinite(min_y) || !std::isfinite(max_y) || (min_y == max_y)) { - min_y = -nominal_range_v; - max_y = nominal_range_v; - } else { - const double pad = std::max(0.1, (max_y - min_y) * 0.08); - min_y -= pad; - max_y += pad; - } - - const auto trace1 = build_min_max_trace(ch1, 1800); - const auto trace2 = build_min_max_trace(ch2, 1800); - - const double width = 1400.0; - const double height = 800.0; - const double left = 90.0; - const double right = 40.0; - const double top = 40.0; - const double bottom = 80.0; - const double plot_w = width - left - right; - const double plot_h = height - top - bottom; - const double zero_y = top + plot_h - ((0.0 - min_y) / std::max(1e-12, max_y - min_y)) * plot_h; - - file << "\n"; - file << " \n"; - file << " \n"; - - for (int i = 0; i <= 10; ++i) { - const double x = left + (plot_w * i / 10.0); - const double y = top + (plot_h * i / 10.0); - file << " \n"; - file << " \n"; - } - - if ((0.0 >= min_y) && (0.0 <= max_y)) { - file << " \n"; - } - - file << " \n"; - file << " \n"; - - file << " E-502 capture: CH1 and CH2\n"; - file << " time, s\n"; - file << " V\n"; - - file << std::fixed << std::setprecision(6); - for (int i = 0; i <= 10; ++i) { - const double x = left + (plot_w * i / 10.0); - const double t = total_time_s * i / 10.0; - file << " " << t << "\n"; - - const double y = top + plot_h - (plot_h * i / 10.0); - const double v = min_y + (max_y - min_y) * i / 10.0; - file << " " << v << "\n"; - } - - const double legend_y = height - 48.0; - file << " \n"; - file << " CH1\n"; - file << " \n"; - file << " CH2\n"; - file << "\n"; -} - int run(const Config& cfg) { Api api; DeviceHandle device(api); @@ -826,7 +809,9 @@ int run(const Config& cfg) { expect_ok(api, api.SetSyncMode(device.hnd, cfg.sync_mode), "Set sync mode"); expect_ok(api, api.SetSyncStartMode(device.hnd, cfg.sync_start_mode), "Set sync start mode"); - if (cfg.sync_mode != X502_SYNC_INTERNAL) { + if (cfg.sync_mode == X502_SYNC_INTERNAL) { + expect_ok(api, api.SetRefFreq(device.hnd, cfg.internal_ref_freq), "Set internal reference frequency"); + } else { const int32_t ext_ref_err = api.SetExtRefFreqValue(device.hnd, cfg.sample_clock_hz); if (ext_ref_err != X502_ERR_OK) { if (cfg.sample_clock_hz <= 1500000.0) { @@ -842,8 +827,29 @@ int run(const Config& cfg) { expect_ok(api, api.SetLChannelCount(device.hnd, 2), "Set logical channel count"); expect_ok(api, api.SetLChannel(device.hnd, 0, cfg.ch1, cfg.mode, cfg.range, 1), "Set logical channel 0"); expect_ok(api, api.SetLChannel(device.hnd, 1, cfg.ch2, cfg.mode, cfg.range, 1), "Set logical channel 1"); - expect_ok(api, api.SetAdcFreqDivider(device.hnd, 1), "Set ADC frequency divider"); - expect_ok(api, api.SetAdcInterframeDelay(device.hnd, 0), "Set ADC interframe delay"); + + const bool internal_max_clock = use_internal_max_clock(cfg); + double actual_sample_clock_hz = cfg.sample_clock_hz; + double actual_frame_freq_hz = cfg.sample_clock_hz / 2.0; + if (internal_max_clock) { + actual_sample_clock_hz = static_cast(cfg.internal_ref_freq); + actual_frame_freq_hz = 0.0; + expect_ok(api, api.SetAdcFreq(device.hnd, &actual_sample_clock_hz, nullptr), "Set ADC frequency to maximum"); + expect_ok(api, api.GetAdcFreq(device.hnd, &actual_sample_clock_hz, &actual_frame_freq_hz), + "Get effective ADC frequency"); + } else { + expect_ok(api, api.SetAdcFreq(device.hnd, &actual_sample_clock_hz, &actual_frame_freq_hz), "Set ADC frequency"); + } + + double actual_din_freq_hz = actual_sample_clock_hz; + expect_ok(api, api.SetDinFreq(device.hnd, &actual_din_freq_hz), "Set DIN frequency"); + if (std::fabs(actual_din_freq_hz - actual_sample_clock_hz) > std::max(0.5, actual_sample_clock_hz * 1e-6)) { + std::ostringstream message; + message << "ADC and DIN frequencies no longer match closely enough for 1:1 masking logic. " + << "ADC=" << actual_sample_clock_hz << " Hz, DIN=" << actual_din_freq_hz << " Hz."; + fail(message.str()); + } + expect_ok(api, api.SetStreamBufSize(device.hnd, X502_STREAM_CH_IN, cfg.input_buffer_words), "Set input buffer size"); expect_ok(api, api.SetStreamStep(device.hnd, X502_STREAM_CH_IN, cfg.input_step_words), "Set input stream step"); @@ -863,47 +869,198 @@ int run(const Config& cfg) { expect_ok(api, api.SetDigInPullup(device.hnd, pullups), "Set digital input pullups/pulldowns"); expect_ok(api, api.Configure(device.hnd, 0), "Configure device"); - expect_ok(api, api.StreamsEnable(device.hnd, X502_STREAM_ADC), "Enable ADC stream"); + expect_ok(api, api.StreamsEnable(device.hnd, X502_STREAM_ADC | X502_STREAM_DIN), "Enable ADC+DIN stream"); - const double frame_freq_hz = cfg.sample_clock_hz / 2.0; const std::size_t target_frames = std::max( - 1, static_cast(std::llround((cfg.duration_ms / 1000.0) * frame_freq_hz))); + 1, static_cast(std::llround((cfg.duration_ms / 1000.0) * actual_frame_freq_hz))); std::cout << "Capture settings:\n" << " clock source: " << sync_mode_to_string(cfg.sync_mode, false) << "\n" << " start source: " << sync_mode_to_string(cfg.sync_start_mode, true) << "\n" << " stop source: " << stop_mode_to_string(cfg.stop_mode) << "\n" - << " sample clock: " << cfg.sample_clock_hz << " Hz\n" - << " per-channel frame rate: " << frame_freq_hz << " Hz\n" + << " sample clock: " << actual_sample_clock_hz << " Hz"; + if (cfg.sync_mode == X502_SYNC_INTERNAL) { + std::cout << " (internal ref " << ref_freq_to_string(cfg.internal_ref_freq) << " Hz)"; + if (internal_max_clock) { + std::cout << ", maximum ADC speed"; + } + } + std::cout << "\n" + << " DIN clock: " << actual_din_freq_hz << " Hz\n" + << " per-channel frame rate: " << actual_frame_freq_hz << " Hz\n" << " duration: " << cfg.duration_ms << " ms\n" + << " packet limit: " + << ((cfg.packet_limit == 0U) ? std::string("continuous until Ctrl+C") + : std::to_string(cfg.packet_limit) + " packet(s)") << "\n" << " channel 1: " << phy_channel_name(cfg.mode, cfg.ch1) << "\n" << " channel 2: " << phy_channel_name(cfg.mode, cfg.ch2) << "\n" << " ADC range: +/-" << range_to_volts(cfg.range) << " V\n" - << " target frames per channel: " << target_frames << "\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(); + std::cout << " live plot html: " << writer.live_html_path() << "\n" + << " live plot data: " << writer.live_json_path() << "\n"; + + ConsoleCtrlGuard console_guard; + if (!console_guard.installed) { + std::cerr << "Warning: Ctrl+C handler could not be installed; continuous mode may stop abruptly.\n"; + } expect_ok(api, api.StreamsStart(device.hnd), "Start streams"); device.streams_started = true; std::vector raw(cfg.recv_block_words); - std::vector processed(cfg.recv_block_words); - std::array, 2> channels; - channels[0].reserve(target_frames); - channels[1].reserve(target_frames); + std::vector adc_buffer(cfg.recv_block_words); + std::vector din_buffer(cfg.recv_block_words); + std::deque pending_adc; + std::deque pending_din; + std::vector packets; + if (cfg.packet_limit != 0U) { + packets.reserve(cfg.packet_limit); + } + PacketAccumulator current_packet; + current_packet.reset(target_frames); bool capture_started = false; - bool stop_requested = false; - bool stop_prev_initialized = false; - bool stop_prev_level = false; - bool stopped_by_external_signal = false; + bool stop_loop_requested = false; + bool packet_active = false; + bool trigger_level_initialized = false; + bool trigger_prev_level = false; + std::size_t total_zeroed_samples = 0; uint32_t next_lch = 0; + bool di1_initialized = false; + bool di1_prev_level = false; const ULONGLONG start_wait_deadline = GetTickCount64() + cfg.start_wait_ms; + const ULONGLONG capture_loop_start = GetTickCount64(); + ULONGLONG stats_window_start = capture_loop_start; + ULONGLONG last_stats_print = capture_loop_start; + + uint64_t total_raw_words = 0; + uint64_t total_adc_samples = 0; + uint64_t total_din_samples = 0; + uint64_t total_stored_adc_samples = 0; + uint64_t total_completed_frames = 0; + uint64_t total_completed_packets = 0; + + uint64_t stats_raw_words = 0; + uint64_t stats_adc_samples = 0; + uint64_t stats_din_samples = 0; + uint64_t stats_stored_adc_samples = 0; + uint64_t stats_zeroed_samples = 0; + uint64_t stats_completed_frames = 0; + uint64_t stats_completed_packets = 0; + + auto print_stats = [&](bool final_report) { + const ULONGLONG now = GetTickCount64(); + const double elapsed_s = std::max(1e-9, static_cast(now - stats_window_start) / 1000.0); + const double mb_per_s = (static_cast(stats_raw_words) * sizeof(uint32_t)) / elapsed_s / 1000.0 / 1000.0; + const double adc_samples_per_s = static_cast(stats_adc_samples) / elapsed_s; + const double din_samples_per_s = static_cast(stats_din_samples) / elapsed_s; + const double frames_per_ch_per_s = static_cast(stats_completed_frames) / elapsed_s; + const double packets_per_s = static_cast(stats_completed_packets) / elapsed_s; + const double zeroed_fraction = (stats_stored_adc_samples == 0U) + ? 0.0 + : (100.0 * static_cast(stats_zeroed_samples) / static_cast(stats_stored_adc_samples)); + + std::cout << std::fixed << std::setprecision(3) + << (final_report ? "Final stats: " : "Online stats: ") + << "MB/s=" << mb_per_s + << ", 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"; + + if (!final_report) { + stats_window_start = now; + last_stats_print = now; + stats_raw_words = 0; + stats_adc_samples = 0; + stats_din_samples = 0; + stats_stored_adc_samples = 0; + stats_zeroed_samples = 0; + stats_completed_frames = 0; + stats_completed_packets = 0; + } + }; + + auto start_packet = [&]() { + if (packet_active) { + return; + } + current_packet.reset(target_frames); + packet_active = true; + }; + + auto finish_packet = [&](PacketCloseReason reason) { + const std::size_t frames = current_packet.frame_count(); + if (frames != 0U) { + current_packet.channels[0].resize(frames); + current_packet.channels[1].resize(frames); + + CapturePacket packet; + packet.packet_index = packets.size() + 1U; + packet.ch1 = std::move(current_packet.channels[0]); + packet.ch2 = std::move(current_packet.channels[1]); + + const double packet_duration_ms = (1000.0 * static_cast(frames)) / actual_frame_freq_hz; + const double zeroed_fraction = (current_packet.stored_samples == 0U) + ? 0.0 + : (100.0 * static_cast(current_packet.zeroed_samples) / + static_cast(current_packet.stored_samples)); + + std::cout << std::fixed << std::setprecision(3) + << "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"; + + packets.push_back(std::move(packet)); + ++total_completed_packets; + ++stats_completed_packets; + const double elapsed_capture_s = + std::max(1e-9, static_cast(GetTickCount64() - capture_loop_start) / 1000.0); + const double packets_per_second = static_cast(total_completed_packets) / elapsed_capture_s; + writer.update_live_plot(packets.back(), + packets.size(), + packets_per_second, + actual_frame_freq_hz, + packet_close_reason_to_string(reason), + current_packet.zeroed_samples, + current_packet.stored_samples); + std::cout << std::fixed << std::setprecision(3) + << " packets/s(avg)=" << packets_per_second << "\n"; + } + + packet_active = false; + current_packet.reset(target_frames); + }; + + while (!stop_loop_requested) { + if ((cfg.packet_limit != 0U) && (packets.size() >= cfg.packet_limit)) { + stop_loop_requested = true; + break; + } + if (console_stop_requested()) { + if (packet_active) { + finish_packet(PacketCloseReason::UserStop); + } + stop_loop_requested = true; + break; + } - while (!stop_requested && - ((channels[0].size() < target_frames) || (channels[1].size() < target_frames))) { const int32_t recvd = api.Recv(device.hnd, raw.data(), cfg.recv_block_words, cfg.recv_timeout_ms); if (recvd < 0) { fail("X502_Recv failed: " + x502_error(api, recvd)); } + if (recvd > 0) { + total_raw_words += static_cast(recvd); + stats_raw_words += static_cast(recvd); + } if (recvd == 0) { if (!capture_started && (GetTickCount64() >= start_wait_deadline)) { std::ostringstream message; @@ -929,58 +1086,185 @@ int run(const Config& cfg) { continue; } - uint32_t adc_count = static_cast(recvd); + uint32_t adc_count = static_cast(adc_buffer.size()); + uint32_t din_count = static_cast(din_buffer.size()); expect_ok(api, - api.ProcessAdcData(device.hnd, raw.data(), processed.data(), &adc_count, X502_PROC_FLAGS_VOLT), - "Process ADC data"); + api.ProcessData(device.hnd, + raw.data(), + static_cast(recvd), + X502_PROC_FLAGS_VOLT, + adc_buffer.data(), + &adc_count, + din_buffer.data(), + &din_count), + "Process ADC+DIN data"); - if (adc_count == 0) { + if ((adc_count == 0U) && (din_count == 0U)) { continue; } - capture_started = true; + capture_started = capture_started || (adc_count != 0U); + total_adc_samples += adc_count; + total_din_samples += din_count; + stats_adc_samples += adc_count; + stats_din_samples += din_count; for (uint32_t i = 0; i < adc_count; ++i) { + pending_adc.push_back(adc_buffer[i]); + } + for (uint32_t i = 0; i < din_count; ++i) { + pending_din.push_back(din_buffer[i]); + } + + if ((pending_adc.size() > 1000000U) || (pending_din.size() > 1000000U)) { + fail("Internal backlog grew too large while aligning ADC and DIN samples"); + } + + while (!pending_adc.empty() && + !pending_din.empty() && + !stop_loop_requested) { + const double adc_value = pending_adc.front(); + pending_adc.pop_front(); + + const uint32_t din_value = pending_din.front(); + pending_din.pop_front(); + + const bool di1_level = (din_value & kE502Digital1Mask) != 0U; + bool zero_on_di1_change = 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_prev_level = di1_level; + } + + const bool di_syn2_level = (din_value & kE502DiSyn2Mask) != 0U; + bool start_edge = false; + bool stop_edge = false; + if (!trigger_level_initialized) { + trigger_prev_level = di_syn2_level; + trigger_level_initialized = true; + if (!packet_active) { + if (cfg.sync_start_mode == X502_SYNC_INTERNAL) { + start_packet(); + } else if (sync_uses_di_syn2(cfg.sync_start_mode) && di_syn2_level) { + start_packet(); + } else if (!sync_uses_di_syn2(cfg.sync_start_mode)) { + start_packet(); + } + } + } else { + start_edge = matches_sync_edge(cfg.sync_start_mode, trigger_prev_level, di_syn2_level); + if (cfg.stop_mode != StopMode::TargetFrames) { + stop_edge = matches_stop_edge(cfg.stop_mode, trigger_prev_level, di_syn2_level); + } + trigger_prev_level = di_syn2_level; + } + + if (!packet_active && (cfg.sync_start_mode == X502_SYNC_INTERNAL)) { + start_packet(); + } + + if (!packet_active && start_edge) { + start_packet(); + } + + if (packet_active && stop_edge) { + finish_packet(PacketCloseReason::ExternalStopEdge); + if ((cfg.packet_limit != 0U) && (packets.size() >= cfg.packet_limit)) { + stop_loop_requested = true; + } + continue; + } + + if (!packet_active) { + continue; + } + const uint32_t lch = next_lch; next_lch = (next_lch + 1U) % 2U; - if (channels[lch].size() < target_frames) { - channels[lch].push_back(processed[i]); + + double stored_value = adc_value; + if (zero_on_di1_change) { + stored_value = 0.0; + ++total_zeroed_samples; + ++stats_zeroed_samples; + ++current_packet.zeroed_samples; } - if ((channels[0].size() >= target_frames) && (channels[1].size() >= target_frames)) { - break; + + if (current_packet.channels[lch].size() < target_frames) { + current_packet.channels[lch].push_back(stored_value); + ++current_packet.stored_samples; + ++total_stored_adc_samples; + ++stats_stored_adc_samples; + if (lch == 1U) { + ++total_completed_frames; + ++stats_completed_frames; + } + } + + if (current_packet.frame_count() >= target_frames) { + finish_packet(PacketCloseReason::DurationLimit); + if ((cfg.packet_limit != 0U) && (packets.size() >= cfg.packet_limit)) { + stop_loop_requested = true; + } } } - if ((cfg.stop_mode != StopMode::TargetFrames) && capture_started) { - const bool stop_level = read_di_syn2_level(api, device.hnd); - if (!stop_prev_initialized) { - stop_prev_level = stop_level; - stop_prev_initialized = true; - } else { - const bool is_edge = - ((cfg.stop_mode == StopMode::DiSyn2Rise) && !stop_prev_level && stop_level) || - ((cfg.stop_mode == StopMode::DiSyn2Fall) && stop_prev_level && !stop_level); - if (is_edge) { - stop_requested = true; - stopped_by_external_signal = true; - } - stop_prev_level = stop_level; + if (console_stop_requested()) { + if (packet_active) { + finish_packet(PacketCloseReason::UserStop); } + stop_loop_requested = true; + } + + if ((cfg.stats_period_ms != 0U) && ((GetTickCount64() - last_stats_print) >= cfg.stats_period_ms)) { + print_stats(false); } } expect_ok(api, api.StreamsStop(device.hnd), "Stop streams"); device.streams_started = false; - const std::size_t frames = std::min(channels[0].size(), channels[1].size()); - channels[0].resize(frames); - channels[1].resize(frames); + if ((cfg.stats_period_ms != 0U) && ((stats_raw_words != 0U) || (stats_adc_samples != 0U) || + (stats_din_samples != 0U) || (stats_stored_adc_samples != 0U) || (stats_zeroed_samples != 0U) || + (stats_completed_frames != 0U))) { + print_stats(false); + } - write_csv(cfg.csv_path, channels[0], channels[1], frame_freq_hz); - write_svg(cfg.svg_path, channels[0], channels[1], frame_freq_hz, range_to_volts(cfg.range)); + writer.write(packets, actual_frame_freq_hz, range_to_volts(cfg.range)); - std::cout << "Captured " << frames << " frames per channel\n" - << "Stop reason: " - << (stopped_by_external_signal ? "DI_SYN2 edge" : "target frame count") << "\n" + std::size_t total_packet_frames = 0; + for (const auto& packet : packets) { + total_packet_frames += std::min(packet.ch1.size(), packet.ch2.size()); + } + + std::cout << "Captured " << packets.size() << " packet(s), " + << total_packet_frames << " total frames per channel\n" + << "ADC samples forced to 0 on DI1 change: " << total_zeroed_samples << "\n" + << "Average stats: " + << "MB/s=" << std::fixed << std::setprecision(3) + << ((static_cast(total_raw_words) * sizeof(uint32_t)) / + std::max(1e-9, static_cast(GetTickCount64() - capture_loop_start) / 1000.0) / + 1000.0 / 1000.0) + << ", ADC samples/s=" + << (static_cast(total_adc_samples) / + std::max(1e-9, static_cast(GetTickCount64() - capture_loop_start) / 1000.0)) + << ", DIN samples/s=" + << (static_cast(total_din_samples) / + std::max(1e-9, static_cast(GetTickCount64() - capture_loop_start) / 1000.0)) + << ", frames/s per channel=" + << (static_cast(total_completed_frames) / + std::max(1e-9, static_cast(GetTickCount64() - capture_loop_start) / 1000.0)) + << ", packets/s=" + << (static_cast(total_completed_packets) / + std::max(1e-9, static_cast(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(total_zeroed_samples) / static_cast(total_stored_adc_samples))) + << "% (" << total_zeroed_samples << "/" << total_stored_adc_samples << ")\n" << "CSV: " << cfg.csv_path << "\n" << "SVG: " << cfg.svg_path << "\n"; diff --git a/main.exe b/main.exe index adaad89..268dc64 100644 Binary files a/main.exe and b/main.exe differ diff --git a/main.obj b/main.obj index 4db6de1..62ca7ae 100644 Binary files a/main.obj and b/main.obj differ