govnovod 5

This commit is contained in:
kamil
2026-04-09 12:04:50 +03:00
parent 8710ed8b39
commit 887e2f6730
35 changed files with 200516 additions and 1000059 deletions

View File

@ -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<PlotPoint> build_min_max_trace(const std::vector<double>& data, std::size_t max_columns) {
@ -88,6 +97,32 @@ std::vector<PlotPoint> build_min_max_trace(const std::vector<double>& data, std:
return result;
}
std::vector<PlotPoint> build_digital_step_trace(const std::vector<uint8_t>& data) {
std::vector<PlotPoint> result;
if (data.empty()) {
return result;
}
result.reserve(std::min<std::size_t>(data.size() * 2U, 16384U));
uint8_t previous = data.front();
result.push_back({0U, static_cast<double>(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<double>(previous)});
result.push_back({i, static_cast<double>(current)});
previous = current;
}
}
if (result.back().sample_index != (data.size() - 1U)) {
result.push_back({data.size() - 1U, static_cast<double>(data.back())});
}
return result;
}
std::string polyline_points(const std::vector<PlotPoint>& trace,
std::size_t packet_start_index,
std::size_t total_frames,
@ -112,6 +147,27 @@ std::string polyline_points(const std::vector<PlotPoint>& trace,
return out.str();
}
std::string digital_polyline_points(const std::vector<PlotPoint>& 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<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 - ((0.15 + 0.7 * point.value) * 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);
@ -203,7 +259,7 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " <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"
<< " :root { color-scheme: light; --bg:#f4f7fb; --panel:#ffffff; --ink:#1f2d3d; --muted:#607080; --grid:#d9e2ec; --blue:#005bbb; --red:#d62828; --green:#1b8f3a; --orange:#ee8a12; }\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"
@ -254,6 +310,7 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " <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"
<< " <span id=\"legendDi1\"><span class=\"sw\" style=\"background:#1b8f3a\"></span>DI1</span>\n"
<< " </div>\n"
<< " </div>\n"
<< " </div>\n"
@ -269,6 +326,7 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " const packetRateInfo = document.getElementById('packetRateInfo');\n"
<< " const updateInfo = document.getElementById('updateInfo');\n"
<< " const legendCh2 = document.getElementById('legendCh2');\n"
<< " const legendDi1 = document.getElementById('legendDi1');\n"
<< " const zoomXIn = document.getElementById('zoomXIn');\n"
<< " const zoomXOut = document.getElementById('zoomXOut');\n"
<< " const zoomYIn = document.getElementById('zoomYIn');\n"
@ -403,6 +461,21 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " });\n"
<< " ctx.strokeStyle = color; ctx.lineWidth = 1.25; ctx.stroke();\n"
<< " }\n"
<< " function drawDigitalTrace(points, box, xMin, xMax) {\n"
<< " if (!points || points.length === 0) return;\n"
<< " const spanT = Math.max(1e-9, xMax - xMin);\n"
<< " const band = { left: box.left, top: box.top + 10, width: box.width, height: 44 };\n"
<< " ctx.fillStyle = 'rgba(27,143,58,0.06)'; ctx.fillRect(band.left, band.top, band.width, band.height);\n"
<< " ctx.strokeStyle = 'rgba(27,143,58,0.20)'; ctx.strokeRect(band.left, band.top, band.width, band.height);\n"
<< " ctx.fillStyle = '#1b8f3a'; ctx.font = '12px Segoe UI'; ctx.fillText('DI1', band.left + 6, band.top + 14);\n"
<< " ctx.beginPath();\n"
<< " points.forEach((p, i) => {\n"
<< " const x = band.left + ((p[0] - xMin) / spanT) * band.width;\n"
<< " const y = band.top + band.height - ((0.15 + 0.7 * p[1]) * band.height);\n"
<< " if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);\n"
<< " });\n"
<< " ctx.strokeStyle = '#1b8f3a'; ctx.lineWidth = 1.4; ctx.stroke();\n"
<< " }\n"
<< " function renderWaiting(message) {\n"
<< " packetInfo.textContent = 'Waiting for packets...';\n"
<< " timingInfo.textContent = '';\n"
@ -410,6 +483,7 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " zoomInfo.textContent = 'View: auto';\n"
<< " packetRateInfo.textContent = '';\n"
<< " legendCh2.style.display = '';\n"
<< " legendDi1.style.display = 'none';\n"
<< " refreshAutoCheckbox();\n"
<< " updateInfo.textContent = 'Polling every 500 ms';\n"
<< " statusText.textContent = message;\n"
@ -421,7 +495,9 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " latestPacket = data;\n"
<< " const ch1 = data.ch1 || [];\n"
<< " const ch2 = data.ch2 || [];\n"
<< " const di1 = data.di1 || [];\n"
<< " const hasCh2 = (data.channelCount || 2) > 1 && ch2.length > 0;\n"
<< " const hasDi1Trace = !!data.hasDi1Trace && di1.length > 0;\n"
<< " const values = [];\n"
<< " ch1.forEach(p => values.push(p[1]));\n"
<< " if (hasCh2) ch2.forEach(p => values.push(p[1]));\n"
@ -439,12 +515,16 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " const box = drawAxes(view.yMin, view.yMax, view.xMin, view.xMax);\n"
<< " drawTrace(ch1, '#005bbb', box, view.yMin, view.yMax, view.xMin, view.xMax);\n"
<< " if (hasCh2) drawTrace(ch2, '#d62828', box, view.yMin, view.yMax, view.xMin, view.xMax);\n"
<< " if (hasDi1Trace) drawDigitalTrace(di1, box, view.xMin, view.xMax);\n"
<< " legendCh2.style.display = hasCh2 ? '' : 'none';\n"
<< " legendDi1.style.display = hasDi1Trace ? '' : 'none';\n"
<< " refreshAutoCheckbox();\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"
<< " zeroInfo.textContent = hasDi1Trace\n"
<< " ? ('DI1 trace: ' + data.di1Frames + ' frame samples')\n"
<< " : ('Zeroed on DI1 change: ' + data.zeroedPercent.toFixed(3) + '% (' + data.zeroedSamples + '/' + data.storedSamples + ')');\n"
<< " zoomInfo.textContent = viewState.auto ? 'View: auto' : ('View: X ' + view.xMin.toFixed(6) + '..' + view.xMax.toFixed(6) + ' s, Y ' + view.yMin.toFixed(3) + '..' + view.yMax.toFixed(3) + ' V');\n"
<< " updateInfo.textContent = 'Snapshot: ' + data.updatedAt;\n"
<< " statusText.textContent = 'Close reason: ' + data.closeReason + '. This page refreshes its data every 500 ms.';\n"
@ -548,9 +628,7 @@ void CaptureFileWriter::initialize_live_plot() const {
write_live_html_document(live_html_path_, path_to_script_url(live_script_path));
}
void CaptureFileWriter::initialize_csv(std::size_t channel_count) const {
(void) channel_count;
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_);
@ -562,7 +640,12 @@ void CaptureFileWriter::initialize_csv(std::size_t channel_count) const {
if (!spool) {
fail_write("Cannot open CSV spool for writing: " + spool_path);
}
const CsvSpoolFileHeader header{kCsvSpoolMagic, kCsvSpoolVersion, static_cast<uint32_t>(channel_count), 0U};
const CsvSpoolFileHeader header{
kCsvSpoolMagic,
kCsvSpoolVersion,
static_cast<uint32_t>(channel_count),
static_cast<uint32_t>(has_di1_trace ? 1U : 0U)
};
spool.write(reinterpret_cast<const char*>(&header), sizeof(header));
if (!spool) {
fail_write("Cannot initialize CSV spool: " + spool_path);
@ -586,7 +669,8 @@ void CaptureFileWriter::append_csv_packet(const CapturePacket& packet,
const CsvSpoolPacketHeader header{
static_cast<uint64_t>(packet.packet_index),
static_cast<uint64_t>(channel_count),
static_cast<uint64_t>(frames)
static_cast<uint64_t>(frames),
static_cast<uint64_t>(packet_has_di1_trace(packet) ? 1U : 0U)
};
spool.write(reinterpret_cast<const char*>(&header), sizeof(header));
if (frames != 0U) {
@ -596,6 +680,14 @@ void CaptureFileWriter::append_csv_packet(const CapturePacket& packet,
spool.write(reinterpret_cast<const char*>(packet.ch2.data()),
static_cast<std::streamsize>(frames * sizeof(double)));
}
if (packet_has_di1_trace(packet)) {
std::vector<uint8_t> di1_bytes(frames);
for (std::size_t i = 0; i < frames; ++i) {
di1_bytes[i] = packet.di1[i] ? 1U : 0U;
}
spool.write(reinterpret_cast<const char*>(di1_bytes.data()),
static_cast<std::streamsize>(frames * sizeof(uint8_t)));
}
}
if (!spool) {
fail_write("Cannot append packet to CSV spool: " + spool_path);
@ -616,6 +708,7 @@ void CaptureFileWriter::update_live_plot(const CapturePacket& packet,
: (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);
const auto di1_trace = build_digital_step_trace(packet.di1);
std::ostringstream json;
json << std::fixed << std::setprecision(9);
@ -632,9 +725,12 @@ void CaptureFileWriter::update_live_plot(const CapturePacket& packet,
<< " \"zeroedSamples\": " << zeroed_samples << ",\n"
<< " \"storedSamples\": " << stored_samples << ",\n"
<< " \"zeroedPercent\": " << zeroed_percent << ",\n"
<< " \"hasDi1Trace\": " << (packet_has_di1_trace(packet) ? "true" : "false") << ",\n"
<< " \"di1Frames\": " << packet.di1.size() << ",\n"
<< " \"updatedAt\": \"packet " << packet.packet_index << "\",\n"
<< " \"ch1\": " << json_points(trace1, frame_freq_hz) << ",\n"
<< " \"ch2\": " << json_points(trace2, frame_freq_hz) << "\n"
<< " \"ch2\": " << json_points(trace2, frame_freq_hz) << ",\n"
<< " \"di1\": " << json_points(di1_trace, frame_freq_hz) << "\n"
<< "}\n";
const std::string json_text = json.str();
@ -676,18 +772,23 @@ void CaptureFileWriter::finalize_csv_from_spool(double frame_freq_hz) const {
const std::size_t packet_index = static_cast<std::size_t>(packet_header.packet_index);
const std::size_t channel_count = static_cast<std::size_t>(packet_header.channel_count);
const std::size_t frames = static_cast<std::size_t>(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";
}
if (has_di1_trace || (file_header.has_di1_trace != 0U)) {
csv << ",di1";
}
csv << "\n";
header_written = true;
}
std::vector<double> ch1(frames);
std::vector<double> ch2((channel_count >= 2U) ? frames : 0U);
std::vector<uint8_t> di1(has_di1_trace ? frames : 0U);
if (frames != 0U) {
spool.read(reinterpret_cast<char*>(ch1.data()), static_cast<std::streamsize>(frames * sizeof(double)));
if (!spool) {
@ -699,6 +800,12 @@ void CaptureFileWriter::finalize_csv_from_spool(double frame_freq_hz) const {
fail_write("Cannot read CH2 data from CSV spool: " + spool_path);
}
}
if (has_di1_trace) {
spool.read(reinterpret_cast<char*>(di1.data()), static_cast<std::streamsize>(frames * sizeof(uint8_t)));
if (!spool) {
fail_write("Cannot read DI1 trace from CSV spool: " + spool_path);
}
}
}
std::ostringstream out;
@ -711,6 +818,9 @@ void CaptureFileWriter::finalize_csv_from_spool(double frame_freq_hz) const {
if (channel_count >= 2U) {
out << "," << ch2[i];
}
if (has_di1_trace || (file_header.has_di1_trace != 0U)) {
out << "," << (has_di1_trace ? static_cast<unsigned>(di1[i]) : 0U);
}
out << "\n";
++global_frame_index;
}
@ -722,6 +832,9 @@ void CaptureFileWriter::finalize_csv_from_spool(double frame_freq_hz) const {
if (file_header.channel_count >= 2U) {
csv << ",ch2_v";
}
if (file_header.has_di1_trace != 0U) {
csv << ",di1";
}
csv << "\n";
}
@ -741,6 +854,9 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
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());
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;
@ -773,6 +889,8 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
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 << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" << width
<< "\" height=\"" << height << "\" viewBox=\"0 0 " << width << " " << height << "\">\n";
@ -793,6 +911,14 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
file << " <line x1=\"" << left << "\" y1=\"" << zero_y << "\" x2=\"" << (left + plot_w)
<< "\" y2=\"" << zero_y << "\" stroke=\"#8fa1b3\" stroke-width=\"1.2\"/>\n";
}
if (has_di1_trace) {
file << " <rect x=\"" << left << "\" y=\"" << di1_band_top << "\" width=\"" << plot_w
<< "\" height=\"" << di1_band_height << "\" fill=\"#edf8f0\" opacity=\"0.85\"/>\n";
file << " <rect x=\"" << left << "\" y=\"" << di1_band_top << "\" width=\"" << plot_w
<< "\" height=\"" << di1_band_height << "\" fill=\"none\" stroke=\"#9fcdab\" stroke-width=\"1\"/>\n";
file << " <text x=\"" << (left + 8.0) << "\" y=\"" << (di1_band_top + 15.0)
<< "\" font-size=\"12\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#1b8f3a\">DI1</text>\n";
}
std::size_t frame_offset = 0;
for (std::size_t packet_idx = 0; packet_idx < packets.size(); ++packet_idx) {
@ -818,6 +944,7 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
const auto trace1 = build_min_max_trace(packet.ch1, 1200);
const auto trace2 = build_min_max_trace(packet.ch2, 1200);
const auto trace_di1 = build_digital_step_trace(packet.di1);
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";
@ -826,6 +953,11 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
<< polyline_points(trace2, frame_offset, total_frames, min_y, max_y, left, top, plot_w, plot_h)
<< "\"/>\n";
}
if (packet_has_di1_trace(packet)) {
file << " <polyline fill=\"none\" stroke=\"#1b8f3a\" stroke-width=\"1.3\" points=\""
<< digital_polyline_points(trace_di1, frame_offset, total_frames, left, di1_band_top, plot_w, di1_band_height)
<< "\"/>\n";
}
if (packets.size() <= 24U) {
file << " <text x=\"" << (x0 + 6.0) << "\" y=\"" << (top + 18.0)
@ -838,7 +970,11 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
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";
<< ((channel_count >= 2U) ? "CH1 and CH2" : "CH1 only");
if (has_di1_trace) {
file << ", DI1 trace";
}
file << "</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)
@ -872,5 +1008,12 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
file << " <text x=\"" << (width - 110) << "\" y=\"" << (legend_y + 4)
<< "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">CH2</text>\n";
}
if (has_di1_trace) {
file << " <line x1=\"" << (width - 80) << "\" y1=\"" << legend_y
<< "\" x2=\"" << (width - 40) << "\" y2=\"" << legend_y
<< "\" stroke=\"#1b8f3a\" stroke-width=\"3\"/>\n";
file << " <text x=\"" << (width - 30) << "\" y=\"" << (legend_y + 4)
<< "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">DI1</text>\n";
}
file << "</svg>\n";
}