From 6bdcfcb605f811c1dd0cfc4256f0db65c176d6a2 Mon Sep 17 00:00:00 2001 From: kamil Date: Thu, 9 Apr 2026 12:04:50 +0300 Subject: [PATCH] govnovod 5 --- capture_file_writer.cpp | 167 ++++++++++++++++++-- capture_file_writer.h | 4 +- live_plot.html | 332 ++++++++++++++++++++++++++++++++++++++++ live_plot.js | 21 +++ live_plot.json | 20 +++ main.cpp | 135 +++++++++++++--- 6 files changed, 644 insertions(+), 35 deletions(-) create mode 100644 live_plot.html create mode 100644 live_plot.js create mode 100644 live_plot.json diff --git a/capture_file_writer.cpp b/capture_file_writer.cpp index 4d4aff1..e3dcbf1 100644 --- a/capture_file_writer.cpp +++ b/capture_file_writer.cpp @@ -16,19 +16,20 @@ namespace { constexpr std::size_t kLivePlotMaxColumns = 800; constexpr uint32_t kCsvSpoolMagic = 0x4C564353U; // "SVCL" -constexpr uint32_t kCsvSpoolVersion = 1U; +constexpr uint32_t kCsvSpoolVersion = 2U; struct CsvSpoolFileHeader { uint32_t magic = kCsvSpoolMagic; uint32_t version = kCsvSpoolVersion; uint32_t channel_count = 0; - uint32_t reserved = 0; + uint32_t has_di1_trace = 0; }; struct CsvSpoolPacketHeader { uint64_t packet_index = 0; uint64_t channel_count = 0; uint64_t frame_count = 0; + uint64_t has_di1_trace = 0; }; struct PlotPoint { @@ -44,8 +45,16 @@ bool packet_has_ch2(const CapturePacket& packet) { return packet_channel_count(packet) >= 2U; } +bool packet_has_di1_trace(const CapturePacket& packet) { + return packet.has_di1_trace && !packet.di1.empty(); +} + std::size_t packet_frame_count(const CapturePacket& packet) { - return packet_has_ch2(packet) ? std::min(packet.ch1.size(), packet.ch2.size()) : packet.ch1.size(); + std::size_t frames = packet_has_ch2(packet) ? std::min(packet.ch1.size(), packet.ch2.size()) : packet.ch1.size(); + if (packet_has_di1_trace(packet)) { + frames = std::min(frames, packet.di1.size()); + } + return frames; } std::vector build_min_max_trace(const std::vector& data, std::size_t max_columns) { @@ -88,6 +97,32 @@ std::vector build_min_max_trace(const std::vector& data, std: return result; } +std::vector build_digital_step_trace(const std::vector& data) { + std::vector result; + if (data.empty()) { + return result; + } + + result.reserve(std::min(data.size() * 2U, 16384U)); + uint8_t previous = data.front(); + result.push_back({0U, static_cast(previous)}); + + for (std::size_t i = 1; i < data.size(); ++i) { + const uint8_t current = data[i]; + if (current != previous) { + result.push_back({i - 1U, static_cast(previous)}); + result.push_back({i, static_cast(current)}); + previous = current; + } + } + + if (result.back().sample_index != (data.size() - 1U)) { + result.push_back({data.size() - 1U, static_cast(data.back())}); + } + + return result; +} + std::string polyline_points(const std::vector& trace, std::size_t packet_start_index, std::size_t total_frames, @@ -112,6 +147,27 @@ std::string polyline_points(const std::vector& trace, return out.str(); } +std::string digital_polyline_points(const std::vector& trace, + std::size_t packet_start_index, + std::size_t total_frames, + double left, + double top, + double width, + double height) { + std::ostringstream out; + out << std::fixed << std::setprecision(3); + 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 - ((0.15 + 0.7 * point.value) * 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); @@ -203,7 +259,7 @@ void write_live_html_document(const std::string& path, const std::string& data_s << " \n" << " E-502 Live Plot\n" << " + + +
+
+
+
+
E-502 Live Packet Plot
+
+ Waiting for packets... + + + + +
+
+
+ Auto-refresh every 500 ms +
+
+
Open this page once and leave it open. It refreshes its data script to pick up new packets.
+
+ + + + + + + Wheel: X zoom, Shift+wheel: Y zoom, double-click: reset +
+ +
+ CH1 + CH2 + DI1 +
+
+
+ + + diff --git a/live_plot.js b/live_plot.js new file mode 100644 index 0000000..ca63a1d --- /dev/null +++ b/live_plot.js @@ -0,0 +1,21 @@ +window.e502LiveData = { + "status": "packet", + "packetIndex": 7206, + "channelCount": 2, + "packetsSeen": 7206, + "packetsPerSecond": 23.749101911, + "framesPerChannel": 1, + "frameFreqHz": 1000000.000000000, + "durationMs": 0.001000000, + "closeReason": "DI_SYN2 edge", + "zeroedSamples": 0, + "storedSamples": 2, + "zeroedPercent": 0.000000000, + "hasDi1Trace": true, + "di1Frames": 1, + "updatedAt": "packet 7206", + "ch1": [[0.000000000,0.006862633]], + "ch2": [[0.000000000,0.016366433]], + "di1": [[0.000000000,0.000000000]] +} +; diff --git a/live_plot.json b/live_plot.json new file mode 100644 index 0000000..12e1982 --- /dev/null +++ b/live_plot.json @@ -0,0 +1,20 @@ +{ + "status": "packet", + "packetIndex": 7206, + "channelCount": 2, + "packetsSeen": 7206, + "packetsPerSecond": 23.749101911, + "framesPerChannel": 1, + "frameFreqHz": 1000000.000000000, + "durationMs": 0.001000000, + "closeReason": "DI_SYN2 edge", + "zeroedSamples": 0, + "storedSamples": 2, + "zeroedPercent": 0.000000000, + "hasDi1Trace": true, + "di1Frames": 1, + "updatedAt": "packet 7206", + "ch1": [[0.000000000,0.006862633]], + "ch2": [[0.000000000,0.016366433]], + "di1": [[0.000000000,0.000000000]] +} diff --git a/main.cpp b/main.cpp index 00a08ef..0cf51b5 100644 --- a/main.cpp +++ b/main.cpp @@ -40,6 +40,12 @@ enum class StopMode { DiSyn2Fall }; +enum class Di1Mode { + ZeroOnChange, + Trace, + Ignore +}; + struct Config { std::string serial; std::optional ip_addr; @@ -60,6 +66,7 @@ struct Config { uint32_t sync_mode = X502_SYNC_DI_SYN1_RISE; uint32_t sync_start_mode = X502_SYNC_DI_SYN2_RISE; StopMode stop_mode = StopMode::DiSyn2Fall; + Di1Mode di1_mode = Di1Mode::ZeroOnChange; uint32_t recv_block_words = 32768; uint32_t recv_timeout_ms = 50; @@ -307,6 +314,33 @@ std::string stop_mode_to_string(StopMode mode) { } } +Di1Mode parse_di1_mode(const std::string& text) { + const std::string value = trim_copy(text); + if ((value == "zero") || (value == "zero_on_change") || (value == "mark")) { + return Di1Mode::ZeroOnChange; + } + if ((value == "trace") || (value == "plot") || (value == "stream")) { + return Di1Mode::Trace; + } + if ((value == "ignore") || (value == "off") || (value == "none")) { + return Di1Mode::Ignore; + } + fail("Unsupported di1 mode: " + text); +} + +std::string di1_mode_to_string(Di1Mode mode) { + switch (mode) { + case Di1Mode::ZeroOnChange: + return "zero"; + case Di1Mode::Trace: + return "trace"; + case Di1Mode::Ignore: + return "ignore"; + default: + return "unknown"; + } +} + bool sync_uses_di_syn1(uint32_t mode) { return (mode == X502_SYNC_DI_SYN1_RISE) || (mode == X502_SYNC_DI_SYN1_FALL); } @@ -335,6 +369,7 @@ void print_help(const char* exe_name) { << " [mode:diff|comm] [range:0.2] [clock:di_syn1_rise]\n" << " [start:di_syn2_rise] [stop:di_syn2_fall] [sample_clock_hz:125000|max]\n" << " [internal_ref_hz:2000000]\n" + << " [di1:zero|trace|ignore]\n" << " [duration_ms:100] [packet_limit:0] [csv:capture.csv] [svg:capture.svg]\n" << " [live_html:live_plot.html] [live_json:live_plot.json]\n" << " [recv_block:32768] [stats_period_ms:1000] [live_update_period_ms:1000] [svg_history_packets:50] [start_wait_ms:10000]\n" @@ -355,6 +390,9 @@ void print_help(const char* exe_name) { << " sample_clock_hz:125000 -> requested ADC sample rate; for external clock it is the expected rate\n" << " sample_clock_hz:max -> with clock:internal, use the maximum ADC speed\n" << " internal_ref_hz:2000000 -> internal base clock for clock:internal (1500000 or 2000000)\n" + << " di1:zero -> write ADC sample as 0 on each DI1 level change\n" + << " di1:trace -> keep ADC unchanged and store DI1 as a separate synchronous trace\n" + << " di1:ignore -> ignore DI1 for both zeroing and plotting\n" << " stats_period_ms:1000 -> print online transfer/capture statistics every 1000 ms (0 disables)\n" << " recv_block:32768 -> request up to 32768 raw 32-bit words per X502_Recv() call\n" << " live_update_period_ms:1000 -> refresh live HTML/JSON no more than once per second\n" @@ -379,8 +417,8 @@ void print_help(const char* exe_name) { << " stop: frames | di_syn2_rise | di_syn2_fall\n" << "\n" << "This build enables synchronous DIN together with ADC. DI_SYN2 stop edges are detected\n" - << "inside the same input stream, packets are split continuously by DI_SYN2 edges, and if\n" - << "digital input 1 changes state the corresponding ADC sample is written to the buffer as 0.\n" + << "inside the same input stream, packets are split continuously by DI_SYN2 edges, and DI1\n" + << "can either zero ADC samples on change, be exported as a separate synchronous trace, or be ignored.\n" << "Open live_plot.html in a browser to see the live graph update over time.\n" << "The live HTML supports X/Y zoom buttons, mouse-wheel zoom, and reset.\n" << "\n" @@ -460,6 +498,10 @@ Config parse_args(int argc, char** argv) { cfg.stop_mode = parse_stop_mode(arg.substr(5)); continue; } + if (starts_with(arg, "di1:")) { + cfg.di1_mode = parse_di1_mode(arg.substr(4)); + continue; + } if (starts_with(arg, "sample_clock_hz:")) { const std::string value = trim_copy(arg.substr(16)); cfg.sample_clock_specified = true; @@ -784,8 +826,11 @@ bool matches_stop_edge(StopMode mode, bool prev_level, bool current_level) { struct PacketAccumulator { std::array, 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) { @@ -795,12 +840,20 @@ struct PacketAccumulator { channel.reserve(reserve_frames); } } + di1.clear(); + di1.reserve(reserve_frames); zeroed_samples = 0; stored_samples = 0; + pending_frame_di1_valid = false; + pending_frame_di1 = 0; } std::size_t frame_count(std::size_t channel_count) const { - return (channel_count <= 1U) ? channels[0].size() : std::min(channels[0].size(), channels[1].size()); + std::size_t frames = (channel_count <= 1U) ? channels[0].size() : std::min(channels[0].size(), channels[1].size()); + if (!di1.empty()) { + frames = std::min(frames, di1.size()); + } + return frames; } }; @@ -982,12 +1035,13 @@ int run(const Config& cfg) { << " channel 1: " << phy_channel_name(cfg.mode, cfg.ch1) << "\n" << " channel 2: " << ((cfg.channel_count >= 2U) ? phy_channel_name(cfg.mode, cfg.ch2) : std::string("disabled")) << "\n" + << " DI1 handling: " << di1_mode_to_string(cfg.di1_mode) << "\n" << " ADC range: +/-" << range_to_volts(cfg.range) << " V\n" << " max frames per packet per channel: " << target_frames << "\n"; CaptureFileWriter writer(cfg.csv_path, cfg.svg_path, cfg.live_html_path, cfg.live_json_path); writer.initialize_live_plot(); - writer.initialize_csv(cfg.channel_count); + writer.initialize_csv(cfg.channel_count, cfg.di1_mode == Di1Mode::Trace); std::cout << " live plot html: " << writer.live_html_path() << "\n" << " live plot data: " << writer.live_json_path() << "\n" << " recv block words: " << cfg.recv_block_words << "\n" @@ -1064,9 +1118,14 @@ int run(const Config& cfg) { << ", ADC samples/s=" << adc_samples_per_s << ", DIN samples/s=" << din_samples_per_s << ", frames/s per channel=" << frames_per_ch_per_s - << ", packets/s=" << packets_per_s - << ", zeroed on DI1 change=" << zeroed_fraction << "% (" - << stats_zeroed_samples << "/" << stats_stored_adc_samples << ")\n"; + << ", packets/s=" << packets_per_s; + if (cfg.di1_mode == Di1Mode::ZeroOnChange) { + std::cout << ", zeroed on DI1 change=" << zeroed_fraction << "% (" + << stats_zeroed_samples << "/" << stats_stored_adc_samples << ")"; + } else if (cfg.di1_mode == Di1Mode::Trace) { + std::cout << ", DI1 trace=enabled"; + } + std::cout << "\n"; if (!final_report) { stats_window_start = now; @@ -1098,12 +1157,19 @@ int run(const Config& cfg) { } else { current_packet.channels[1].clear(); } + if (cfg.di1_mode == Di1Mode::Trace) { + current_packet.di1.resize(frames); + } else { + current_packet.di1.clear(); + } CapturePacket packet; packet.packet_index = static_cast(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) @@ -1115,9 +1181,14 @@ int run(const Config& cfg) { << "Packet " << packet.packet_index << ": frames/ch=" << frames << ", duration_ms=" << packet_duration_ms - << ", close_reason=" << packet_close_reason_to_string(reason) - << ", zeroed_on_DI1_change=" << zeroed_fraction << "% (" - << current_packet.zeroed_samples << "/" << current_packet.stored_samples << ")\n"; + << ", close_reason=" << packet_close_reason_to_string(reason); + if (cfg.di1_mode == Di1Mode::ZeroOnChange) { + std::cout << ", zeroed_on_DI1_change=" << zeroed_fraction << "% (" + << current_packet.zeroed_samples << "/" << current_packet.stored_samples << ")"; + } else if (cfg.di1_mode == Di1Mode::Trace) { + std::cout << ", di1_trace_frames=" << packet.di1.size(); + } + std::cout << "\n"; writer.append_csv_packet(packet, actual_frame_freq_hz, csv_global_frame_index); ++total_completed_packets; @@ -1268,12 +1339,12 @@ int run(const Config& cfg) { pending_din.pop_front(); const bool di1_level = (din_value & kE502Digital1Mask) != 0U; - bool zero_on_di1_change = false; + bool di1_changed = false; if (!di1_initialized) { di1_prev_level = di1_level; di1_initialized = true; } else if (di1_level != di1_prev_level) { - zero_on_di1_change = true; + di1_changed = true; di1_prev_level = di1_level; } @@ -1323,8 +1394,13 @@ int run(const Config& cfg) { const uint32_t lch = next_lch; next_lch = (next_lch + 1U) % cfg.channel_count; + if ((cfg.di1_mode == Di1Mode::Trace) && ((cfg.channel_count <= 1U) || (lch == 0U))) { + current_packet.pending_frame_di1 = static_cast(di1_level ? 1U : 0U); + current_packet.pending_frame_di1_valid = true; + } + double stored_value = adc_value; - if (zero_on_di1_change) { + if ((cfg.di1_mode == Di1Mode::ZeroOnChange) && di1_changed) { stored_value = 0.0; ++total_zeroed_samples; ++stats_zeroed_samples; @@ -1337,6 +1413,12 @@ int run(const Config& cfg) { ++total_stored_adc_samples; ++stats_stored_adc_samples; if (lch == (cfg.channel_count - 1U)) { + if ((cfg.di1_mode == Di1Mode::Trace) && + current_packet.pending_frame_di1_valid && + (current_packet.di1.size() < target_frames)) { + current_packet.di1.push_back(current_packet.pending_frame_di1); + current_packet.pending_frame_di1_valid = false; + } ++total_completed_frames; ++stats_completed_frames; } @@ -1382,9 +1464,13 @@ int run(const Config& cfg) { } std::cout << "Captured " << total_completed_packets << " packet(s), " - << total_packet_frames << " total frames per channel kept for final SVG\n" - << "ADC samples forced to 0 on DI1 change: " << total_zeroed_samples << "\n" - << "Average stats: " + << total_packet_frames << " total frames per channel kept for final SVG\n"; + if (cfg.di1_mode == Di1Mode::ZeroOnChange) { + std::cout << "ADC samples forced to 0 on DI1 change: " << total_zeroed_samples << "\n"; + } else if (cfg.di1_mode == Di1Mode::Trace) { + std::cout << "DI1 synchronous trace exported to CSV/live plot/SVG\n"; + } + std::cout << "Average stats: " << "MB/s=" << std::fixed << std::setprecision(3) << ((static_cast(total_raw_words) * sizeof(uint32_t)) / std::max(1e-9, static_cast(GetTickCount64() - capture_loop_start) / 1000.0) / @@ -1401,12 +1487,17 @@ int run(const Config& cfg) { << ", 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" + << ", 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";