#ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif #endif #ifdef _MSC_VER #pragma warning(push) #pragma warning(disable: 4995) #endif #include "x502api.h" #include "e502api.h" #ifdef _MSC_VER #pragma warning(pop) #endif #include "capture_file_writer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { enum class StopMode { TargetFrames, DiSyn2Rise, DiSyn2Fall }; enum class Di1Mode { ZeroOnChange, Trace, Ignore }; struct Config { std::string serial; std::optional ip_addr; uint32_t channel_count = 2; uint32_t mode = X502_LCH_MODE_DIFF; uint32_t range = X502_ADC_RANGE_02; uint32_t ch1 = 2; uint32_t ch2 = 3; 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_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; uint32_t stats_period_ms = 1000; uint32_t start_wait_ms = 10000; uint32_t input_buffer_words = 8 * 1024 * 1024; uint32_t input_step_words = 32768; uint32_t live_update_period_ms = 1000; uint32_t live_history_packets = 8; uint32_t svg_history_packets = 50; bool full_res_live = false; bool di1_group_average = false; bool recv_block_specified = false; bool input_buffer_specified = false; bool input_step_specified = false; bool live_update_specified = false; bool pullup_syn1 = false; bool pullup_syn2 = false; bool pulldown_conv_in = false; bool pulldown_start_in = false; 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) { throw std::runtime_error(message); } std::string trim_copy(const std::string& text) { const auto first = text.find_first_not_of(" \t\r\n"); if (first == std::string::npos) { return {}; } const auto last = text.find_last_not_of(" \t\r\n"); return text.substr(first, last - first + 1); } bool starts_with(const std::string& value, const std::string& prefix) { return value.rfind(prefix, 0) == 0; } uint32_t parse_u32(const std::string& text, const std::string& field_name) { const std::string clean = trim_copy(text); char* end = nullptr; const auto value = std::strtoull(clean.c_str(), &end, 0); if ((end == clean.c_str()) || (*end != '\0') || (value > std::numeric_limits::max())) { fail("Invalid integer for " + field_name + ": " + text); } return static_cast(value); } double parse_double(const std::string& text, const std::string& field_name) { const std::string clean = trim_copy(text); char* end = nullptr; const double value = std::strtod(clean.c_str(), &end); if ((end == clean.c_str()) || (*end != '\0') || !std::isfinite(value)) { fail("Invalid floating point value for " + field_name + ": " + text); } return value; } uint32_t parse_ipv4(const std::string& text) { std::array parts{}; std::stringstream ss(text); std::string token; for (std::size_t i = 0; i < parts.size(); ++i) { if (!std::getline(ss, token, '.')) { fail("Invalid IPv4 address: " + text); } parts[i] = parse_u32(token, "ip"); if (parts[i] > 255) { fail("IPv4 byte out of range: " + token); } } if (std::getline(ss, token, '.')) { fail("Invalid IPv4 address: " + text); } return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]; } std::string ipv4_to_string(uint32_t ip_addr) { std::ostringstream out; out << ((ip_addr >> 24) & 0xFF) << '.' << ((ip_addr >> 16) & 0xFF) << '.' << ((ip_addr >> 8) & 0xFF) << '.' << (ip_addr & 0xFF); return out.str(); } uint32_t parse_range(const std::string& text) { const double value = parse_double(text, "range"); if (std::fabs(value - 10.0) < 1e-9) { return X502_ADC_RANGE_10; } if (std::fabs(value - 5.0) < 1e-9) { return X502_ADC_RANGE_5; } if (std::fabs(value - 2.0) < 1e-9) { return X502_ADC_RANGE_2; } if (std::fabs(value - 1.0) < 1e-9) { return X502_ADC_RANGE_1; } if (std::fabs(value - 0.5) < 1e-9) { return X502_ADC_RANGE_05; } if (std::fabs(value - 0.2) < 1e-9) { return X502_ADC_RANGE_02; } fail("Unsupported E-502 range: " + text); } double range_to_volts(uint32_t range) { switch (range) { case X502_ADC_RANGE_10: return 10.0; case X502_ADC_RANGE_5: return 5.0; case X502_ADC_RANGE_2: return 2.0; case X502_ADC_RANGE_1: return 1.0; case X502_ADC_RANGE_05: return 0.5; case X502_ADC_RANGE_02: return 0.2; default: fail("Unknown ADC range enum"); } } uint32_t parse_mode(const std::string& text) { const std::string value = trim_copy(text); if ((value == "comm") || (value == "gnd") || (value == "single_ended")) { return X502_LCH_MODE_COMM; } if ((value == "diff") || (value == "differential")) { return X502_LCH_MODE_DIFF; } 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"); } uint32_t parse_channel_count(const std::string& text) { const uint32_t value = parse_u32(text, "channels"); if ((value == 1U) || (value == 2U)) { return value; } fail("channels must be 1 or 2"); } 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")) { return X502_SYNC_EXTERNAL_MASTER; } if (value == "di_syn1_rise") { return X502_SYNC_DI_SYN1_RISE; } if (value == "di_syn1_fall") { return X502_SYNC_DI_SYN1_FALL; } if (value == "di_syn2_rise") { return X502_SYNC_DI_SYN2_RISE; } if (value == "di_syn2_fall") { return X502_SYNC_DI_SYN2_FALL; } if ((value == "internal") || (value == "immediate")) { return X502_SYNC_INTERNAL; } fail("Unsupported sync mode: " + text); } std::string sync_mode_to_string(uint32_t mode, bool for_start) { switch (mode) { case X502_SYNC_INTERNAL: return for_start ? "immediate" : "internal"; case X502_SYNC_EXTERNAL_MASTER: return for_start ? "start_in" : "conv_in"; case X502_SYNC_DI_SYN1_RISE: return "di_syn1_rise"; case X502_SYNC_DI_SYN1_FALL: return "di_syn1_fall"; case X502_SYNC_DI_SYN2_RISE: return "di_syn2_rise"; case X502_SYNC_DI_SYN2_FALL: return "di_syn2_fall"; default: return "unknown"; } } StopMode parse_stop_mode(const std::string& text) { const std::string value = trim_copy(text); if ((value == "frames") || (value == "duration") || (value == "none")) { return StopMode::TargetFrames; } if (value == "di_syn2_rise") { return StopMode::DiSyn2Rise; } if (value == "di_syn2_fall") { return StopMode::DiSyn2Fall; } fail("Unsupported stop mode: " + text); } std::string stop_mode_to_string(StopMode mode) { switch (mode) { case StopMode::TargetFrames: return "target_frames"; case StopMode::DiSyn2Rise: return "di_syn2_rise"; case StopMode::DiSyn2Fall: return "di_syn2_fall"; default: return "unknown"; } } 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); } bool sync_uses_di_syn2(uint32_t mode) { return (mode == X502_SYNC_DI_SYN2_RISE) || (mode == X502_SYNC_DI_SYN2_FALL); } std::string phy_channel_name(uint32_t mode, uint32_t phy_ch) { if (mode == X502_LCH_MODE_DIFF) { return "X" + std::to_string(phy_ch + 1) + "-Y" + std::to_string(phy_ch + 1); } if (phy_ch < 16) { return "X" + std::to_string(phy_ch + 1); } if (phy_ch < 32) { return "Y" + std::to_string((phy_ch - 16) + 1); } return "CH" + std::to_string(phy_ch); } void print_help(const char* exe_name) { std::cout << "Usage:\n" << " " << exe_name << " [serial:SN] [ip:192.168.0.10] [channels:2] [ch1:2] [ch2:3]\n" << " [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] [full_res_live] [di1_group_avg]\n" << " [recv_block:32768] [stats_period_ms:1000] [live_update_period_ms:1000] [live_history_packets:8] [svg_history_packets:50] [start_wait_ms:10000]\n" << " [buffer_words:8388608] [step_words:32768]\n" << " [pullup_syn1] [pullup_syn2] [pulldown_conv_in] [pulldown_start_in]\n" << "\n" << "Defaults for E-502:\n" << " channels:2 -> capture CH1 and CH2\n" << " ch1:2, ch2:3 -> X3-Y3 and X4-Y4\n" << " mode:diff -> differential measurement\n" << " range:0.2 -> +/-0.2 V range\n" << " supported ranges -> 10, 5, 2, 1, 0.5, 0.2 V\n" << " channels:1 -> capture only CH1, CH2 is ignored\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" << " 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" << " live_history_packets:8 -> live graph shows the last 8 packets as one continuous time window (0 = all kept in RAM)\n" << " full_res_live -> add denser live traces and increase graph resolution when zooming\n" << " di1_group_avg -> for di1:trace, average ADC points inside each constant-DI1 run before plotting\n" << " svg_history_packets:50 -> keep last 50 packets in RAM for final SVG (0 keeps all, risky)\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" << " buffer_words:8388608 -> input stream buffer size in 32-bit words\n" << " step_words:32768 -> input stream transfer step in 32-bit words\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" << "\n" << "Common-ground physical channel mapping:\n" << " 0..15 -> X1..X16\n" << " 16..31 -> Y1..Y16\n" << "\n" << "Useful sync lines on E-502:\n" << " clock: conv_in | di_syn1_rise | di_syn1_fall | di_syn2_rise | di_syn2_fall\n" << " 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" << "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 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, Left/Right pan, mouse-wheel zoom, and reset.\n" << "CSV keeps all ADC points; the live graph and SVG use min/max compression for speed.\n" << "Use full_res_live to write denser live traces and sharpen the graph when zooming.\n" << "Use di1_group_avg together with di1:trace to plot one averaged point per constant-DI1 subgroup.\n" << "\n" << "Recommended working example:\n" << " " << exe_name << " 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) { Config cfg; for (int i = 1; i < argc; ++i) { const std::string arg = argv[i]; if ((arg == "help") || (arg == "--help") || (arg == "-h")) { print_help(argv[0]); std::exit(0); } if (arg == "pullup_syn1") { cfg.pullup_syn1 = true; continue; } if (arg == "pullup_syn2") { cfg.pullup_syn2 = true; continue; } if (arg == "pulldown_conv_in") { cfg.pulldown_conv_in = true; continue; } if (arg == "pulldown_start_in") { cfg.pulldown_start_in = true; continue; } if (starts_with(arg, "serial:")) { cfg.serial = arg.substr(7); continue; } if (starts_with(arg, "ip:")) { cfg.ip_addr = parse_ipv4(arg.substr(3)); continue; } if (starts_with(arg, "mode:")) { cfg.mode = parse_mode(arg.substr(5)); continue; } if (starts_with(arg, "channels:")) { cfg.channel_count = parse_channel_count(arg.substr(9)); continue; } if (starts_with(arg, "range:")) { cfg.range = parse_range(arg.substr(6)); continue; } if (starts_with(arg, "ch1:")) { cfg.ch1 = parse_u32(arg.substr(4), "ch1"); continue; } if (starts_with(arg, "ch2:")) { cfg.ch2 = parse_u32(arg.substr(4), "ch2"); continue; } if (starts_with(arg, "clock:")) { cfg.sync_mode = parse_sync_mode(arg.substr(6)); continue; } if (starts_with(arg, "start:")) { cfg.sync_start_mode = parse_sync_mode(arg.substr(6)); continue; } if (starts_with(arg, "stop:")) { 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 (arg == "full_res_live") { cfg.full_res_live = true; continue; } if ((arg == "di1_group_avg") || (arg == "di1_avg")) { cfg.di1_group_average = true; continue; } if (starts_with(arg, "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"); cfg.recv_block_specified = true; continue; } if (starts_with(arg, "recv_timeout_ms:")) { 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, "live_update_period_ms:")) { cfg.live_update_period_ms = parse_u32(arg.substr(22), "live_update_period_ms"); cfg.live_update_specified = true; continue; } if (starts_with(arg, "live_history_packets:")) { cfg.live_history_packets = parse_u32(arg.substr(21), "live_history_packets"); continue; } if (starts_with(arg, "svg_history_packets:")) { cfg.svg_history_packets = parse_u32(arg.substr(20), "svg_history_packets"); continue; } if (starts_with(arg, "start_wait_ms:")) { cfg.start_wait_ms = parse_u32(arg.substr(14), "start_wait_ms"); continue; } if (starts_with(arg, "buffer_words:")) { cfg.input_buffer_words = parse_u32(arg.substr(13), "buffer_words"); cfg.input_buffer_specified = true; continue; } if (starts_with(arg, "step_words:")) { cfg.input_step_words = parse_u32(arg.substr(11), "step_words"); cfg.input_step_specified = true; continue; } if (starts_with(arg, "csv:")) { cfg.csv_path = arg.substr(4); continue; } if (starts_with(arg, "svg:")) { 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.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"); } const bool high_rate_capture = use_internal_max_clock(cfg) || ((cfg.sample_clock_hz >= 1000000.0) && ((cfg.sync_mode == X502_SYNC_INTERNAL) || cfg.sample_clock_specified)); if (high_rate_capture) { if (!cfg.recv_block_specified) { cfg.recv_block_words = std::max(cfg.recv_block_words, 32768U); } if (!cfg.input_step_specified) { cfg.input_step_words = std::max(cfg.input_step_words, 32768U); } if (!cfg.input_buffer_specified) { cfg.input_buffer_words = std::max(cfg.input_buffer_words, 8U * 1024U * 1024U); } if (!cfg.live_update_specified) { cfg.live_update_period_ms = std::max(cfg.live_update_period_ms, 1000U); } } if (cfg.recv_block_words == 0) { fail("recv_block must be > 0"); } if (cfg.input_step_words == 0) { cfg.input_step_words = cfg.recv_block_words; } if (cfg.input_buffer_words < cfg.recv_block_words) { cfg.input_buffer_words = cfg.recv_block_words; } if (sync_uses_di_syn1(cfg.sync_mode) && sync_uses_di_syn1(cfg.sync_start_mode)) { fail("clock and start cannot both use DI_SYN1; use start_in or immediate start"); } if (sync_uses_di_syn2(cfg.sync_mode) && sync_uses_di_syn2(cfg.sync_start_mode)) { fail("clock and start cannot both use DI_SYN2; use start_in or immediate start"); } 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::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) { if (cfg.ch1 >= X502_ADC_DIFF_CH_CNT) { fail("For differential mode E-502 ch1 must be in range 0..15"); } if ((cfg.channel_count >= 2U) && (cfg.ch2 >= X502_ADC_DIFF_CH_CNT)) { fail("For differential mode E-502 ch2 must be in range 0..15"); } } else { if (cfg.ch1 >= X502_ADC_COMM_CH_CNT) { fail("For common-ground mode E-502 ch1 must be in range 0..31"); } if ((cfg.channel_count >= 2U) && (cfg.ch2 >= X502_ADC_COMM_CH_CNT)) { fail("For common-ground mode E-502 ch2 must be in range 0..31"); } } return cfg; } template Fn load_symbol(HMODULE module, const char* name) { const auto addr = GetProcAddress(module, name); if (addr == nullptr) { fail(std::string("GetProcAddress failed for symbol: ") + name); } return reinterpret_cast(addr); } struct Api { HMODULE x502_module = nullptr; HMODULE e502_module = nullptr; decltype(&X502_Create) Create = nullptr; decltype(&X502_Free) Free = nullptr; decltype(&X502_Close) Close = nullptr; decltype(&X502_GetErrorString) GetErrorString = nullptr; decltype(&X502_GetDevInfo2) GetDevInfo2 = nullptr; decltype(&X502_SetMode) SetMode = nullptr; decltype(&X502_StreamsStop) StreamsStop = nullptr; decltype(&X502_StreamsDisable) StreamsDisable = nullptr; decltype(&X502_SetSyncMode) SetSyncMode = nullptr; decltype(&X502_SetSyncStartMode) SetSyncStartMode = nullptr; decltype(&X502_SetLChannelCount) SetLChannelCount = nullptr; 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; decltype(&X502_SetExtRefFreqValue) SetExtRefFreqValue = nullptr; decltype(&X502_Configure) Configure = nullptr; decltype(&X502_StreamsEnable) StreamsEnable = nullptr; decltype(&X502_StreamsStart) StreamsStart = nullptr; decltype(&X502_GetRecvReadyCount) GetRecvReadyCount = nullptr; decltype(&X502_Recv) Recv = nullptr; decltype(&X502_ProcessData) ProcessData = nullptr; decltype(&E502_OpenUsb) OpenUsb = nullptr; decltype(&E502_OpenByIpAddr) OpenByIpAddr = nullptr; Api() { x502_module = LoadLibraryA("x502api.dll"); if (x502_module == nullptr) { fail("Cannot load x502api.dll"); } e502_module = LoadLibraryA("e502api.dll"); if (e502_module == nullptr) { fail("Cannot load e502api.dll"); } Create = load_symbol(x502_module, "X502_Create"); Free = load_symbol(x502_module, "X502_Free"); Close = load_symbol(x502_module, "X502_Close"); GetErrorString = load_symbol(x502_module, "X502_GetErrorString"); GetDevInfo2 = load_symbol(x502_module, "X502_GetDevInfo2"); SetMode = load_symbol(x502_module, "X502_SetMode"); StreamsStop = load_symbol(x502_module, "X502_StreamsStop"); StreamsDisable = load_symbol(x502_module, "X502_StreamsDisable"); SetSyncMode = load_symbol(x502_module, "X502_SetSyncMode"); SetSyncStartMode = load_symbol(x502_module, "X502_SetSyncStartMode"); SetLChannelCount = load_symbol(x502_module, "X502_SetLChannelCount"); 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"); SetExtRefFreqValue = load_symbol(x502_module, "X502_SetExtRefFreqValue"); Configure = load_symbol(x502_module, "X502_Configure"); StreamsEnable = load_symbol(x502_module, "X502_StreamsEnable"); StreamsStart = load_symbol(x502_module, "X502_StreamsStart"); GetRecvReadyCount = load_symbol(x502_module, "X502_GetRecvReadyCount"); Recv = load_symbol(x502_module, "X502_Recv"); ProcessData = load_symbol(x502_module, "X502_ProcessData"); OpenUsb = load_symbol(e502_module, "E502_OpenUsb"); OpenByIpAddr = load_symbol(e502_module, "E502_OpenByIpAddr"); } ~Api() { if (e502_module != nullptr) { FreeLibrary(e502_module); } if (x502_module != nullptr) { FreeLibrary(x502_module); } } }; std::string x502_error(const Api& api, int32_t err) { const char* text = api.GetErrorString ? api.GetErrorString(err) : nullptr; std::ostringstream out; out << "err=" << err; if ((text != nullptr) && (*text != '\0')) { out << " (" << text << ")"; } return out.str(); } void expect_ok(const Api& api, int32_t err, const std::string& what) { if (err != X502_ERR_OK) { fail(what + ": " + x502_error(api, err)); } } constexpr uint32_t kE502DiSyn2Mask = (static_cast(1U) << 13U) | (static_cast(1U) << 17U); constexpr uint32_t kE502Digital1Mask = (static_cast(1U) << 0U); 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::vector 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) { auto& channel = channels[i]; channel.clear(); if (i < channel_count) { 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 { 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; } }; 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; bool opened = false; bool streams_started = false; explicit DeviceHandle(const Api& api_ref) : api(api_ref), hnd(api.Create()) { if (hnd == nullptr) { fail("X502_Create failed"); } } ~DeviceHandle() { if (hnd != nullptr) { if (streams_started) { api.StreamsStop(hnd); } if (opened) { api.Close(hnd); } api.Free(hnd); } } }; void print_device_info(const t_x502_info& info) { std::cout << "Device: " << info.name << "\n" << "Serial: " << info.serial << "\n" << "FPGA version: " << static_cast(info.fpga_ver >> 8) << "." << static_cast(info.fpga_ver & 0xFF) << "\n" << "PLDA version: " << static_cast(info.plda_ver) << "\n" << "Board revision: " << static_cast(info.board_rev) << "\n" << "MCU firmware: " << info.mcu_firmware_ver << "\n"; } int run(const Config& cfg) { Api api; DeviceHandle device(api); int32_t err = X502_ERR_OK; if (cfg.ip_addr.has_value()) { err = api.OpenByIpAddr(device.hnd, *cfg.ip_addr, 0, 5000); } else { err = api.OpenUsb(device.hnd, cfg.serial.empty() ? nullptr : cfg.serial.c_str()); } expect_ok(api, err, "Open device"); device.opened = true; t_x502_info info{}; err = api.GetDevInfo2(device.hnd, &info, sizeof(info)); expect_ok(api, err, "Get device info"); print_device_info(info); expect_ok(api, api.SetMode(device.hnd, X502_MODE_FPGA), "Set FPGA mode"); api.StreamsStop(device.hnd); api.StreamsDisable(device.hnd, X502_STREAM_ALL_IN | X502_STREAM_ALL_OUT); 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) { 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) { expect_ok(api, ext_ref_err, "Set external reference frequency"); } else { std::cerr << "Warning: X502_SetExtRefFreqValue(" << cfg.sample_clock_hz << ") failed, continuing with manual divider configuration: " << x502_error(api, ext_ref_err) << "\n"; } } } expect_ok(api, api.SetLChannelCount(device.hnd, cfg.channel_count), "Set logical channel count"); expect_ok(api, api.SetLChannel(device.hnd, 0, cfg.ch1, cfg.mode, cfg.range, 1), "Set logical channel 0"); if (cfg.channel_count >= 2U) { expect_ok(api, api.SetLChannel(device.hnd, 1, cfg.ch2, cfg.mode, cfg.range, 1), "Set logical channel 1"); } 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()); } const double combined_input_rate_hz = actual_sample_clock_hz + actual_din_freq_hz; if (cfg.ip_addr.has_value() && (combined_input_rate_hz > 2500000.0)) { std::ostringstream message; message << "Current Ethernet input load is too high for E-502: ADC " << actual_sample_clock_hz << " Hz + DIN " << actual_din_freq_hz << " Hz = " << combined_input_rate_hz << " words/s, while the documented Ethernet input-only limit is about 2500000 words/s."; 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"); uint32_t pullups = 0; if (cfg.pullup_syn1) { pullups |= X502_PULLUPS_DI_SYN1; } if (cfg.pullup_syn2) { pullups |= X502_PULLUPS_DI_SYN2; } if (cfg.pulldown_conv_in) { pullups |= X502_PULLDOWN_CONV_IN; } if (cfg.pulldown_start_in) { pullups |= X502_PULLDOWN_START_IN; } 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 | X502_STREAM_DIN), "Enable ADC+DIN stream"); const std::size_t target_frames = std::max( 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: " << 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" << " ADC+DIN total input rate: " << combined_input_rate_hz << " words/s\n" << " ADC logical channels: " << cfg.channel_count << "\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" << " live update period: " << ((cfg.live_update_period_ms == 0U) ? std::string("every packet") : std::to_string(cfg.live_update_period_ms) + " ms") << "\n" << " 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, cfg.full_res_live, cfg.live_history_packets, cfg.di1_group_average); writer.initialize_live_plot(); 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" << " input step words: " << cfg.input_step_words << "\n" << " input buffer words: " << cfg.input_buffer_words << "\n" << " live history packets: " << ((cfg.live_history_packets == 0U) ? std::string("all kept in RAM") : std::to_string(cfg.live_history_packets)) << "\n" << " SVG history packets kept in RAM: " << ((cfg.svg_history_packets == 0U) ? std::string("all (unbounded)") : std::to_string(cfg.svg_history_packets)) << "\n" << " full_res_live: " << (cfg.full_res_live ? "enabled" : "disabled") << "\n" << " di1_group_avg: " << (cfg.di1_group_average ? "enabled" : "disabled") << "\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; const uint32_t read_capacity_words = std::max(cfg.recv_block_words, cfg.input_step_words); std::vector raw(read_capacity_words); std::vector adc_buffer(read_capacity_words); std::vector din_buffer(read_capacity_words); std::deque pending_adc; std::deque pending_din; std::deque packets; PacketAccumulator current_packet; current_packet.reset(target_frames, cfg.channel_count); std::size_t csv_global_frame_index = 0; bool capture_started = 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; ULONGLONG last_live_update = 0; 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; 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; 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, cfg.channel_count); packet_active = true; }; auto finish_packet = [&](PacketCloseReason reason) { const std::size_t frames = current_packet.frame_count(cfg.channel_count); if (frames != 0U) { current_packet.channels[0].resize(frames); if (cfg.channel_count >= 2U) { current_packet.channels[1].resize(frames); } 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(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(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); 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; ++stats_completed_packets; packets.push_back(std::move(packet)); if ((cfg.svg_history_packets != 0U) && (packets.size() > cfg.svg_history_packets)) { packets.pop_front(); } const double elapsed_capture_s = std::max(1e-9, static_cast(GetTickCount64() - capture_loop_start) / 1000.0); const double packets_per_second = (elapsed_capture_s >= 0.1) ? (static_cast(total_completed_packets) / elapsed_capture_s) : 0.0; const ULONGLONG now = GetTickCount64(); const bool should_update_live = (cfg.live_update_period_ms == 0U) || (last_live_update == 0U) || ((now - last_live_update) >= cfg.live_update_period_ms); if (should_update_live) { writer.update_live_plot(packets, static_cast(total_completed_packets), packets_per_second, actual_frame_freq_hz, packet_close_reason_to_string(reason), current_packet.zeroed_samples, current_packet.stored_samples); last_live_update = now; } std::cout << std::fixed << std::setprecision(3) << " packets/s(avg)=" << packets_per_second; if (elapsed_capture_s < 0.1) { std::cout << " (warming up)"; } std::cout << "\n"; } packet_active = false; current_packet.reset(target_frames, cfg.channel_count); }; while (!stop_loop_requested) { if ((cfg.packet_limit != 0U) && (total_completed_packets >= cfg.packet_limit)) { stop_loop_requested = true; break; } if (console_stop_requested()) { if (packet_active) { finish_packet(PacketCloseReason::UserStop); } stop_loop_requested = true; break; } uint32_t recv_request_words = cfg.recv_block_words; uint32_t recv_timeout_ms = cfg.recv_timeout_ms; uint32_t ready_words = 0; const int32_t ready_err = api.GetRecvReadyCount(device.hnd, &ready_words); if ((ready_err == X502_ERR_OK) && (ready_words != 0U)) { recv_request_words = std::min(ready_words, read_capacity_words); recv_timeout_ms = 0; } const int32_t recvd = api.Recv(device.hnd, raw.data(), recv_request_words, 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; message << "Timeout before first ADC data. start=" << sync_mode_to_string(cfg.sync_start_mode, true) << ", clock=" << sync_mode_to_string(cfg.sync_mode, false) << ". "; if (cfg.sync_start_mode == X502_SYNC_EXTERNAL_MASTER) { message << "With start:start_in the module waits for START_IN after StreamsStart(); " "until that condition occurs the external clock is ignored. "; } else if (cfg.sync_start_mode == X502_SYNC_INTERNAL) { message << "Start is immediate, so this usually means there is no valid external clock on the selected clock line. "; } else { message << "This usually means the selected start condition did not occur after StreamsStart(), " "or no valid external clock arrived afterwards. "; } message << "For a quick clock-only check, try start:immediate. " "If you use a separate start pulse, it must arrive after the program starts waiting. " "You can also increase start_wait_ms."; fail(message.str()); } continue; } uint32_t adc_count = static_cast(adc_buffer.size()); uint32_t din_count = static_cast(din_buffer.size()); const int32_t process_err = api.ProcessData(device.hnd, raw.data(), static_cast(recvd), X502_PROC_FLAGS_VOLT, adc_buffer.data(), &adc_count, din_buffer.data(), &din_count); if (process_err == X502_ERR_STREAM_OVERFLOW) { std::ostringstream message; message << "Process ADC+DIN data: " << x502_error(api, process_err) << ". Current ADC rate=" << actual_sample_clock_hz << " Hz, DIN rate=" << actual_din_freq_hz << " Hz, combined input=" << combined_input_rate_hz << " words/s. Try larger recv_block/buffer_words/step_words, a longer " << "live_update_period_ms, or a lower sample_clock_hz / DIN load."; fail(message.str()); } expect_ok(api, process_err, "Process ADC+DIN data"); if ((adc_count == 0U) && (din_count == 0U)) { continue; } 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 di1_changed = false; if (!di1_initialized) { di1_prev_level = di1_level; di1_initialized = true; } else if (di1_level != di1_prev_level) { di1_changed = 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) && (total_completed_packets >= cfg.packet_limit)) { stop_loop_requested = true; } continue; } if (!packet_active) { continue; } 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(di1_level ? 1U : 0U); current_packet.pending_frame_di1_valid = true; } double stored_value = adc_value; if ((cfg.di1_mode == Di1Mode::ZeroOnChange) && di1_changed) { stored_value = 0.0; ++total_zeroed_samples; ++stats_zeroed_samples; ++current_packet.zeroed_samples; } 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 == (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; } } if (current_packet.frame_count(cfg.channel_count) >= target_frames) { finish_packet(PacketCloseReason::DurationLimit); if ((cfg.packet_limit != 0U) && (total_completed_packets >= cfg.packet_limit)) { stop_loop_requested = true; } } } 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; 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); } const std::vector svg_packets(packets.begin(), packets.end()); writer.write(svg_packets, actual_frame_freq_hz, range_to_volts(cfg.range)); std::size_t total_packet_frames = 0; for (const auto& packet : svg_packets) { total_packet_frames += (packet.channel_count <= 1U) ? packet.ch1.size() : std::min(packet.ch1.size(), packet.ch2.size()); } std::cout << "Captured " << total_completed_packets << " packet(s), " << 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(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; if (cfg.di1_mode == Di1Mode::ZeroOnChange) { std::cout << ", 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 << ")"; } 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"; return 0; } } // namespace int main(int argc, char** argv) { try { const Config cfg = parse_args(argc, argv); return run(cfg); } catch (const std::bad_alloc&) { std::cerr << "Error: bad allocation. The process ran out of RAM. " "Try a smaller duration_ms, fewer logical channels, or a lower svg_history_packets " "(for example svg_history_packets:10 or svg_history_packets:0 only if you really need all packets in SVG).\n"; return 1; } catch (const std::exception& ex) { std::cerr << "Error: " << ex.what() << "\n"; return 1; } }