Reduce capture-path I/O to avoid ADC+DIN overflow
This commit is contained in:
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user