#include "capture_file_writer.h" #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr std::size_t kLivePlotMaxColumns = 800; constexpr std::size_t kLivePlotMidColumns = 4096; constexpr std::size_t kLivePlotHighColumns = 16384; constexpr std::size_t kLivePlotRawFrameLimit = 50000; constexpr uint32_t kCsvSpoolMagic = 0x4C564353U; // "SVCL" constexpr uint32_t kCsvSpoolVersion = 2U; struct CsvSpoolFileHeader { uint32_t magic = kCsvSpoolMagic; uint32_t version = kCsvSpoolVersion; uint32_t channel_count = 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 { std::size_t sample_index = 0; double value = 0.0; }; struct Di1GroupedTraces { std::vector ch1; std::vector ch2; std::vector rss; }; std::size_t packet_frame_count(const CapturePacket& packet); 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; } bool packet_has_di1_trace(const CapturePacket& packet) { return packet.has_di1_trace && !packet.di1.empty(); } std::vector build_rss_values(const CapturePacket& packet) { const std::size_t frames = packet_frame_count(packet); std::vector rss; if (!packet_has_ch2(packet) || (frames == 0U)) { return rss; } rss.reserve(frames); for (std::size_t i = 0; i < frames; ++i) { const double ch1 = packet.ch1[i]; const double ch2 = packet.ch2[i]; rss.push_back(std::sqrt((ch1 * ch1) + (ch2 * ch2))); } return rss; } std::size_t packet_frame_count(const CapturePacket& packet) { 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) { 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::vector build_full_trace(const std::vector& data) { std::vector result; result.reserve(data.size()); for (std::size_t i = 0; i < data.size(); ++i) { result.push_back({i, data[i]}); } return result; } void append_offset_trace(std::vector& dst, const std::vector& src, std::size_t sample_offset) { dst.reserve(dst.size() + src.size()); for (const auto& point : src) { dst.push_back({sample_offset + point.sample_index, point.value}); } } Di1GroupedTraces build_di1_grouped_traces(const std::vector& ch1, const std::vector& ch2, const std::vector& di1, bool has_ch2) { Di1GroupedTraces traces; const std::size_t frames = std::min(ch1.size(), di1.size()); if (frames == 0U) { return traces; } traces.ch1.reserve(frames / 2U + 1U); if (has_ch2) { traces.ch2.reserve(frames / 2U + 1U); traces.rss.reserve(frames / 2U + 1U); } std::size_t begin = 0U; while (begin < frames) { const uint8_t level = di1[begin]; std::size_t end = begin + 1U; while ((end < frames) && (di1[end] == level)) { ++end; } double sum1 = 0.0; double sum2 = 0.0; for (std::size_t i = begin; i < end; ++i) { sum1 += ch1[i]; if (has_ch2 && (i < ch2.size())) { sum2 += ch2[i]; } } const double count = static_cast(end - begin); const std::size_t mid = begin + ((end - begin - 1U) / 2U); const double avg1 = sum1 / count; traces.ch1.push_back({mid, avg1}); if (has_ch2) { const double avg2 = sum2 / count; traces.ch2.push_back({mid, avg2}); traces.rss.push_back({mid, std::sqrt((avg1 * avg1) + (avg2 * avg2))}); } begin = end; } return traces; } 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, 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 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); 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(); } std::string json_packet_markers(const std::vector>& markers, double frame_freq_hz) { std::ostringstream out; out << std::fixed << std::setprecision(9); out << "["; bool first = true; for (const auto& marker : markers) { if (!first) { out << ","; } first = false; out << "{\"t\":" << (static_cast(marker.first) / frame_freq_hz) << ",\"label\":\"P" << marker.second << "\"}"; } out << "]"; return out.str(); } [[noreturn]] void fail_write(const std::string& message); std::string replace_extension(const std::string& path, const std::string& new_extension) { const std::size_t slash_pos = path.find_last_of("/\\"); const std::size_t dot_pos = path.find_last_of('.'); if ((dot_pos == std::string::npos) || ((slash_pos != std::string::npos) && (dot_pos < slash_pos))) { return path + new_extension; } return path.substr(0, dot_pos) + new_extension; } std::string path_to_script_url(std::string path) { for (char& ch : path) { if (ch == '\\') { ch = '/'; } } std::string encoded; encoded.reserve(path.size() + 8); for (char ch : path) { switch (ch) { case ' ': encoded += "%20"; break; case '#': encoded += "%23"; break; case '%': encoded += "%25"; break; case '?': encoded += "%3F"; break; default: encoded.push_back(ch); break; } } if ((encoded.size() >= 2U) && std::isalpha(static_cast(encoded[0])) && (encoded[1] == ':')) { return "file:///" + encoded; } return encoded; } std::string csv_spool_path(const std::string& csv_path) { return replace_extension(csv_path, ".capture_spool.bin"); } 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_data_script(const std::string& path, const std::string& data_json) { write_text_file(path, "window.e502LiveData = " + data_json + ";\n"); } void write_live_html_document(const std::string& path, const std::string& data_script_url) { 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" << "
\n" << " Auto-refresh every 500 ms\n" << "
\n" << "
\n" << "
Open this page once and leave it open. It refreshes its data script to pick up a rolling continuous packet window.
\n" << "
\n" << " \n" << " \n" << " \n" << " \n" << " \n" << " \n" << " \n" << " \n" << " Wheel: X zoom, Shift+wheel: Y zoom, Left/Right buttons or arrow keys: pan X, double-click: reset. The live view shows a rolling continuous packet window; CSV stores all samples.\n" << "
\n" << " \n" << "
\n" << " CH1\n" << " CH2\n" << " sqrt(CH1^2 + CH2^2)\n" << " DI1\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, bool full_res_live, std::size_t live_history_packets, bool di1_group_average) : 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)), full_res_live_(full_res_live), live_history_packets_(live_history_packets), di1_group_average_(di1_group_average) {} 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 { finalize_csv_from_spool(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"; const std::string live_script_path = replace_extension(live_json_path_, ".js"); write_text_file(live_json_path_, waiting_json); write_live_data_script(live_script_path, waiting_json); write_live_html_document(live_html_path_, path_to_script_url(live_script_path)); } void CaptureFileWriter::initialize_csv(std::size_t channel_count, bool has_di1_trace) const { std::ofstream csv_file(csv_path_, std::ios::binary | std::ios::trunc); if (!csv_file) { fail_write("Cannot prepare CSV output file: " + csv_path_); } csv_file << ""; const std::string spool_path = csv_spool_path(csv_path_); std::ofstream spool(spool_path, std::ios::binary | std::ios::trunc); if (!spool) { fail_write("Cannot open CSV spool for writing: " + spool_path); } const CsvSpoolFileHeader header{ kCsvSpoolMagic, kCsvSpoolVersion, static_cast(channel_count), static_cast(has_di1_trace ? 1U : 0U) }; spool.write(reinterpret_cast(&header), sizeof(header)); if (!spool) { fail_write("Cannot initialize CSV spool: " + spool_path); } } void CaptureFileWriter::append_csv_packet(const CapturePacket& packet, double frame_freq_hz, std::size_t& global_frame_index) const { (void) frame_freq_hz; (void) global_frame_index; const std::string spool_path = csv_spool_path(csv_path_); std::ofstream spool(spool_path, std::ios::binary | std::ios::app); if (!spool) { fail_write("Cannot open CSV spool for appending: " + spool_path); } const std::size_t frames = packet_frame_count(packet); const std::size_t channel_count = packet_channel_count(packet); const CsvSpoolPacketHeader header{ static_cast(packet.packet_index), static_cast(channel_count), static_cast(frames), static_cast(packet_has_di1_trace(packet) ? 1U : 0U) }; spool.write(reinterpret_cast(&header), sizeof(header)); if (frames != 0U) { spool.write(reinterpret_cast(packet.ch1.data()), static_cast(frames * sizeof(double))); if (channel_count >= 2U) { spool.write(reinterpret_cast(packet.ch2.data()), static_cast(frames * sizeof(double))); } if (packet_has_di1_trace(packet)) { std::vector di1_bytes(frames); for (std::size_t i = 0; i < frames; ++i) { di1_bytes[i] = packet.di1[i] ? 1U : 0U; } spool.write(reinterpret_cast(di1_bytes.data()), static_cast(frames * sizeof(uint8_t))); } } if (!spool) { fail_write("Cannot append packet to CSV spool: " + spool_path); } } void CaptureFileWriter::update_live_plot(const std::deque& packets, 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 { if (packets.empty()) { return; } const std::size_t begin_index = ((live_history_packets_ != 0U) && (packets.size() > live_history_packets_)) ? (packets.size() - live_history_packets_) : 0U; const CapturePacket& latest_packet = packets.back(); const std::size_t latest_frames = packet_frame_count(latest_packet); const double latest_duration_ms = (latest_frames == 0U) ? 0.0 : (1000.0 * static_cast(latest_frames) / frame_freq_hz); const double zeroed_percent = (stored_samples == 0U) ? 0.0 : (100.0 * static_cast(zeroed_samples) / static_cast(stored_samples)); std::size_t total_frames = 0; std::vector> packet_markers; std::vector ch1_values; std::vector ch2_values; std::vector di1_values; for (std::size_t i = begin_index; i < packets.size(); ++i) { total_frames += packet_frame_count(packets[i]); } ch1_values.reserve(total_frames); if (packet_has_ch2(latest_packet)) { ch2_values.reserve(total_frames); } if (packet_has_di1_trace(latest_packet)) { di1_values.reserve(total_frames); } std::size_t frame_offset = 0; bool first_packet = true; for (std::size_t i = begin_index; i < packets.size(); ++i) { const CapturePacket& packet = packets[i]; const std::size_t frames = packet_frame_count(packet); if (frames == 0U) { continue; } if (!first_packet) { packet_markers.push_back({frame_offset, packet.packet_index}); } first_packet = false; ch1_values.insert(ch1_values.end(), packet.ch1.begin(), packet.ch1.begin() + static_cast(frames)); if (packet_has_ch2(packet)) { ch2_values.insert(ch2_values.end(), packet.ch2.begin(), packet.ch2.begin() + static_cast(frames)); } if (packet_has_di1_trace(packet)) { di1_values.insert(di1_values.end(), packet.di1.begin(), packet.di1.begin() + static_cast(frames)); } frame_offset += frames; } const double duration_ms = (total_frames == 0U) ? 0.0 : (1000.0 * static_cast(total_frames) / frame_freq_hz); std::vector rss_values; if (packet_has_ch2(latest_packet)) { rss_values.reserve(total_frames); for (std::size_t i = 0; i < total_frames; ++i) { const double ch1 = ch1_values[i]; const double ch2 = ch2_values[i]; rss_values.push_back(std::sqrt((ch1 * ch1) + (ch2 * ch2))); } } const auto trace1 = build_min_max_trace(ch1_values, kLivePlotMaxColumns); const auto trace2 = build_min_max_trace(ch2_values, kLivePlotMaxColumns); const auto rss_trace = build_min_max_trace(rss_values, kLivePlotMaxColumns); const auto di1_trace = build_digital_step_trace(di1_values); const bool di1_grouped = di1_group_average_ && !di1_values.empty(); const auto grouped_traces = di1_grouped ? build_di1_grouped_traces(ch1_values, ch2_values, di1_values, packet_has_ch2(latest_packet)) : Di1GroupedTraces{}; const auto trace1_mid = full_res_live_ ? build_min_max_trace(ch1_values, kLivePlotMidColumns) : std::vector{}; const auto trace2_mid = full_res_live_ ? build_min_max_trace(ch2_values, kLivePlotMidColumns) : std::vector{}; const auto rss_trace_mid = full_res_live_ ? build_min_max_trace(rss_values, kLivePlotMidColumns) : std::vector{}; const auto trace1_high = full_res_live_ ? ((total_frames <= kLivePlotRawFrameLimit) ? build_full_trace(ch1_values) : build_min_max_trace(ch1_values, kLivePlotHighColumns)) : std::vector{}; const auto trace2_high = full_res_live_ ? ((total_frames <= kLivePlotRawFrameLimit) ? build_full_trace(ch2_values) : build_min_max_trace(ch2_values, kLivePlotHighColumns)) : std::vector{}; const auto rss_trace_high = full_res_live_ ? ((total_frames <= kLivePlotRawFrameLimit) ? build_full_trace(rss_values) : build_min_max_trace(rss_values, kLivePlotHighColumns)) : std::vector{}; std::ostringstream json; json << std::fixed << std::setprecision(9); json << "{\n" << " \"status\": \"packet\",\n" << " \"packetIndex\": " << latest_packet.packet_index << ",\n" << " \"firstPacketIndex\": " << packets[begin_index].packet_index << ",\n" << " \"lastPacketIndex\": " << latest_packet.packet_index << ",\n" << " \"windowPacketCount\": " << (packets.size() - begin_index) << ",\n" << " \"channelCount\": " << packet_channel_count(latest_packet) << ",\n" << " \"packetsSeen\": " << packets_seen << ",\n" << " \"packetsPerSecond\": " << packets_per_second << ",\n" << " \"framesPerChannel\": " << total_frames << ",\n" << " \"latestPacketFrames\": " << latest_frames << ",\n" << " \"frameFreqHz\": " << frame_freq_hz << ",\n" << " \"durationMs\": " << duration_ms << ",\n" << " \"latestPacketDurationMs\": " << latest_duration_ms << ",\n" << " \"closeReason\": \"" << close_reason << "\",\n" << " \"zeroedSamples\": " << zeroed_samples << ",\n" << " \"storedSamples\": " << stored_samples << ",\n" << " \"zeroedPercent\": " << zeroed_percent << ",\n" << " \"fullResLive\": " << (full_res_live_ ? "true" : "false") << ",\n" << " \"di1Grouped\": " << (di1_grouped ? "true" : "false") << ",\n" << " \"groupedPointCount\": " << grouped_traces.ch1.size() << ",\n" << " \"hasDi1Trace\": " << (packet_has_di1_trace(latest_packet) ? "true" : "false") << ",\n" << " \"di1Frames\": " << di1_values.size() << ",\n" << " \"updatedAt\": \"packet " << latest_packet.packet_index << "\",\n" << " \"ch1\": " << json_points(trace1, frame_freq_hz) << ",\n" << " \"ch1Mid\": " << json_points(trace1_mid, frame_freq_hz) << ",\n" << " \"ch1High\": " << json_points(trace1_high, frame_freq_hz) << ",\n" << " \"ch2\": " << json_points(trace2, frame_freq_hz) << ",\n" << " \"ch2Mid\": " << json_points(trace2_mid, frame_freq_hz) << ",\n" << " \"ch2High\": " << json_points(trace2_high, frame_freq_hz) << ",\n" << " \"rss\": " << json_points(rss_trace, frame_freq_hz) << ",\n" << " \"rssMid\": " << json_points(rss_trace_mid, frame_freq_hz) << ",\n" << " \"rssHigh\": " << json_points(rss_trace_high, frame_freq_hz) << ",\n" << " \"ch1Grouped\": " << json_points(grouped_traces.ch1, frame_freq_hz) << ",\n" << " \"ch2Grouped\": " << json_points(grouped_traces.ch2, frame_freq_hz) << ",\n" << " \"rssGrouped\": " << json_points(grouped_traces.rss, frame_freq_hz) << ",\n" << " \"di1\": " << json_points(di1_trace, frame_freq_hz) << ",\n" << " \"packetMarkers\": " << json_packet_markers(packet_markers, frame_freq_hz) << "\n" << "}\n"; const std::string json_text = json.str(); write_text_file(live_json_path_, json_text); write_live_data_script(replace_extension(live_json_path_, ".js"), json_text); } void CaptureFileWriter::finalize_csv_from_spool(double frame_freq_hz) const { const std::string spool_path = csv_spool_path(csv_path_); std::ifstream spool(spool_path, std::ios::binary); if (!spool) { fail_write("Cannot open CSV spool for reading: " + spool_path); } CsvSpoolFileHeader file_header{}; spool.read(reinterpret_cast(&file_header), sizeof(file_header)); if (!spool || (file_header.magic != kCsvSpoolMagic) || (file_header.version != kCsvSpoolVersion)) { fail_write("Invalid CSV spool format: " + spool_path); } std::ofstream csv(csv_path_, std::ios::binary | std::ios::trunc); if (!csv) { fail_write("Cannot open CSV for final writing: " + csv_path_); } bool header_written = false; std::size_t global_frame_index = 0; while (true) { CsvSpoolPacketHeader packet_header{}; spool.read(reinterpret_cast(&packet_header), sizeof(packet_header)); if (!spool) { if (spool.eof()) { break; } fail_write("Cannot read packet header from CSV spool: " + spool_path); } const std::size_t packet_index = static_cast(packet_header.packet_index); const std::size_t channel_count = static_cast(packet_header.channel_count); const std::size_t frames = static_cast(packet_header.frame_count); const bool has_di1_trace = packet_header.has_di1_trace != 0U; if (!header_written) { csv << "packet_index,frame_index,global_frame_index,time_s,packet_time_s,ch1_v"; if (channel_count >= 2U) { csv << ",ch2_v,rss_v"; } if (has_di1_trace || (file_header.has_di1_trace != 0U)) { csv << ",di1"; } csv << "\n"; header_written = true; } std::vector ch1(frames); std::vector ch2((channel_count >= 2U) ? frames : 0U); std::vector di1(has_di1_trace ? frames : 0U); if (frames != 0U) { spool.read(reinterpret_cast(ch1.data()), static_cast(frames * sizeof(double))); if (!spool) { fail_write("Cannot read CH1 data from CSV spool: " + spool_path); } if (channel_count >= 2U) { spool.read(reinterpret_cast(ch2.data()), static_cast(frames * sizeof(double))); if (!spool) { fail_write("Cannot read CH2 data from CSV spool: " + spool_path); } } if (has_di1_trace) { spool.read(reinterpret_cast(di1.data()), static_cast(frames * sizeof(uint8_t))); if (!spool) { fail_write("Cannot read DI1 trace from CSV spool: " + spool_path); } } } std::ostringstream out; out << std::fixed << std::setprecision(9); 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; out << packet_index << "," << i << "," << global_frame_index << "," << time_s << "," << packet_time_s << "," << ch1[i]; if (channel_count >= 2U) { const double rss = std::sqrt((ch1[i] * ch1[i]) + (ch2[i] * ch2[i])); out << "," << ch2[i] << "," << rss; } if (has_di1_trace || (file_header.has_di1_trace != 0U)) { out << "," << (has_di1_trace ? static_cast(di1[i]) : 0U); } out << "\n"; ++global_frame_index; } csv << out.str(); } if (!header_written) { csv << "packet_index,frame_index,global_frame_index,time_s,packet_time_s,ch1_v"; if (file_header.channel_count >= 2U) { csv << ",ch2_v,rss_v"; } if (file_header.has_di1_trace != 0U) { csv << ",di1"; } csv << "\n"; } csv.flush(); std::remove(spool_path.c_str()); } 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(); const std::size_t channel_count = packets.empty() ? 2U : packet_channel_count(packets.front()); const bool has_di1_trace = std::any_of(packets.begin(), packets.end(), [](const CapturePacket& packet) { return packet_has_di1_trace(packet); }); 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]); if (packet_has_ch2(packet)) { min_y = std::min(min_y, packet.ch2[i]); max_y = std::max(max_y, packet.ch2[i]); const double rss = std::sqrt((packet.ch1[i] * packet.ch1[i]) + (packet.ch2[i] * packet.ch2[i])); min_y = std::min(min_y, rss); max_y = std::max(max_y, rss); } } } 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; const double di1_band_top = top + 10.0; const double di1_band_height = 44.0; 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"; } if (has_di1_trace) { file << " \n"; file << " \n"; file << " DI1\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); const auto rss_values = build_rss_values(packet); const auto trace_rss = build_min_max_trace(rss_values, 1200); const bool di1_grouped = di1_group_average_ && packet_has_di1_trace(packet); const auto grouped_traces = di1_grouped ? build_di1_grouped_traces(packet.ch1, packet.ch2, packet.di1, packet_has_ch2(packet)) : Di1GroupedTraces{}; const auto trace_di1 = build_digital_step_trace(packet.di1); const auto& plot_trace1 = di1_grouped ? grouped_traces.ch1 : trace1; const auto& plot_trace2 = di1_grouped ? grouped_traces.ch2 : trace2; const auto& plot_trace_rss = di1_grouped ? grouped_traces.rss : trace_rss; file << " \n"; if (packet_has_ch2(packet)) { file << " \n"; file << " \n"; } if (packet_has_di1_trace(packet)) { file << " \n"; } if (packets.size() <= 24U) { file << " P" << packet.packet_index << "\n"; } frame_offset += frames; } file << " E-502 capture: " << packets.size() << " packet(s), " << ((channel_count >= 2U) ? "CH1 and CH2" : "CH1 only"); if (has_di1_trace) { file << ", DI1 trace"; } if (di1_group_average_ && has_di1_trace) { file << ", averaged by DI1 runs"; } file << "\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"; if (channel_count >= 2U) { file << " \n"; file << " CH2\n"; file << " \n"; file << " sqrt(CH1^2+CH2^2)\n"; } if (has_di1_trace) { file << " \n"; file << " DI1\n"; } file << "\n"; }