#include "capture_file_writer.h" #include #include #include #include #include #include #include #include namespace { struct PlotPoint { std::size_t sample_index = 0; double value = 0.0; }; std::size_t packet_frame_count(const CapturePacket& packet) { return std::min(packet.ch1.size(), packet.ch2.size()); } std::vector build_min_max_trace(const std::vector& data, std::size_t max_columns) { std::vector result; if (data.empty()) { return result; } const std::size_t bucket_size = std::max(1, (data.size() + max_columns - 1) / max_columns); result.reserve(max_columns * 2); for (std::size_t begin = 0; begin < data.size(); begin += bucket_size) { const std::size_t end = std::min(begin + bucket_size, data.size()); std::size_t min_index = begin; std::size_t max_index = begin; for (std::size_t i = begin + 1; i < end; ++i) { if (data[i] < data[min_index]) { min_index = i; } if (data[i] > data[max_index]) { max_index = i; } } if (min_index <= max_index) { result.push_back({min_index, data[min_index]}); if (max_index != min_index) { result.push_back({max_index, data[max_index]}); } } else { result.push_back({max_index, data[max_index]}); result.push_back({min_index, data[min_index]}); } } if (result.back().sample_index != (data.size() - 1)) { result.push_back({data.size() - 1, data.back()}); } return result; } std::string polyline_points(const std::vector& trace, std::size_t packet_start_index, std::size_t total_frames, double y_min, double y_max, double left, double top, double width, double height) { std::ostringstream out; out << std::fixed << std::setprecision(3); const double y_span = std::max(1e-12, y_max - y_min); const double x_span = (total_frames > 1U) ? static_cast(total_frames - 1U) : 1.0; for (const auto& point : trace) { const double global_index = static_cast(packet_start_index + point.sample_index); const double x = left + (global_index / x_span) * width; const double y = top + height - ((point.value - y_min) / y_span) * height; out << x << "," << y << " "; } return out.str(); } std::string json_points(const std::vector& trace, double frame_freq_hz) { std::ostringstream out; out << std::fixed << std::setprecision(9); out << "["; bool first = true; for (const auto& point : trace) { if (!first) { out << ","; } first = false; out << "[" << (static_cast(point.sample_index) / frame_freq_hz) << "," << point.value << "]"; } out << "]"; return out.str(); } [[noreturn]] void fail_write(const std::string& message); void write_text_file(const std::string& path, const std::string& text) { std::ofstream file(path, std::ios::binary); if (!file) { fail_write("Cannot open file for writing: " + path); } file << text; } void write_live_html_document(const std::string& path, const std::string& data_json) { std::ofstream html(path, std::ios::binary); if (!html) { fail_write("Cannot open live HTML for writing: " + path); } html << "\n" << "\n" << "\n" << " \n" << " \n" << " E-502 Live Plot\n" << " \n" << "\n" << "\n" << "
\n" << "
\n" << "
\n" << "
\n" << "
E-502 Live Packet Plot
\n" << "
\n" << " Waiting for packets...\n" << " \n" << " \n" << " \n" << "
\n" << "
\n" << "
\n" << " Auto-refresh every 500 ms\n" << "
\n" << "
\n" << "
Open this page once and leave it open. It reloads itself to pick up new packets.
\n" << " \n" << "
\n" << " CH1\n" << " CH2\n" << "
\n" << "
\n" << "
\n" << " \n" << "\n" << "\n"; } [[noreturn]] void fail_write(const std::string& message) { throw std::runtime_error(message); } } // namespace CaptureFileWriter::CaptureFileWriter(std::string csv_path, std::string svg_path, std::string live_html_path, std::string live_json_path) : csv_path_(std::move(csv_path)), svg_path_(std::move(svg_path)), live_html_path_(std::move(live_html_path)), live_json_path_(std::move(live_json_path)) {} const std::string& CaptureFileWriter::live_html_path() const { return live_html_path_; } const std::string& CaptureFileWriter::live_json_path() const { return live_json_path_; } void CaptureFileWriter::write(const std::vector& packets, double frame_freq_hz, double nominal_range_v) const { write_csv(packets, frame_freq_hz); write_svg(packets, frame_freq_hz, nominal_range_v); } void CaptureFileWriter::initialize_live_plot() const { const std::string waiting_json = "{\n" " \"status\": \"waiting\",\n" " \"message\": \"Waiting for first packet...\"\n" "}\n"; write_text_file(live_json_path_, waiting_json); write_live_html_document(live_html_path_, waiting_json); } void CaptureFileWriter::update_live_plot(const CapturePacket& packet, std::size_t packets_seen, double packets_per_second, double frame_freq_hz, const std::string& close_reason, std::size_t zeroed_samples, std::size_t stored_samples) const { const std::size_t frames = packet_frame_count(packet); const double duration_ms = (frames == 0U) ? 0.0 : (1000.0 * static_cast(frames) / frame_freq_hz); const double zeroed_percent = (stored_samples == 0U) ? 0.0 : (100.0 * static_cast(zeroed_samples) / static_cast(stored_samples)); const auto trace1 = build_min_max_trace(packet.ch1, 1800); const auto trace2 = build_min_max_trace(packet.ch2, 1800); std::ostringstream json; json << std::fixed << std::setprecision(9); json << "{\n" << " \"status\": \"packet\",\n" << " \"packetIndex\": " << packet.packet_index << ",\n" << " \"packetsSeen\": " << packets_seen << ",\n" << " \"packetsPerSecond\": " << packets_per_second << ",\n" << " \"framesPerChannel\": " << frames << ",\n" << " \"frameFreqHz\": " << frame_freq_hz << ",\n" << " \"durationMs\": " << duration_ms << ",\n" << " \"closeReason\": \"" << close_reason << "\",\n" << " \"zeroedSamples\": " << zeroed_samples << ",\n" << " \"storedSamples\": " << stored_samples << ",\n" << " \"zeroedPercent\": " << zeroed_percent << ",\n" << " \"updatedAt\": \"packet " << packet.packet_index << "\",\n" << " \"ch1\": " << json_points(trace1, frame_freq_hz) << ",\n" << " \"ch2\": " << json_points(trace2, frame_freq_hz) << "\n" << "}\n"; write_text_file(live_json_path_, json.str()); write_live_html_document(live_html_path_, json.str()); } void CaptureFileWriter::write_csv(const std::vector& packets, double frame_freq_hz) const { std::ofstream file(csv_path_, std::ios::binary); if (!file) { fail_write("Cannot open CSV for writing: " + csv_path_); } file << "packet_index,frame_index,global_frame_index,time_s,packet_time_s,ch1_v,ch2_v\n"; file << std::fixed << std::setprecision(9); std::size_t global_frame_index = 0; for (const auto& packet : packets) { const std::size_t frames = packet_frame_count(packet); for (std::size_t i = 0; i < frames; ++i) { const double time_s = static_cast(global_frame_index) / frame_freq_hz; const double packet_time_s = static_cast(i) / frame_freq_hz; file << packet.packet_index << "," << i << "," << global_frame_index << "," << time_s << "," << packet_time_s << "," << packet.ch1[i] << "," << packet.ch2[i] << "\n"; ++global_frame_index; } } } void CaptureFileWriter::write_svg(const std::vector& packets, double frame_freq_hz, double nominal_range_v) const { std::ofstream file(svg_path_, std::ios::binary); if (!file) { fail_write("Cannot open SVG for writing: " + svg_path_); } std::size_t total_frames = 0; double min_y = std::numeric_limits::infinity(); double max_y = -std::numeric_limits::infinity(); for (const auto& packet : packets) { const std::size_t frames = packet_frame_count(packet); total_frames += frames; for (std::size_t i = 0; i < frames; ++i) { min_y = std::min(min_y, packet.ch1[i]); max_y = std::max(max_y, packet.ch1[i]); min_y = std::min(min_y, packet.ch2[i]); max_y = std::max(max_y, packet.ch2[i]); } } if (!std::isfinite(min_y) || !std::isfinite(max_y) || (min_y == max_y)) { min_y = -nominal_range_v; max_y = nominal_range_v; } else { const double pad = std::max(0.1, (max_y - min_y) * 0.08); min_y -= pad; max_y += pad; } const double total_time_s = (total_frames > 1U) ? (static_cast(total_frames - 1U) / frame_freq_hz) : 0.0; const double width = 1400.0; const double height = 800.0; const double left = 90.0; const double right = 40.0; const double top = 40.0; const double bottom = 80.0; const double plot_w = width - left - right; const double plot_h = height - top - bottom; const double zero_y = top + plot_h - ((0.0 - min_y) / std::max(1e-12, max_y - min_y)) * plot_h; file << "\n"; file << " \n"; file << " \n"; for (int i = 0; i <= 10; ++i) { const double x = left + (plot_w * i / 10.0); const double y = top + (plot_h * i / 10.0); file << " \n"; file << " \n"; } if ((0.0 >= min_y) && (0.0 <= max_y)) { file << " \n"; } std::size_t frame_offset = 0; for (std::size_t packet_idx = 0; packet_idx < packets.size(); ++packet_idx) { const auto& packet = packets[packet_idx]; const std::size_t frames = packet_frame_count(packet); if (frames == 0U) { continue; } const double x0 = left + ((total_frames > 1U) ? (static_cast(frame_offset) / static_cast(total_frames - 1U)) * plot_w : 0.0); const double x1 = left + ((total_frames > 1U) ? (static_cast(frame_offset + frames - 1U) / static_cast(total_frames - 1U)) * plot_w : plot_w); const char* packet_fill = (packet_idx % 2U == 0U) ? "#f7fafc" : "#fdfefe"; file << " \n"; if (packet_idx != 0U) { file << " \n"; } const auto trace1 = build_min_max_trace(packet.ch1, 1200); const auto trace2 = build_min_max_trace(packet.ch2, 1200); file << " \n"; file << " \n"; if (packets.size() <= 24U) { file << " P" << packet.packet_index << "\n"; } frame_offset += frames; } file << " E-502 capture: " << packets.size() << " packet(s), CH1 and CH2\n"; file << " time, s\n"; file << " V\n"; file << std::fixed << std::setprecision(6); for (int i = 0; i <= 10; ++i) { const double x = left + (plot_w * i / 10.0); const double t = total_time_s * i / 10.0; file << " " << t << "\n"; const double y = top + plot_h - (plot_h * i / 10.0); const double v = min_y + (max_y - min_y) * i / 10.0; file << " " << v << "\n"; } const double legend_y = height - 48.0; file << " \n"; file << " CH1\n"; file << " \n"; file << " CH2\n"; file << "\n"; }