diff --git a/capture_file_writer.cpp b/capture_file_writer.cpp index 7ded561..6f6767e 100644 --- a/capture_file_writer.cpp +++ b/capture_file_writer.cpp @@ -11,13 +11,23 @@ namespace { +constexpr std::size_t kLivePlotMaxColumns = 800; + struct PlotPoint { std::size_t sample_index = 0; double value = 0.0; }; +std::size_t packet_channel_count(const CapturePacket& packet) { + return (packet.channel_count <= 1U) ? 1U : 2U; +} + +bool packet_has_ch2(const CapturePacket& packet) { + return packet_channel_count(packet) >= 2U; +} + std::size_t packet_frame_count(const CapturePacket& packet) { - return std::min(packet.ch1.size(), packet.ch2.size()); + return packet_has_ch2(packet) ? std::min(packet.ch1.size(), packet.ch2.size()) : packet.ch1.size(); } std::vector build_min_max_trace(const std::vector& data, std::size_t max_columns) { @@ -156,8 +166,8 @@ void write_live_html_document(const std::string& path, const std::string& data_j << "
Open this page once and leave it open. It reloads itself to pick up new packets.
\n" << " \n" << "
\n" - << " CH1\n" - << " CH2\n" + << " CH1\n" + << " CH2\n" << "
\n" << " \n" << " \n" @@ -171,6 +181,7 @@ void write_live_html_document(const std::string& path, const std::string& data_j << " const zeroInfo = document.getElementById('zeroInfo');\n" << " const packetRateInfo = document.getElementById('packetRateInfo');\n" << " const updateInfo = document.getElementById('updateInfo');\n" + << " const legendCh2 = document.getElementById('legendCh2');\n" << " function drawAxes(minY, maxY, maxT) {\n" << " const left = 78, top = 24, width = canvas.width - 112, height = canvas.height - 92;\n" << " ctx.clearRect(0, 0, canvas.width, canvas.height);\n" @@ -212,6 +223,7 @@ void write_live_html_document(const std::string& path, const std::string& data_j << " timingInfo.textContent = '';\n" << " zeroInfo.textContent = '';\n" << " packetRateInfo.textContent = '';\n" + << " legendCh2.style.display = '';\n" << " updateInfo.textContent = 'Auto-refresh every 500 ms';\n" << " statusText.textContent = message;\n" << " ctx.clearRect(0, 0, canvas.width, canvas.height);\n" @@ -221,9 +233,10 @@ void write_live_html_document(const std::string& path, const std::string& data_j << " function renderPacket(data) {\n" << " const ch1 = data.ch1 || [];\n" << " const ch2 = data.ch2 || [];\n" + << " const hasCh2 = (data.channelCount || 2) > 1 && ch2.length > 0;\n" << " const values = [];\n" << " ch1.forEach(p => values.push(p[1]));\n" - << " ch2.forEach(p => values.push(p[1]));\n" + << " if (hasCh2) ch2.forEach(p => values.push(p[1]));\n" << " let minY = Math.min(...values);\n" << " let maxY = Math.max(...values);\n" << " if (!Number.isFinite(minY) || !Number.isFinite(maxY) || minY === maxY) {\n" @@ -235,8 +248,9 @@ void write_live_html_document(const std::string& path, const std::string& data_j << " const maxT = Math.max(data.durationMs / 1000.0, 1e-9);\n" << " const box = drawAxes(minY, maxY, maxT);\n" << " drawTrace(ch1, '#005bbb', box, minY, maxY, maxT);\n" - << " drawTrace(ch2, '#d62828', box, minY, maxY, maxT);\n" - << " packetInfo.textContent = 'Packet #' + data.packetIndex + ' of ' + data.packetsSeen;\n" + << " if (hasCh2) drawTrace(ch2, '#d62828', box, minY, maxY, maxT);\n" + << " legendCh2.style.display = hasCh2 ? '' : 'none';\n" + << " packetInfo.textContent = 'Packet #' + data.packetIndex + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);\n" << " timingInfo.textContent = 'Frames/ch: ' + data.framesPerChannel + ', duration: ' + data.durationMs.toFixed(3) + ' ms';\n" << " packetRateInfo.textContent = 'Packets/s: ' + data.packetsPerSecond.toFixed(3);\n" << " zeroInfo.textContent = 'Zeroed on DI1 change: ' + data.zeroedPercent.toFixed(3) + '% (' + data.zeroedSamples + '/' + data.storedSamples + ')';\n" @@ -306,14 +320,15 @@ void CaptureFileWriter::update_live_plot(const CapturePacket& packet, 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); + const auto trace1 = build_min_max_trace(packet.ch1, kLivePlotMaxColumns); + const auto trace2 = build_min_max_trace(packet.ch2, kLivePlotMaxColumns); std::ostringstream json; json << std::fixed << std::setprecision(9); json << "{\n" << " \"status\": \"packet\",\n" << " \"packetIndex\": " << packet.packet_index << ",\n" + << " \"channelCount\": " << packet_channel_count(packet) << ",\n" << " \"packetsSeen\": " << packets_seen << ",\n" << " \"packetsPerSecond\": " << packets_per_second << ",\n" << " \"framesPerChannel\": " << frames << ",\n" @@ -339,7 +354,12 @@ void CaptureFileWriter::write_csv(const std::vector& packets, 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"; + const std::size_t channel_count = packets.empty() ? 2U : packet_channel_count(packets.front()); + file << "packet_index,frame_index,global_frame_index,time_s,packet_time_s,ch1_v"; + if (channel_count >= 2U) { + file << ",ch2_v"; + } + file << "\n"; file << std::fixed << std::setprecision(9); std::size_t global_frame_index = 0; @@ -349,7 +369,11 @@ void CaptureFileWriter::write_csv(const std::vector& packets, 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"; + << time_s << "," << packet_time_s << "," << packet.ch1[i]; + if (channel_count >= 2U) { + file << "," << packet.ch2[i]; + } + file << "\n"; ++global_frame_index; } } @@ -366,14 +390,17 @@ void CaptureFileWriter::write_svg(const std::vector& packets, std::size_t total_frames = 0; double min_y = std::numeric_limits::infinity(); double max_y = -std::numeric_limits::infinity(); + const std::size_t channel_count = packets.empty() ? 2U : packet_channel_count(packets.front()); 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 (packet_has_ch2(packet)) { + min_y = std::min(min_y, packet.ch2[i]); + max_y = std::max(max_y, packet.ch2[i]); + } } } @@ -444,9 +471,11 @@ void CaptureFileWriter::write_svg(const std::vector& packets, file << " \n"; - file << " \n"; + if (packet_has_ch2(packet)) { + file << " \n"; + } if (packets.size() <= 24U) { file << " & packets, } file << " E-502 capture: " << packets.size() << " packet(s), CH1 and CH2\n"; + << " fill=\"#203040\">E-502 capture: " << packets.size() << " packet(s), " + << ((channel_count >= 2U) ? "CH1 and CH2" : "CH1 only") << "\n"; file << " time, s\n"; file << " & packets, << "\" stroke=\"#005bbb\" stroke-width=\"3\"/>\n"; file << " CH1\n"; - file << " \n"; - file << " CH2\n"; + if (channel_count >= 2U) { + file << " \n"; + file << " CH2\n"; + } file << "\n"; } diff --git a/capture_file_writer.h b/capture_file_writer.h index 9fdee52..6249bef 100644 --- a/capture_file_writer.h +++ b/capture_file_writer.h @@ -6,6 +6,7 @@ struct CapturePacket { std::size_t packet_index = 0; + std::size_t channel_count = 2; std::vector ch1; std::vector ch2; }; diff --git a/main.cpp b/main.cpp index 2aa91d1..97e6946 100644 --- a/main.cpp +++ b/main.cpp @@ -44,8 +44,9 @@ 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_5; + uint32_t range = X502_ADC_RANGE_02; uint32_t ch1 = 2; uint32_t ch2 = 3; @@ -60,12 +61,13 @@ struct Config { 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 recv_block_words = 4096; + uint32_t recv_timeout_ms = 50; uint32_t stats_period_ms = 1000; uint32_t start_wait_ms = 10000; - uint32_t input_buffer_words = 262144; - uint32_t input_step_words = 8192; + uint32_t input_buffer_words = 4 * 1024 * 1024; + uint32_t input_step_words = 4096; + uint32_t live_update_period_ms = 500; bool pullup_syn1 = false; bool pullup_syn2 = false; @@ -207,6 +209,14 @@ uint32_t parse_internal_ref_freq(const std::string& text) { 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: @@ -315,19 +325,22 @@ std::string phy_channel_name(uint32_t mode, uint32_t phy_ch) { 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:di_syn1_rise]\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" << " [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" + << " [recv_block:4096] [stats_period_ms:1000] [live_update_period_ms:500] [start_wait_ms:10000]\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:5 -> +/-5 V range\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" @@ -336,6 +349,7 @@ void print_help(const char* exe_name) { << " 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" + << " live_update_period_ms:500 -> refresh live HTML/JSON no more than twice per second\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" @@ -406,6 +420,10 @@ Config parse_args(int argc, char** argv) { 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; @@ -465,6 +483,10 @@ Config parse_args(int argc, char** argv) { 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"); + continue; + } if (starts_with(arg, "start_wait_ms:")) { cfg.start_wait_ms = parse_u32(arg.substr(14), "start_wait_ms"); continue; @@ -531,12 +553,18 @@ Config parse_args(int argc, char** argv) { } if (cfg.mode == X502_LCH_MODE_DIFF) { - if ((cfg.ch1 >= X502_ADC_DIFF_CH_CNT) || (cfg.ch2 >= X502_ADC_DIFF_CH_CNT)) { - fail("For differential mode E-502 channels must be in range 0..15"); + 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) || (cfg.ch2 >= X502_ADC_COMM_CH_CNT)) { - fail("For common-ground mode E-502 channels must be in range 0..31"); + 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"); } } @@ -719,17 +747,20 @@ struct PacketAccumulator { std::size_t zeroed_samples = 0; std::size_t stored_samples = 0; - void reset(std::size_t reserve_frames) { - for (auto& channel : channels) { + 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(); - channel.reserve(reserve_frames); + if (i < channel_count) { + 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()); + std::size_t frame_count(std::size_t channel_count) const { + return (channel_count <= 1U) ? channels[0].size() : std::min(channels[0].size(), channels[1].size()); } }; @@ -824,9 +855,11 @@ int run(const Config& cfg) { } } - expect_ok(api, api.SetLChannelCount(device.hnd, 2), "Set logical channel count"); + 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"); - expect_ok(api, api.SetLChannel(device.hnd, 1, cfg.ch2, cfg.mode, cfg.range, 1), "Set logical channel 1"); + 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; @@ -887,13 +920,18 @@ int run(const Config& cfg) { } std::cout << "\n" << " DIN clock: " << actual_din_freq_hz << " Hz\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: " << phy_channel_name(cfg.mode, cfg.ch2) << "\n" + << " channel 2: " + << ((cfg.channel_count >= 2U) ? phy_channel_name(cfg.mode, cfg.ch2) : std::string("disabled")) << "\n" << " ADC range: +/-" << range_to_volts(cfg.range) << " V\n" << " max frames per packet per channel: " << target_frames << "\n"; @@ -920,7 +958,7 @@ int run(const Config& cfg) { packets.reserve(cfg.packet_limit); } PacketAccumulator current_packet; - current_packet.reset(target_frames); + current_packet.reset(target_frames, cfg.channel_count); bool capture_started = false; bool stop_loop_requested = false; @@ -935,6 +973,7 @@ int run(const Config& cfg) { 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; @@ -990,18 +1029,23 @@ int run(const Config& cfg) { if (packet_active) { return; } - current_packet.reset(target_frames); + 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(); + const std::size_t frames = current_packet.frame_count(cfg.channel_count); if (frames != 0U) { current_packet.channels[0].resize(frames); - current_packet.channels[1].resize(frames); + if (cfg.channel_count >= 2U) { + current_packet.channels[1].resize(frames); + } else { + current_packet.channels[1].clear(); + } CapturePacket packet; packet.packet_index = packets.size() + 1U; + packet.channel_count = cfg.channel_count; packet.ch1 = std::move(current_packet.channels[0]); packet.ch2 = std::move(current_packet.channels[1]); @@ -1024,20 +1068,35 @@ int run(const Config& cfg) { ++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); + 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.back(), + packets.size(), + 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 << "\n"; + << " 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); + current_packet.reset(target_frames, cfg.channel_count); }; while (!stop_loop_requested) { @@ -1088,16 +1147,22 @@ int run(const Config& cfg) { uint32_t adc_count = static_cast(adc_buffer.size()); uint32_t din_count = static_cast(din_buffer.size()); - expect_ok(api, - 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"); + 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) + << ". Try larger 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; @@ -1182,7 +1247,7 @@ int run(const Config& cfg) { } const uint32_t lch = next_lch; - next_lch = (next_lch + 1U) % 2U; + next_lch = (next_lch + 1U) % cfg.channel_count; double stored_value = adc_value; if (zero_on_di1_change) { @@ -1197,13 +1262,13 @@ int run(const Config& cfg) { ++current_packet.stored_samples; ++total_stored_adc_samples; ++stats_stored_adc_samples; - if (lch == 1U) { + if (lch == (cfg.channel_count - 1U)) { ++total_completed_frames; ++stats_completed_frames; } } - if (current_packet.frame_count() >= target_frames) { + if (current_packet.frame_count(cfg.channel_count) >= target_frames) { finish_packet(PacketCloseReason::DurationLimit); if ((cfg.packet_limit != 0U) && (packets.size() >= cfg.packet_limit)) { stop_loop_requested = true; @@ -1236,7 +1301,9 @@ int run(const Config& cfg) { std::size_t total_packet_frames = 0; for (const auto& packet : packets) { - total_packet_frames += std::min(packet.ch1.size(), packet.ch2.size()); + total_packet_frames += (packet.channel_count <= 1U) + ? packet.ch1.size() + : std::min(packet.ch1.size(), packet.ch2.size()); } std::cout << "Captured " << packets.size() << " packet(s), " diff --git a/main.exe b/main.exe index 268dc64..c825b14 100644 Binary files a/main.exe and b/main.exe differ diff --git a/main.obj b/main.obj index 62ca7ae..58fe454 100644 Binary files a/main.obj and b/main.obj differ