Reduce capture-path I/O to avoid ADC+DIN overflow

This commit is contained in:
kamil
2026-04-09 01:22:33 +03:00
parent b5a76d1f7c
commit 8710ed8b39
3 changed files with 347 additions and 62 deletions

View File

@ -1,6 +1,9 @@
#include "capture_file_writer.h"
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdint>
#include <cmath>
#include <fstream>
#include <iomanip>
@ -12,6 +15,21 @@
namespace {
constexpr std::size_t kLivePlotMaxColumns = 800;
constexpr uint32_t kCsvSpoolMagic = 0x4C564353U; // "SVCL"
constexpr uint32_t kCsvSpoolVersion = 1U;
struct CsvSpoolFileHeader {
uint32_t magic = kCsvSpoolMagic;
uint32_t version = kCsvSpoolVersion;
uint32_t channel_count = 0;
uint32_t reserved = 0;
};
struct CsvSpoolPacketHeader {
uint64_t packet_index = 0;
uint64_t channel_count = 0;
uint64_t frame_count = 0;
};
struct PlotPoint {
std::size_t sample_index = 0;
@ -112,6 +130,54 @@ std::string json_points(const std::vector<PlotPoint>& trace, double frame_freq_h
[[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<unsigned char>(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) {
@ -120,7 +186,11 @@ void write_text_file(const std::string& path, const std::string& text) {
file << text;
}
void write_live_html_document(const std::string& path, const std::string& data_json) {
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);
@ -170,7 +240,7 @@ void write_live_html_document(const std::string& path, const std::string& data_j
<< " <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"
<< " <div class=\"status\" id=\"statusText\">Open this page once and leave it open. It refreshes its data script to pick up new packets.</div>\n"
<< " <div class=\"controls\">\n"
<< " <button type=\"button\" id=\"zoomXIn\">X+</button>\n"
<< " <button type=\"button\" id=\"zoomXOut\">X-</button>\n"
@ -188,7 +258,7 @@ void write_live_html_document(const std::string& path, const std::string& data_j
<< " </div>\n"
<< " </div>\n"
<< " <script>\n"
<< " const liveData = " << data_json << ";\n"
<< " const dataScriptUrl = '" << data_script_url << "';\n"
<< " const canvas = document.getElementById('plot');\n"
<< " const ctx = canvas.getContext('2d');\n"
<< " const statusText = document.getElementById('statusText');\n"
@ -341,7 +411,7 @@ void write_live_html_document(const std::string& path, const std::string& data_j
<< " packetRateInfo.textContent = '';\n"
<< " legendCh2.style.display = '';\n"
<< " refreshAutoCheckbox();\n"
<< " updateInfo.textContent = 'Auto-refresh every 500 ms';\n"
<< " updateInfo.textContent = 'Polling 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"
@ -377,7 +447,30 @@ void write_live_html_document(const std::string& path, const std::string& data_j
<< " zeroInfo.textContent = '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 reloads itself every 500 ms.';\n"
<< " statusText.textContent = 'Close reason: ' + data.closeReason + '. This page refreshes its data every 500 ms.';\n"
<< " }\n"
<< " function applyLiveData(data) {\n"
<< " if (!data || data.status === 'waiting') {\n"
<< " renderWaiting((data && data.message) ? data.message : 'Waiting for first packet...');\n"
<< " } else {\n"
<< " renderPacket(data);\n"
<< " }\n"
<< " }\n"
<< " function loadLatestData() {\n"
<< " const oldScript = document.getElementById('liveDataScript');\n"
<< " if (oldScript) oldScript.remove();\n"
<< " window.e502LiveData = null;\n"
<< " const script = document.createElement('script');\n"
<< " script.id = 'liveDataScript';\n"
<< " const sep = dataScriptUrl.includes('?') ? '&' : '?';\n"
<< " script.src = dataScriptUrl + sep + 'ts=' + Date.now();\n"
<< " script.onload = () => {\n"
<< " applyLiveData(window.e502LiveData);\n"
<< " };\n"
<< " script.onerror = () => {\n"
<< " statusText.textContent = 'Cannot load live data script: ' + dataScriptUrl;\n"
<< " };\n"
<< " document.head.appendChild(script);\n"
<< " }\n"
<< " zoomXIn.addEventListener('click', () => applyZoomX(0.5));\n"
<< " zoomXOut.addEventListener('click', () => applyZoomX(2.0));\n"
@ -405,12 +498,9 @@ void write_live_html_document(const std::string& path, const std::string& data_j
<< " }\n"
<< " }, { passive: false });\n"
<< " canvas.addEventListener('dblclick', () => resetView());\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"
<< " renderWaiting('Waiting for first packet...');\n"
<< " loadLatestData();\n"
<< " window.setInterval(loadLatestData, 500);\n"
<< " </script>\n"
<< "</body>\n"
<< "</html>\n";
@ -442,7 +532,7 @@ const std::string& CaptureFileWriter::live_json_path() const {
void CaptureFileWriter::write(const std::vector<CapturePacket>& packets,
double frame_freq_hz,
double nominal_range_v) const {
write_csv(packets, frame_freq_hz);
finalize_csv_from_spool(frame_freq_hz);
write_svg(packets, frame_freq_hz, nominal_range_v);
}
@ -452,8 +542,64 @@ void CaptureFileWriter::initialize_live_plot() const {
" \"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_html_document(live_html_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) const {
(void) channel_count;
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<uint32_t>(channel_count), 0U};
spool.write(reinterpret_cast<const char*>(&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<uint64_t>(packet.packet_index),
static_cast<uint64_t>(channel_count),
static_cast<uint64_t>(frames)
};
spool.write(reinterpret_cast<const char*>(&header), sizeof(header));
if (frames != 0U) {
spool.write(reinterpret_cast<const char*>(packet.ch1.data()),
static_cast<std::streamsize>(frames * sizeof(double)));
if (channel_count >= 2U) {
spool.write(reinterpret_cast<const char*>(packet.ch2.data()),
static_cast<std::streamsize>(frames * sizeof(double)));
}
}
if (!spool) {
fail_write("Cannot append packet to CSV spool: " + spool_path);
}
}
void CaptureFileWriter::update_live_plot(const CapturePacket& packet,
@ -491,40 +637,96 @@ void CaptureFileWriter::update_live_plot(const CapturePacket& packet,
<< " \"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());
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::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_);
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);
}
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";
CsvSpoolFileHeader file_header{};
spool.read(reinterpret_cast<char*>(&file_header), sizeof(file_header));
if (!spool || (file_header.magic != kCsvSpoolMagic) || (file_header.version != kCsvSpoolVersion)) {
fail_write("Invalid CSV spool format: " + spool_path);
}
file << "\n";
file << std::fixed << std::setprecision(9);
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;
for (const auto& packet : packets) {
const std::size_t frames = packet_frame_count(packet);
while (true) {
CsvSpoolPacketHeader packet_header{};
spool.read(reinterpret_cast<char*>(&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<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);
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";
}
csv << "\n";
header_written = true;
}
std::vector<double> ch1(frames);
std::vector<double> ch2((channel_count >= 2U) ? frames : 0U);
if (frames != 0U) {
spool.read(reinterpret_cast<char*>(ch1.data()), static_cast<std::streamsize>(frames * sizeof(double)));
if (!spool) {
fail_write("Cannot read CH1 data from CSV spool: " + spool_path);
}
if (channel_count >= 2U) {
spool.read(reinterpret_cast<char*>(ch2.data()), static_cast<std::streamsize>(frames * sizeof(double)));
if (!spool) {
fail_write("Cannot read CH2 data 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<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];
out << packet_index << "," << i << "," << global_frame_index << ","
<< time_s << "," << packet_time_s << "," << ch1[i];
if (channel_count >= 2U) {
file << "," << packet.ch2[i];
out << "," << ch2[i];
}
file << "\n";
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";
}
csv << "\n";
}
csv.flush();
std::remove(spool_path.c_str());
}
void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,