govnovod 5
This commit is contained in:
@ -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";
|
||||
}
|
||||
|
||||
@ -8,8 +8,10 @@
|
||||
struct CapturePacket {
|
||||
std::size_t packet_index = 0;
|
||||
std::size_t channel_count = 2;
|
||||
bool has_di1_trace = false;
|
||||
std::vector<double> ch1;
|
||||
std::vector<double> ch2;
|
||||
std::vector<uint8_t> di1;
|
||||
};
|
||||
|
||||
class CaptureFileWriter {
|
||||
@ -24,7 +26,7 @@ public:
|
||||
double nominal_range_v) const;
|
||||
|
||||
void initialize_live_plot() const;
|
||||
void initialize_csv(std::size_t channel_count) const;
|
||||
void initialize_csv(std::size_t channel_count, bool has_di1_trace) const;
|
||||
void append_csv_packet(const CapturePacket& packet,
|
||||
double frame_freq_hz,
|
||||
std::size_t& global_frame_index) const;
|
||||
|
||||
332
live_plot.html
Normal file
332
live_plot.html
Normal file
@ -0,0 +1,332 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>E-502 Live Plot</title>
|
||||
<style>
|
||||
:root { color-scheme: light; --bg:#f4f7fb; --panel:#ffffff; --ink:#1f2d3d; --muted:#607080; --grid:#d9e2ec; --blue:#005bbb; --red:#d62828; --green:#1b8f3a; --orange:#ee8a12; }
|
||||
body { margin:0; font-family:"Segoe UI", Arial, sans-serif; background:linear-gradient(180deg,#eef3f8 0%,#f8fbfd 100%); color:var(--ink); }
|
||||
.wrap { max-width:1200px; margin:0 auto; padding:24px; }
|
||||
.panel { background:var(--panel); border:1px solid #dbe4ed; border-radius:16px; box-shadow:0 10px 30px rgba(23,43,77,0.08); overflow:hidden; }
|
||||
.head { padding:18px 22px; border-bottom:1px solid #e6edf4; display:flex; justify-content:space-between; gap:16px; flex-wrap:wrap; }
|
||||
.title { font-size:22px; font-weight:600; }
|
||||
.meta { color:var(--muted); font-size:14px; display:flex; gap:18px; flex-wrap:wrap; }
|
||||
.status { padding:14px 22px 0 22px; color:var(--muted); font-size:14px; }
|
||||
.controls { display:flex; gap:10px; align-items:center; flex-wrap:wrap; padding:12px 22px 0 22px; color:var(--muted); font-size:13px; }
|
||||
.controls button { border:1px solid #cfd8e3; background:#ffffff; color:#203040; border-radius:10px; padding:7px 12px; font:inherit; cursor:pointer; }
|
||||
.controls button:hover { background:#f5f8fb; }
|
||||
.controls label { display:flex; align-items:center; gap:6px; }
|
||||
.controls input { accent-color:#005bbb; }
|
||||
.hint { color:#7a8794; }
|
||||
canvas { display:block; width:100%; height:620px; background:#fbfdff; }
|
||||
.legend { display:flex; gap:22px; padding:10px 22px 20px 22px; color:var(--muted); font-size:14px; }
|
||||
.sw { display:inline-block; width:28px; height:3px; border-radius:2px; margin-right:8px; vertical-align:middle; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="panel">
|
||||
<div class="head">
|
||||
<div>
|
||||
<div class="title">E-502 Live Packet Plot</div>
|
||||
<div class="meta">
|
||||
<span id="packetInfo">Waiting for packets...</span>
|
||||
<span id="timingInfo"></span>
|
||||
<span id="packetRateInfo"></span>
|
||||
<span id="zeroInfo"></span>
|
||||
<span id="zoomInfo"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span id="updateInfo">Auto-refresh every 500 ms</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status" id="statusText">Open this page once and leave it open. It refreshes its data script to pick up new packets.</div>
|
||||
<div class="controls">
|
||||
<button type="button" id="zoomXIn">X+</button>
|
||||
<button type="button" id="zoomXOut">X-</button>
|
||||
<button type="button" id="zoomYIn">Y+</button>
|
||||
<button type="button" id="zoomYOut">Y-</button>
|
||||
<button type="button" id="resetZoom">Reset</button>
|
||||
<label><input type="checkbox" id="autoZoom" checked/>Auto view</label>
|
||||
<span class="hint">Wheel: X zoom, Shift+wheel: Y zoom, double-click: reset</span>
|
||||
</div>
|
||||
<canvas id="plot" width="1200" height="620"></canvas>
|
||||
<div class="legend">
|
||||
<span id="legendCh1"><span class="sw" style="background:#005bbb"></span>CH1</span>
|
||||
<span id="legendCh2"><span class="sw" style="background:#d62828"></span>CH2</span>
|
||||
<span id="legendDi1"><span class="sw" style="background:#1b8f3a"></span>DI1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const dataScriptUrl = 'live_plot.js';
|
||||
const canvas = document.getElementById('plot');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const statusText = document.getElementById('statusText');
|
||||
const packetInfo = document.getElementById('packetInfo');
|
||||
const timingInfo = document.getElementById('timingInfo');
|
||||
const zeroInfo = document.getElementById('zeroInfo');
|
||||
const zoomInfo = document.getElementById('zoomInfo');
|
||||
const packetRateInfo = document.getElementById('packetRateInfo');
|
||||
const updateInfo = document.getElementById('updateInfo');
|
||||
const legendCh2 = document.getElementById('legendCh2');
|
||||
const legendDi1 = document.getElementById('legendDi1');
|
||||
const zoomXIn = document.getElementById('zoomXIn');
|
||||
const zoomXOut = document.getElementById('zoomXOut');
|
||||
const zoomYIn = document.getElementById('zoomYIn');
|
||||
const zoomYOut = document.getElementById('zoomYOut');
|
||||
const resetZoom = document.getElementById('resetZoom');
|
||||
const autoZoom = document.getElementById('autoZoom');
|
||||
const viewStorageKey = 'e502_live_plot_view_v1';
|
||||
let latestPacket = null;
|
||||
let latestAutoBounds = null;
|
||||
function defaultViewState() {
|
||||
return { auto: true, xMin: null, xMax: null, yMin: null, yMax: null };
|
||||
}
|
||||
function loadViewState() {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(viewStorageKey);
|
||||
if (!raw) return defaultViewState();
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
auto: parsed.auto !== false,
|
||||
xMin: Number.isFinite(parsed.xMin) ? parsed.xMin : null,
|
||||
xMax: Number.isFinite(parsed.xMax) ? parsed.xMax : null,
|
||||
yMin: Number.isFinite(parsed.yMin) ? parsed.yMin : null,
|
||||
yMax: Number.isFinite(parsed.yMax) ? parsed.yMax : null,
|
||||
};
|
||||
} catch (err) {
|
||||
return defaultViewState();
|
||||
}
|
||||
}
|
||||
let viewState = loadViewState();
|
||||
function saveViewState() {
|
||||
try { window.localStorage.setItem(viewStorageKey, JSON.stringify(viewState)); } catch (err) {}
|
||||
}
|
||||
function clampNumber(value, min, max, fallback) {
|
||||
if (!Number.isFinite(value)) return fallback;
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
function currentViewBounds(bounds) {
|
||||
if (!bounds || viewState.auto) {
|
||||
return { xMin: 0, xMax: bounds ? bounds.maxT : 1, yMin: bounds ? bounds.minY : -1, yMax: bounds ? bounds.maxY : 1 };
|
||||
}
|
||||
const totalX = Math.max(1e-9, bounds.maxT);
|
||||
const xMin = clampNumber(viewState.xMin, 0, totalX, 0);
|
||||
const xMax = clampNumber(viewState.xMax, xMin + 1e-9, totalX, totalX);
|
||||
const yPad = Math.max(0.05, (bounds.maxY - bounds.minY) * 4.0);
|
||||
const yLimitMin = bounds.minY - yPad;
|
||||
const yLimitMax = bounds.maxY + yPad;
|
||||
const yMin = clampNumber(viewState.yMin, yLimitMin, yLimitMax - 1e-9, bounds.minY);
|
||||
const yMax = clampNumber(viewState.yMax, yMin + 1e-9, yLimitMax, bounds.maxY);
|
||||
return { xMin, xMax, yMin, yMax };
|
||||
}
|
||||
function zoomRange(min, max, factor, limitMin, limitMax) {
|
||||
const totalSpan = Math.max(1e-9, limitMax - limitMin);
|
||||
const minSpan = totalSpan / 1000.0;
|
||||
let span = (max - min) * factor;
|
||||
span = Math.max(minSpan, Math.min(totalSpan, span));
|
||||
const center = (min + max) / 2.0;
|
||||
let newMin = center - span / 2.0;
|
||||
let newMax = center + span / 2.0;
|
||||
if (newMin < limitMin) {
|
||||
newMax += (limitMin - newMin);
|
||||
newMin = limitMin;
|
||||
}
|
||||
if (newMax > limitMax) {
|
||||
newMin -= (newMax - limitMax);
|
||||
newMax = limitMax;
|
||||
}
|
||||
newMin = Math.max(limitMin, newMin);
|
||||
newMax = Math.min(limitMax, newMax);
|
||||
return { min: newMin, max: newMax };
|
||||
}
|
||||
function refreshAutoCheckbox() {
|
||||
autoZoom.checked = !!viewState.auto;
|
||||
}
|
||||
function applyZoomX(factor) {
|
||||
if (!latestAutoBounds || !latestPacket) return;
|
||||
const current = currentViewBounds(latestAutoBounds);
|
||||
const range = zoomRange(current.xMin, current.xMax, factor, 0, Math.max(1e-9, latestAutoBounds.maxT));
|
||||
viewState = { auto: false, xMin: range.min, xMax: range.max, yMin: current.yMin, yMax: current.yMax };
|
||||
saveViewState();
|
||||
refreshAutoCheckbox();
|
||||
renderPacket(latestPacket);
|
||||
}
|
||||
function applyZoomY(factor) {
|
||||
if (!latestAutoBounds || !latestPacket) return;
|
||||
const current = currentViewBounds(latestAutoBounds);
|
||||
const yPad = Math.max(0.05, (latestAutoBounds.maxY - latestAutoBounds.minY) * 4.0);
|
||||
const range = zoomRange(current.yMin, current.yMax, factor, latestAutoBounds.minY - yPad, latestAutoBounds.maxY + yPad);
|
||||
viewState = { auto: false, xMin: current.xMin, xMax: current.xMax, yMin: range.min, yMax: range.max };
|
||||
saveViewState();
|
||||
refreshAutoCheckbox();
|
||||
renderPacket(latestPacket);
|
||||
}
|
||||
function resetView() {
|
||||
viewState = defaultViewState();
|
||||
saveViewState();
|
||||
refreshAutoCheckbox();
|
||||
if (latestPacket) renderPacket(latestPacket);
|
||||
}
|
||||
function drawAxes(minY, maxY, xMin, xMax) {
|
||||
const left = 78, top = 24, width = canvas.width - 112, height = canvas.height - 92;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#fbfdff'; ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#ffffff'; ctx.fillRect(left, top, width, height);
|
||||
ctx.strokeStyle = '#dbe4ed'; ctx.lineWidth = 1; ctx.strokeRect(left, top, width, height);
|
||||
ctx.strokeStyle = '#edf2f7';
|
||||
for (let i = 0; i <= 10; i += 1) {
|
||||
const x = left + width * i / 10;
|
||||
const y = top + height * i / 10;
|
||||
ctx.beginPath(); ctx.moveTo(x, top); ctx.lineTo(x, top + height); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(left, y); ctx.lineTo(left + width, y); ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = '#607080'; ctx.font = '12px Segoe UI';
|
||||
for (let i = 0; i <= 10; i += 1) {
|
||||
const x = left + width * i / 10;
|
||||
const t = xMin + (xMax - xMin) * i / 10;
|
||||
ctx.fillText(t.toFixed(6), x - 18, top + height + 22);
|
||||
const y = top + height - height * i / 10;
|
||||
const v = minY + (maxY - minY) * i / 10;
|
||||
ctx.fillText(v.toFixed(3), 8, y + 4);
|
||||
}
|
||||
return { left, top, width, height };
|
||||
}
|
||||
function drawTrace(points, color, box, minY, maxY, xMin, xMax) {
|
||||
if (!points || points.length === 0) return;
|
||||
const spanY = Math.max(1e-9, maxY - minY);
|
||||
const spanT = Math.max(1e-9, xMax - xMin);
|
||||
ctx.beginPath();
|
||||
points.forEach((p, i) => {
|
||||
const x = box.left + ((p[0] - xMin) / spanT) * box.width;
|
||||
const y = box.top + box.height - ((p[1] - minY) / spanY) * box.height;
|
||||
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.strokeStyle = color; ctx.lineWidth = 1.25; ctx.stroke();
|
||||
}
|
||||
function drawDigitalTrace(points, box, xMin, xMax) {
|
||||
if (!points || points.length === 0) return;
|
||||
const spanT = Math.max(1e-9, xMax - xMin);
|
||||
const band = { left: box.left, top: box.top + 10, width: box.width, height: 44 };
|
||||
ctx.fillStyle = 'rgba(27,143,58,0.06)'; ctx.fillRect(band.left, band.top, band.width, band.height);
|
||||
ctx.strokeStyle = 'rgba(27,143,58,0.20)'; ctx.strokeRect(band.left, band.top, band.width, band.height);
|
||||
ctx.fillStyle = '#1b8f3a'; ctx.font = '12px Segoe UI'; ctx.fillText('DI1', band.left + 6, band.top + 14);
|
||||
ctx.beginPath();
|
||||
points.forEach((p, i) => {
|
||||
const x = band.left + ((p[0] - xMin) / spanT) * band.width;
|
||||
const y = band.top + band.height - ((0.15 + 0.7 * p[1]) * band.height);
|
||||
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.strokeStyle = '#1b8f3a'; ctx.lineWidth = 1.4; ctx.stroke();
|
||||
}
|
||||
function renderWaiting(message) {
|
||||
packetInfo.textContent = 'Waiting for packets...';
|
||||
timingInfo.textContent = '';
|
||||
zeroInfo.textContent = '';
|
||||
zoomInfo.textContent = 'View: auto';
|
||||
packetRateInfo.textContent = '';
|
||||
legendCh2.style.display = '';
|
||||
legendDi1.style.display = 'none';
|
||||
refreshAutoCheckbox();
|
||||
updateInfo.textContent = 'Polling every 500 ms';
|
||||
statusText.textContent = message;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#fbfdff'; ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#607080'; ctx.font = '20px Segoe UI'; ctx.fillText('Waiting for first packet...', 32, 80);
|
||||
}
|
||||
function renderPacket(data) {
|
||||
latestPacket = data;
|
||||
const ch1 = data.ch1 || [];
|
||||
const ch2 = data.ch2 || [];
|
||||
const di1 = data.di1 || [];
|
||||
const hasCh2 = (data.channelCount || 2) > 1 && ch2.length > 0;
|
||||
const hasDi1Trace = !!data.hasDi1Trace && di1.length > 0;
|
||||
const values = [];
|
||||
ch1.forEach(p => values.push(p[1]));
|
||||
if (hasCh2) ch2.forEach(p => values.push(p[1]));
|
||||
let minY = Math.min(...values);
|
||||
let maxY = Math.max(...values);
|
||||
if (!Number.isFinite(minY) || !Number.isFinite(maxY) || minY === maxY) {
|
||||
minY = -1; maxY = 1;
|
||||
} else {
|
||||
const pad = Math.max(0.05, (maxY - minY) * 0.08);
|
||||
minY -= pad; maxY += pad;
|
||||
}
|
||||
const maxT = Math.max(data.durationMs / 1000.0, 1e-9);
|
||||
latestAutoBounds = { minY, maxY, maxT };
|
||||
const view = currentViewBounds(latestAutoBounds);
|
||||
const box = drawAxes(view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
drawTrace(ch1, '#005bbb', box, view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
if (hasCh2) drawTrace(ch2, '#d62828', box, view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
if (hasDi1Trace) drawDigitalTrace(di1, box, view.xMin, view.xMax);
|
||||
legendCh2.style.display = hasCh2 ? '' : 'none';
|
||||
legendDi1.style.display = hasDi1Trace ? '' : 'none';
|
||||
refreshAutoCheckbox();
|
||||
packetInfo.textContent = 'Packet #' + data.packetIndex + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);
|
||||
timingInfo.textContent = 'Frames/ch: ' + data.framesPerChannel + ', duration: ' + data.durationMs.toFixed(3) + ' ms';
|
||||
packetRateInfo.textContent = 'Packets/s: ' + data.packetsPerSecond.toFixed(3);
|
||||
zeroInfo.textContent = hasDi1Trace
|
||||
? ('DI1 trace: ' + data.di1Frames + ' frame samples')
|
||||
: ('Zeroed on DI1 change: ' + data.zeroedPercent.toFixed(3) + '% (' + data.zeroedSamples + '/' + data.storedSamples + ')');
|
||||
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');
|
||||
updateInfo.textContent = 'Snapshot: ' + data.updatedAt;
|
||||
statusText.textContent = 'Close reason: ' + data.closeReason + '. This page refreshes its data every 500 ms.';
|
||||
}
|
||||
function applyLiveData(data) {
|
||||
if (!data || data.status === 'waiting') {
|
||||
renderWaiting((data && data.message) ? data.message : 'Waiting for first packet...');
|
||||
} else {
|
||||
renderPacket(data);
|
||||
}
|
||||
}
|
||||
function loadLatestData() {
|
||||
const oldScript = document.getElementById('liveDataScript');
|
||||
if (oldScript) oldScript.remove();
|
||||
window.e502LiveData = null;
|
||||
const script = document.createElement('script');
|
||||
script.id = 'liveDataScript';
|
||||
const sep = dataScriptUrl.includes('?') ? '&' : '?';
|
||||
script.src = dataScriptUrl + sep + 'ts=' + Date.now();
|
||||
script.onload = () => {
|
||||
applyLiveData(window.e502LiveData);
|
||||
};
|
||||
script.onerror = () => {
|
||||
statusText.textContent = 'Cannot load live data script: ' + dataScriptUrl;
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
zoomXIn.addEventListener('click', () => applyZoomX(0.5));
|
||||
zoomXOut.addEventListener('click', () => applyZoomX(2.0));
|
||||
zoomYIn.addEventListener('click', () => applyZoomY(0.5));
|
||||
zoomYOut.addEventListener('click', () => applyZoomY(2.0));
|
||||
resetZoom.addEventListener('click', () => resetView());
|
||||
autoZoom.addEventListener('change', () => {
|
||||
if (autoZoom.checked) {
|
||||
resetView();
|
||||
} else if (latestAutoBounds && latestPacket) {
|
||||
const current = currentViewBounds(latestAutoBounds);
|
||||
viewState = { auto: false, xMin: current.xMin, xMax: current.xMax, yMin: current.yMin, yMax: current.yMax };
|
||||
saveViewState();
|
||||
renderPacket(latestPacket);
|
||||
}
|
||||
});
|
||||
canvas.addEventListener('wheel', (event) => {
|
||||
if (!latestPacket) return;
|
||||
event.preventDefault();
|
||||
const factor = event.deltaY < 0 ? 0.8 : 1.25;
|
||||
if (event.shiftKey) {
|
||||
applyZoomY(factor);
|
||||
} else {
|
||||
applyZoomX(factor);
|
||||
}
|
||||
}, { passive: false });
|
||||
canvas.addEventListener('dblclick', () => resetView());
|
||||
renderWaiting('Waiting for first packet...');
|
||||
loadLatestData();
|
||||
window.setInterval(loadLatestData, 500);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
21
live_plot.js
Normal file
21
live_plot.js
Normal file
@ -0,0 +1,21 @@
|
||||
window.e502LiveData = {
|
||||
"status": "packet",
|
||||
"packetIndex": 7206,
|
||||
"channelCount": 2,
|
||||
"packetsSeen": 7206,
|
||||
"packetsPerSecond": 23.749101911,
|
||||
"framesPerChannel": 1,
|
||||
"frameFreqHz": 1000000.000000000,
|
||||
"durationMs": 0.001000000,
|
||||
"closeReason": "DI_SYN2 edge",
|
||||
"zeroedSamples": 0,
|
||||
"storedSamples": 2,
|
||||
"zeroedPercent": 0.000000000,
|
||||
"hasDi1Trace": true,
|
||||
"di1Frames": 1,
|
||||
"updatedAt": "packet 7206",
|
||||
"ch1": [[0.000000000,0.006862633]],
|
||||
"ch2": [[0.000000000,0.016366433]],
|
||||
"di1": [[0.000000000,0.000000000]]
|
||||
}
|
||||
;
|
||||
20
live_plot.json
Normal file
20
live_plot.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"status": "packet",
|
||||
"packetIndex": 7206,
|
||||
"channelCount": 2,
|
||||
"packetsSeen": 7206,
|
||||
"packetsPerSecond": 23.749101911,
|
||||
"framesPerChannel": 1,
|
||||
"frameFreqHz": 1000000.000000000,
|
||||
"durationMs": 0.001000000,
|
||||
"closeReason": "DI_SYN2 edge",
|
||||
"zeroedSamples": 0,
|
||||
"storedSamples": 2,
|
||||
"zeroedPercent": 0.000000000,
|
||||
"hasDi1Trace": true,
|
||||
"di1Frames": 1,
|
||||
"updatedAt": "packet 7206",
|
||||
"ch1": [[0.000000000,0.006862633]],
|
||||
"ch2": [[0.000000000,0.016366433]],
|
||||
"di1": [[0.000000000,0.000000000]]
|
||||
}
|
||||
135
main.cpp
135
main.cpp
@ -40,6 +40,12 @@ enum class StopMode {
|
||||
DiSyn2Fall
|
||||
};
|
||||
|
||||
enum class Di1Mode {
|
||||
ZeroOnChange,
|
||||
Trace,
|
||||
Ignore
|
||||
};
|
||||
|
||||
struct Config {
|
||||
std::string serial;
|
||||
std::optional<uint32_t> ip_addr;
|
||||
@ -60,6 +66,7 @@ struct Config {
|
||||
uint32_t sync_mode = X502_SYNC_DI_SYN1_RISE;
|
||||
uint32_t sync_start_mode = X502_SYNC_DI_SYN2_RISE;
|
||||
StopMode stop_mode = StopMode::DiSyn2Fall;
|
||||
Di1Mode di1_mode = Di1Mode::ZeroOnChange;
|
||||
|
||||
uint32_t recv_block_words = 32768;
|
||||
uint32_t recv_timeout_ms = 50;
|
||||
@ -307,6 +314,33 @@ std::string stop_mode_to_string(StopMode mode) {
|
||||
}
|
||||
}
|
||||
|
||||
Di1Mode parse_di1_mode(const std::string& text) {
|
||||
const std::string value = trim_copy(text);
|
||||
if ((value == "zero") || (value == "zero_on_change") || (value == "mark")) {
|
||||
return Di1Mode::ZeroOnChange;
|
||||
}
|
||||
if ((value == "trace") || (value == "plot") || (value == "stream")) {
|
||||
return Di1Mode::Trace;
|
||||
}
|
||||
if ((value == "ignore") || (value == "off") || (value == "none")) {
|
||||
return Di1Mode::Ignore;
|
||||
}
|
||||
fail("Unsupported di1 mode: " + text);
|
||||
}
|
||||
|
||||
std::string di1_mode_to_string(Di1Mode mode) {
|
||||
switch (mode) {
|
||||
case Di1Mode::ZeroOnChange:
|
||||
return "zero";
|
||||
case Di1Mode::Trace:
|
||||
return "trace";
|
||||
case Di1Mode::Ignore:
|
||||
return "ignore";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
bool sync_uses_di_syn1(uint32_t mode) {
|
||||
return (mode == X502_SYNC_DI_SYN1_RISE) || (mode == X502_SYNC_DI_SYN1_FALL);
|
||||
}
|
||||
@ -335,6 +369,7 @@ void print_help(const char* exe_name) {
|
||||
<< " [mode:diff|comm] [range:0.2] [clock:di_syn1_rise]\n"
|
||||
<< " [start:di_syn2_rise] [stop:di_syn2_fall] [sample_clock_hz:125000|max]\n"
|
||||
<< " [internal_ref_hz:2000000]\n"
|
||||
<< " [di1:zero|trace|ignore]\n"
|
||||
<< " [duration_ms:100] [packet_limit:0] [csv:capture.csv] [svg:capture.svg]\n"
|
||||
<< " [live_html:live_plot.html] [live_json:live_plot.json]\n"
|
||||
<< " [recv_block:32768] [stats_period_ms:1000] [live_update_period_ms:1000] [svg_history_packets:50] [start_wait_ms:10000]\n"
|
||||
@ -355,6 +390,9 @@ void print_help(const char* exe_name) {
|
||||
<< " sample_clock_hz:125000 -> requested ADC sample rate; for external clock it is the expected rate\n"
|
||||
<< " sample_clock_hz:max -> with clock:internal, use the maximum ADC speed\n"
|
||||
<< " internal_ref_hz:2000000 -> internal base clock for clock:internal (1500000 or 2000000)\n"
|
||||
<< " di1:zero -> write ADC sample as 0 on each DI1 level change\n"
|
||||
<< " di1:trace -> keep ADC unchanged and store DI1 as a separate synchronous trace\n"
|
||||
<< " di1:ignore -> ignore DI1 for both zeroing and plotting\n"
|
||||
<< " stats_period_ms:1000 -> print online transfer/capture statistics every 1000 ms (0 disables)\n"
|
||||
<< " recv_block:32768 -> request up to 32768 raw 32-bit words per X502_Recv() call\n"
|
||||
<< " live_update_period_ms:1000 -> refresh live HTML/JSON no more than once per second\n"
|
||||
@ -379,8 +417,8 @@ void print_help(const char* exe_name) {
|
||||
<< " stop: frames | di_syn2_rise | di_syn2_fall\n"
|
||||
<< "\n"
|
||||
<< "This build enables synchronous DIN together with ADC. DI_SYN2 stop edges are detected\n"
|
||||
<< "inside the same input stream, packets are split continuously by DI_SYN2 edges, and if\n"
|
||||
<< "digital input 1 changes state the corresponding ADC sample is written to the buffer as 0.\n"
|
||||
<< "inside the same input stream, packets are split continuously by DI_SYN2 edges, and DI1\n"
|
||||
<< "can either zero ADC samples on change, be exported as a separate synchronous trace, or be ignored.\n"
|
||||
<< "Open live_plot.html in a browser to see the live graph update over time.\n"
|
||||
<< "The live HTML supports X/Y zoom buttons, mouse-wheel zoom, and reset.\n"
|
||||
<< "\n"
|
||||
@ -460,6 +498,10 @@ Config parse_args(int argc, char** argv) {
|
||||
cfg.stop_mode = parse_stop_mode(arg.substr(5));
|
||||
continue;
|
||||
}
|
||||
if (starts_with(arg, "di1:")) {
|
||||
cfg.di1_mode = parse_di1_mode(arg.substr(4));
|
||||
continue;
|
||||
}
|
||||
if (starts_with(arg, "sample_clock_hz:")) {
|
||||
const std::string value = trim_copy(arg.substr(16));
|
||||
cfg.sample_clock_specified = true;
|
||||
@ -784,8 +826,11 @@ bool matches_stop_edge(StopMode mode, bool prev_level, bool current_level) {
|
||||
|
||||
struct PacketAccumulator {
|
||||
std::array<std::vector<double>, 2> channels;
|
||||
std::vector<uint8_t> di1;
|
||||
std::size_t zeroed_samples = 0;
|
||||
std::size_t stored_samples = 0;
|
||||
bool pending_frame_di1_valid = false;
|
||||
uint8_t pending_frame_di1 = 0;
|
||||
|
||||
void reset(std::size_t reserve_frames, std::size_t channel_count) {
|
||||
for (std::size_t i = 0; i < channels.size(); ++i) {
|
||||
@ -795,12 +840,20 @@ struct PacketAccumulator {
|
||||
channel.reserve(reserve_frames);
|
||||
}
|
||||
}
|
||||
di1.clear();
|
||||
di1.reserve(reserve_frames);
|
||||
zeroed_samples = 0;
|
||||
stored_samples = 0;
|
||||
pending_frame_di1_valid = false;
|
||||
pending_frame_di1 = 0;
|
||||
}
|
||||
|
||||
std::size_t frame_count(std::size_t channel_count) const {
|
||||
return (channel_count <= 1U) ? channels[0].size() : std::min(channels[0].size(), channels[1].size());
|
||||
std::size_t frames = (channel_count <= 1U) ? channels[0].size() : std::min(channels[0].size(), channels[1].size());
|
||||
if (!di1.empty()) {
|
||||
frames = std::min(frames, di1.size());
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
};
|
||||
|
||||
@ -982,12 +1035,13 @@ int run(const Config& cfg) {
|
||||
<< " channel 1: " << phy_channel_name(cfg.mode, cfg.ch1) << "\n"
|
||||
<< " channel 2: "
|
||||
<< ((cfg.channel_count >= 2U) ? phy_channel_name(cfg.mode, cfg.ch2) : std::string("disabled")) << "\n"
|
||||
<< " DI1 handling: " << di1_mode_to_string(cfg.di1_mode) << "\n"
|
||||
<< " ADC range: +/-" << range_to_volts(cfg.range) << " V\n"
|
||||
<< " max frames per packet per channel: " << target_frames << "\n";
|
||||
|
||||
CaptureFileWriter writer(cfg.csv_path, cfg.svg_path, cfg.live_html_path, cfg.live_json_path);
|
||||
writer.initialize_live_plot();
|
||||
writer.initialize_csv(cfg.channel_count);
|
||||
writer.initialize_csv(cfg.channel_count, cfg.di1_mode == Di1Mode::Trace);
|
||||
std::cout << " live plot html: " << writer.live_html_path() << "\n"
|
||||
<< " live plot data: " << writer.live_json_path() << "\n"
|
||||
<< " recv block words: " << cfg.recv_block_words << "\n"
|
||||
@ -1064,9 +1118,14 @@ int run(const Config& cfg) {
|
||||
<< ", ADC samples/s=" << adc_samples_per_s
|
||||
<< ", DIN samples/s=" << din_samples_per_s
|
||||
<< ", frames/s per channel=" << frames_per_ch_per_s
|
||||
<< ", packets/s=" << packets_per_s
|
||||
<< ", zeroed on DI1 change=" << zeroed_fraction << "% ("
|
||||
<< stats_zeroed_samples << "/" << stats_stored_adc_samples << ")\n";
|
||||
<< ", packets/s=" << packets_per_s;
|
||||
if (cfg.di1_mode == Di1Mode::ZeroOnChange) {
|
||||
std::cout << ", zeroed on DI1 change=" << zeroed_fraction << "% ("
|
||||
<< stats_zeroed_samples << "/" << stats_stored_adc_samples << ")";
|
||||
} else if (cfg.di1_mode == Di1Mode::Trace) {
|
||||
std::cout << ", DI1 trace=enabled";
|
||||
}
|
||||
std::cout << "\n";
|
||||
|
||||
if (!final_report) {
|
||||
stats_window_start = now;
|
||||
@ -1098,12 +1157,19 @@ int run(const Config& cfg) {
|
||||
} else {
|
||||
current_packet.channels[1].clear();
|
||||
}
|
||||
if (cfg.di1_mode == Di1Mode::Trace) {
|
||||
current_packet.di1.resize(frames);
|
||||
} else {
|
||||
current_packet.di1.clear();
|
||||
}
|
||||
|
||||
CapturePacket packet;
|
||||
packet.packet_index = static_cast<std::size_t>(total_completed_packets + 1U);
|
||||
packet.channel_count = cfg.channel_count;
|
||||
packet.has_di1_trace = (cfg.di1_mode == Di1Mode::Trace);
|
||||
packet.ch1 = std::move(current_packet.channels[0]);
|
||||
packet.ch2 = std::move(current_packet.channels[1]);
|
||||
packet.di1 = std::move(current_packet.di1);
|
||||
|
||||
const double packet_duration_ms = (1000.0 * static_cast<double>(frames)) / actual_frame_freq_hz;
|
||||
const double zeroed_fraction = (current_packet.stored_samples == 0U)
|
||||
@ -1115,9 +1181,14 @@ int run(const Config& cfg) {
|
||||
<< "Packet " << packet.packet_index
|
||||
<< ": frames/ch=" << frames
|
||||
<< ", duration_ms=" << packet_duration_ms
|
||||
<< ", close_reason=" << packet_close_reason_to_string(reason)
|
||||
<< ", zeroed_on_DI1_change=" << zeroed_fraction << "% ("
|
||||
<< current_packet.zeroed_samples << "/" << current_packet.stored_samples << ")\n";
|
||||
<< ", close_reason=" << packet_close_reason_to_string(reason);
|
||||
if (cfg.di1_mode == Di1Mode::ZeroOnChange) {
|
||||
std::cout << ", zeroed_on_DI1_change=" << zeroed_fraction << "% ("
|
||||
<< current_packet.zeroed_samples << "/" << current_packet.stored_samples << ")";
|
||||
} else if (cfg.di1_mode == Di1Mode::Trace) {
|
||||
std::cout << ", di1_trace_frames=" << packet.di1.size();
|
||||
}
|
||||
std::cout << "\n";
|
||||
|
||||
writer.append_csv_packet(packet, actual_frame_freq_hz, csv_global_frame_index);
|
||||
++total_completed_packets;
|
||||
@ -1268,12 +1339,12 @@ int run(const Config& cfg) {
|
||||
pending_din.pop_front();
|
||||
|
||||
const bool di1_level = (din_value & kE502Digital1Mask) != 0U;
|
||||
bool zero_on_di1_change = false;
|
||||
bool di1_changed = false;
|
||||
if (!di1_initialized) {
|
||||
di1_prev_level = di1_level;
|
||||
di1_initialized = true;
|
||||
} else if (di1_level != di1_prev_level) {
|
||||
zero_on_di1_change = true;
|
||||
di1_changed = true;
|
||||
di1_prev_level = di1_level;
|
||||
}
|
||||
|
||||
@ -1323,8 +1394,13 @@ int run(const Config& cfg) {
|
||||
const uint32_t lch = next_lch;
|
||||
next_lch = (next_lch + 1U) % cfg.channel_count;
|
||||
|
||||
if ((cfg.di1_mode == Di1Mode::Trace) && ((cfg.channel_count <= 1U) || (lch == 0U))) {
|
||||
current_packet.pending_frame_di1 = static_cast<uint8_t>(di1_level ? 1U : 0U);
|
||||
current_packet.pending_frame_di1_valid = true;
|
||||
}
|
||||
|
||||
double stored_value = adc_value;
|
||||
if (zero_on_di1_change) {
|
||||
if ((cfg.di1_mode == Di1Mode::ZeroOnChange) && di1_changed) {
|
||||
stored_value = 0.0;
|
||||
++total_zeroed_samples;
|
||||
++stats_zeroed_samples;
|
||||
@ -1337,6 +1413,12 @@ int run(const Config& cfg) {
|
||||
++total_stored_adc_samples;
|
||||
++stats_stored_adc_samples;
|
||||
if (lch == (cfg.channel_count - 1U)) {
|
||||
if ((cfg.di1_mode == Di1Mode::Trace) &&
|
||||
current_packet.pending_frame_di1_valid &&
|
||||
(current_packet.di1.size() < target_frames)) {
|
||||
current_packet.di1.push_back(current_packet.pending_frame_di1);
|
||||
current_packet.pending_frame_di1_valid = false;
|
||||
}
|
||||
++total_completed_frames;
|
||||
++stats_completed_frames;
|
||||
}
|
||||
@ -1382,9 +1464,13 @@ int run(const Config& cfg) {
|
||||
}
|
||||
|
||||
std::cout << "Captured " << total_completed_packets << " packet(s), "
|
||||
<< total_packet_frames << " total frames per channel kept for final SVG\n"
|
||||
<< "ADC samples forced to 0 on DI1 change: " << total_zeroed_samples << "\n"
|
||||
<< "Average stats: "
|
||||
<< total_packet_frames << " total frames per channel kept for final SVG\n";
|
||||
if (cfg.di1_mode == Di1Mode::ZeroOnChange) {
|
||||
std::cout << "ADC samples forced to 0 on DI1 change: " << total_zeroed_samples << "\n";
|
||||
} else if (cfg.di1_mode == Di1Mode::Trace) {
|
||||
std::cout << "DI1 synchronous trace exported to CSV/live plot/SVG\n";
|
||||
}
|
||||
std::cout << "Average stats: "
|
||||
<< "MB/s=" << std::fixed << std::setprecision(3)
|
||||
<< ((static_cast<double>(total_raw_words) * sizeof(uint32_t)) /
|
||||
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0) /
|
||||
@ -1401,12 +1487,17 @@ int run(const Config& cfg) {
|
||||
<< ", packets/s="
|
||||
<< (static_cast<double>(total_completed_packets) /
|
||||
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0))
|
||||
<< ", packets captured=" << total_completed_packets
|
||||
<< ", zeroed on DI1 change="
|
||||
<< ((total_stored_adc_samples == 0U)
|
||||
? 0.0
|
||||
: (100.0 * static_cast<double>(total_zeroed_samples) / static_cast<double>(total_stored_adc_samples)))
|
||||
<< "% (" << total_zeroed_samples << "/" << total_stored_adc_samples << ")\n"
|
||||
<< ", packets captured=" << total_completed_packets;
|
||||
if (cfg.di1_mode == Di1Mode::ZeroOnChange) {
|
||||
std::cout << ", zeroed on DI1 change="
|
||||
<< ((total_stored_adc_samples == 0U)
|
||||
? 0.0
|
||||
: (100.0 * static_cast<double>(total_zeroed_samples) / static_cast<double>(total_stored_adc_samples)))
|
||||
<< "% (" << total_zeroed_samples << "/" << total_stored_adc_samples << ")";
|
||||
} else if (cfg.di1_mode == Di1Mode::Trace) {
|
||||
std::cout << ", DI1 trace=enabled";
|
||||
}
|
||||
std::cout << "\n"
|
||||
<< "Final SVG packets retained in memory: " << svg_packets.size() << "\n"
|
||||
<< "CSV: " << cfg.csv_path << "\n"
|
||||
<< "SVG: " << cfg.svg_path << "\n";
|
||||
|
||||
Reference in New Issue
Block a user