527 lines
25 KiB
C++
527 lines
25 KiB
C++
#include "capture_file_writer.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <limits>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <vector>
|
|
|
|
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 packet_has_ch2(packet) ? std::min(packet.ch1.size(), packet.ch2.size()) : packet.ch1.size();
|
|
}
|
|
|
|
std::vector<PlotPoint> build_min_max_trace(const std::vector<double>& data, std::size_t max_columns) {
|
|
std::vector<PlotPoint> result;
|
|
if (data.empty()) {
|
|
return result;
|
|
}
|
|
|
|
const std::size_t bucket_size = std::max<std::size_t>(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<PlotPoint>& 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<double>(total_frames - 1U) : 1.0;
|
|
|
|
for (const auto& point : trace) {
|
|
const double global_index = static_cast<double>(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<PlotPoint>& 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<double>(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 << "<!DOCTYPE html>\n"
|
|
<< "<html lang=\"en\">\n"
|
|
<< "<head>\n"
|
|
<< " <meta charset=\"utf-8\"/>\n"
|
|
<< " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n"
|
|
<< " <title>E-502 Live Plot</title>\n"
|
|
<< " <style>\n"
|
|
<< " :root { color-scheme: light; --bg:#f4f7fb; --panel:#ffffff; --ink:#1f2d3d; --muted:#607080; --grid:#d9e2ec; --blue:#005bbb; --red:#d62828; }\n"
|
|
<< " body { margin:0; font-family:\"Segoe UI\", Arial, sans-serif; background:linear-gradient(180deg,#eef3f8 0%,#f8fbfd 100%); color:var(--ink); }\n"
|
|
<< " .wrap { max-width:1200px; margin:0 auto; padding:24px; }\n"
|
|
<< " .panel { background:var(--panel); border:1px solid #dbe4ed; border-radius:16px; box-shadow:0 10px 30px rgba(23,43,77,0.08); overflow:hidden; }\n"
|
|
<< " .head { padding:18px 22px; border-bottom:1px solid #e6edf4; display:flex; justify-content:space-between; gap:16px; flex-wrap:wrap; }\n"
|
|
<< " .title { font-size:22px; font-weight:600; }\n"
|
|
<< " .meta { color:var(--muted); font-size:14px; display:flex; gap:18px; flex-wrap:wrap; }\n"
|
|
<< " .status { padding:14px 22px 0 22px; color:var(--muted); font-size:14px; }\n"
|
|
<< " canvas { display:block; width:100%; height:620px; background:#fbfdff; }\n"
|
|
<< " .legend { display:flex; gap:22px; padding:10px 22px 20px 22px; color:var(--muted); font-size:14px; }\n"
|
|
<< " .sw { display:inline-block; width:28px; height:3px; border-radius:2px; margin-right:8px; vertical-align:middle; }\n"
|
|
<< " </style>\n"
|
|
<< "</head>\n"
|
|
<< "<body>\n"
|
|
<< " <div class=\"wrap\">\n"
|
|
<< " <div class=\"panel\">\n"
|
|
<< " <div class=\"head\">\n"
|
|
<< " <div>\n"
|
|
<< " <div class=\"title\">E-502 Live Packet Plot</div>\n"
|
|
<< " <div class=\"meta\">\n"
|
|
<< " <span id=\"packetInfo\">Waiting for packets...</span>\n"
|
|
<< " <span id=\"timingInfo\"></span>\n"
|
|
<< " <span id=\"packetRateInfo\"></span>\n"
|
|
<< " <span id=\"zeroInfo\"></span>\n"
|
|
<< " </div>\n"
|
|
<< " </div>\n"
|
|
<< " <div class=\"meta\">\n"
|
|
<< " <span id=\"updateInfo\">Auto-refresh every 500 ms</span>\n"
|
|
<< " </div>\n"
|
|
<< " </div>\n"
|
|
<< " <div class=\"status\" id=\"statusText\">Open this page once and leave it open. It reloads itself to pick up new packets.</div>\n"
|
|
<< " <canvas id=\"plot\" width=\"1200\" height=\"620\"></canvas>\n"
|
|
<< " <div class=\"legend\">\n"
|
|
<< " <span id=\"legendCh1\"><span class=\"sw\" style=\"background:#005bbb\"></span>CH1</span>\n"
|
|
<< " <span id=\"legendCh2\"><span class=\"sw\" style=\"background:#d62828\"></span>CH2</span>\n"
|
|
<< " </div>\n"
|
|
<< " </div>\n"
|
|
<< " </div>\n"
|
|
<< " <script>\n"
|
|
<< " const liveData = " << data_json << ";\n"
|
|
<< " const canvas = document.getElementById('plot');\n"
|
|
<< " const ctx = canvas.getContext('2d');\n"
|
|
<< " const statusText = document.getElementById('statusText');\n"
|
|
<< " const packetInfo = document.getElementById('packetInfo');\n"
|
|
<< " const timingInfo = document.getElementById('timingInfo');\n"
|
|
<< " 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"
|
|
<< " ctx.fillStyle = '#fbfdff'; ctx.fillRect(0, 0, canvas.width, canvas.height);\n"
|
|
<< " ctx.fillStyle = '#ffffff'; ctx.fillRect(left, top, width, height);\n"
|
|
<< " ctx.strokeStyle = '#dbe4ed'; ctx.lineWidth = 1; ctx.strokeRect(left, top, width, height);\n"
|
|
<< " ctx.strokeStyle = '#edf2f7';\n"
|
|
<< " for (let i = 0; i <= 10; i += 1) {\n"
|
|
<< " const x = left + width * i / 10;\n"
|
|
<< " const y = top + height * i / 10;\n"
|
|
<< " ctx.beginPath(); ctx.moveTo(x, top); ctx.lineTo(x, top + height); ctx.stroke();\n"
|
|
<< " ctx.beginPath(); ctx.moveTo(left, y); ctx.lineTo(left + width, y); ctx.stroke();\n"
|
|
<< " }\n"
|
|
<< " ctx.fillStyle = '#607080'; ctx.font = '12px Segoe UI';\n"
|
|
<< " for (let i = 0; i <= 10; i += 1) {\n"
|
|
<< " const x = left + width * i / 10;\n"
|
|
<< " const t = maxT * i / 10;\n"
|
|
<< " ctx.fillText(t.toFixed(6), x - 18, top + height + 22);\n"
|
|
<< " const y = top + height - height * i / 10;\n"
|
|
<< " const v = minY + (maxY - minY) * i / 10;\n"
|
|
<< " ctx.fillText(v.toFixed(3), 8, y + 4);\n"
|
|
<< " }\n"
|
|
<< " return { left, top, width, height };\n"
|
|
<< " }\n"
|
|
<< " function drawTrace(points, color, box, minY, maxY, maxT) {\n"
|
|
<< " if (!points || points.length === 0) return;\n"
|
|
<< " const spanY = Math.max(1e-9, maxY - minY);\n"
|
|
<< " const spanT = Math.max(1e-9, maxT);\n"
|
|
<< " ctx.beginPath();\n"
|
|
<< " points.forEach((p, i) => {\n"
|
|
<< " const x = box.left + (p[0] / spanT) * box.width;\n"
|
|
<< " const y = box.top + box.height - ((p[1] - minY) / spanY) * box.height;\n"
|
|
<< " if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);\n"
|
|
<< " });\n"
|
|
<< " ctx.strokeStyle = color; ctx.lineWidth = 1.25; ctx.stroke();\n"
|
|
<< " }\n"
|
|
<< " function renderWaiting(message) {\n"
|
|
<< " packetInfo.textContent = 'Waiting for packets...';\n"
|
|
<< " 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"
|
|
<< " ctx.fillStyle = '#fbfdff'; ctx.fillRect(0, 0, canvas.width, canvas.height);\n"
|
|
<< " ctx.fillStyle = '#607080'; ctx.font = '20px Segoe UI'; ctx.fillText('Waiting for first packet...', 32, 80);\n"
|
|
<< " }\n"
|
|
<< " 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"
|
|
<< " 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"
|
|
<< " minY = -1; maxY = 1;\n"
|
|
<< " } else {\n"
|
|
<< " const pad = Math.max(0.05, (maxY - minY) * 0.08);\n"
|
|
<< " minY -= pad; maxY += pad;\n"
|
|
<< " }\n"
|
|
<< " 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"
|
|
<< " 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"
|
|
<< " updateInfo.textContent = 'Snapshot: ' + data.updatedAt;\n"
|
|
<< " statusText.textContent = 'Close reason: ' + data.closeReason + '. This page reloads itself every 500 ms.';\n"
|
|
<< " }\n"
|
|
<< " if (!liveData || liveData.status === 'waiting') {\n"
|
|
<< " renderWaiting((liveData && liveData.message) ? liveData.message : 'Waiting for first packet...');\n"
|
|
<< " } else {\n"
|
|
<< " renderPacket(liveData);\n"
|
|
<< " }\n"
|
|
<< " setTimeout(() => window.location.reload(), 500);\n"
|
|
<< " </script>\n"
|
|
<< "</body>\n"
|
|
<< "</html>\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<CapturePacket>& 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<double>(frames) / frame_freq_hz);
|
|
const double zeroed_percent = (stored_samples == 0U)
|
|
? 0.0
|
|
: (100.0 * static_cast<double>(zeroed_samples) / static_cast<double>(stored_samples));
|
|
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"
|
|
<< " \"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<CapturePacket>& 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_);
|
|
}
|
|
|
|
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;
|
|
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<double>(global_frame_index) / frame_freq_hz;
|
|
const double packet_time_s = static_cast<double>(i) / frame_freq_hz;
|
|
file << packet.packet_index << "," << i << "," << global_frame_index << ","
|
|
<< time_s << "," << packet_time_s << "," << packet.ch1[i];
|
|
if (channel_count >= 2U) {
|
|
file << "," << packet.ch2[i];
|
|
}
|
|
file << "\n";
|
|
++global_frame_index;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& 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<double>::infinity();
|
|
double max_y = -std::numeric_limits<double>::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]);
|
|
if (packet_has_ch2(packet)) {
|
|
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<double>(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 << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" << width
|
|
<< "\" height=\"" << height << "\" viewBox=\"0 0 " << width << " " << height << "\">\n";
|
|
file << " <rect width=\"100%\" height=\"100%\" fill=\"#ffffff\"/>\n";
|
|
file << " <rect x=\"" << left << "\" y=\"" << top << "\" width=\"" << plot_w
|
|
<< "\" height=\"" << plot_h << "\" fill=\"#fbfcfe\" stroke=\"#ccd4dd\"/>\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 << " <line x1=\"" << x << "\" y1=\"" << top << "\" x2=\"" << x << "\" y2=\"" << (top + plot_h)
|
|
<< "\" stroke=\"#edf1f5\" stroke-width=\"1\"/>\n";
|
|
file << " <line x1=\"" << left << "\" y1=\"" << y << "\" x2=\"" << (left + plot_w) << "\" y2=\"" << y
|
|
<< "\" stroke=\"#edf1f5\" stroke-width=\"1\"/>\n";
|
|
}
|
|
|
|
if ((0.0 >= min_y) && (0.0 <= max_y)) {
|
|
file << " <line x1=\"" << left << "\" y1=\"" << zero_y << "\" x2=\"" << (left + plot_w)
|
|
<< "\" y2=\"" << zero_y << "\" stroke=\"#8fa1b3\" stroke-width=\"1.2\"/>\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<double>(frame_offset) / static_cast<double>(total_frames - 1U)) * plot_w
|
|
: 0.0);
|
|
const double x1 = left + ((total_frames > 1U)
|
|
? (static_cast<double>(frame_offset + frames - 1U) / static_cast<double>(total_frames - 1U)) * plot_w
|
|
: plot_w);
|
|
const char* packet_fill = (packet_idx % 2U == 0U) ? "#f7fafc" : "#fdfefe";
|
|
file << " <rect x=\"" << x0 << "\" y=\"" << top << "\" width=\"" << std::max(1.0, x1 - x0)
|
|
<< "\" height=\"" << plot_h << "\" fill=\"" << packet_fill << "\" opacity=\"0.55\"/>\n";
|
|
if (packet_idx != 0U) {
|
|
file << " <line x1=\"" << x0 << "\" y1=\"" << top << "\" x2=\"" << x0 << "\" y2=\"" << (top + plot_h)
|
|
<< "\" stroke=\"#c8d2dc\" stroke-width=\"1.2\" stroke-dasharray=\"4 4\"/>\n";
|
|
}
|
|
|
|
const auto trace1 = build_min_max_trace(packet.ch1, 1200);
|
|
const auto trace2 = build_min_max_trace(packet.ch2, 1200);
|
|
file << " <polyline fill=\"none\" stroke=\"#005bbb\" stroke-width=\"1.2\" points=\""
|
|
<< polyline_points(trace1, frame_offset, total_frames, min_y, max_y, left, top, plot_w, plot_h)
|
|
<< "\"/>\n";
|
|
if (packet_has_ch2(packet)) {
|
|
file << " <polyline fill=\"none\" stroke=\"#d62828\" stroke-width=\"1.2\" points=\""
|
|
<< polyline_points(trace2, frame_offset, total_frames, min_y, max_y, left, top, plot_w, plot_h)
|
|
<< "\"/>\n";
|
|
}
|
|
|
|
if (packets.size() <= 24U) {
|
|
file << " <text x=\"" << (x0 + 6.0) << "\" y=\"" << (top + 18.0)
|
|
<< "\" font-size=\"12\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#5c6f82\">P"
|
|
<< packet.packet_index << "</text>\n";
|
|
}
|
|
|
|
frame_offset += frames;
|
|
}
|
|
|
|
file << " <text x=\"" << left << "\" y=\"24\" font-size=\"22\" font-family=\"Segoe UI, Arial, sans-serif\""
|
|
<< " fill=\"#203040\">E-502 capture: " << packets.size() << " packet(s), "
|
|
<< ((channel_count >= 2U) ? "CH1 and CH2" : "CH1 only") << "</text>\n";
|
|
file << " <text x=\"" << left << "\" y=\"" << (height - 22)
|
|
<< "\" font-size=\"16\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#425466\">time, s</text>\n";
|
|
file << " <text x=\"18\" y=\"" << (top + 16)
|
|
<< "\" font-size=\"16\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#425466\">V</text>\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 << " <text x=\"" << x << "\" y=\"" << (top + plot_h + 24)
|
|
<< "\" text-anchor=\"middle\" font-size=\"12\" font-family=\"Segoe UI, Arial, sans-serif\""
|
|
<< " fill=\"#607080\">" << t << "</text>\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 << " <text x=\"" << (left - 10) << "\" y=\"" << (y + 4)
|
|
<< "\" text-anchor=\"end\" font-size=\"12\" font-family=\"Segoe UI, Arial, sans-serif\""
|
|
<< " fill=\"#607080\">" << v << "</text>\n";
|
|
}
|
|
|
|
const double legend_y = height - 48.0;
|
|
file << " <line x1=\"" << (width - 270) << "\" y1=\"" << legend_y
|
|
<< "\" x2=\"" << (width - 230) << "\" y2=\"" << legend_y
|
|
<< "\" stroke=\"#005bbb\" stroke-width=\"3\"/>\n";
|
|
file << " <text x=\"" << (width - 220) << "\" y=\"" << (legend_y + 4)
|
|
<< "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">CH1</text>\n";
|
|
if (channel_count >= 2U) {
|
|
file << " <line x1=\"" << (width - 160) << "\" y1=\"" << legend_y
|
|
<< "\" x2=\"" << (width - 120) << "\" y2=\"" << legend_y
|
|
<< "\" stroke=\"#d62828\" stroke-width=\"3\"/>\n";
|
|
file << " <text x=\"" << (width - 110) << "\" y=\"" << (legend_y + 4)
|
|
<< "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">CH2</text>\n";
|
|
}
|
|
file << "</svg>\n";
|
|
}
|