clock:internal internal_ref_hz:2000000 start:di_syn2_rise stop:di_syn2_fall sample_clock_hz:max range:0.2 di1:trace di1_group_avg full_res_live live_history_packets:8 duration_ms:100 packet_limit:0 csv:capture.csv svg:capture.svgC
This commit is contained in:
@ -15,6 +15,9 @@
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kLivePlotMaxColumns = 800;
|
||||
constexpr std::size_t kLivePlotMidColumns = 4096;
|
||||
constexpr std::size_t kLivePlotHighColumns = 16384;
|
||||
constexpr std::size_t kLivePlotRawFrameLimit = 50000;
|
||||
constexpr uint32_t kCsvSpoolMagic = 0x4C564353U; // "SVCL"
|
||||
constexpr uint32_t kCsvSpoolVersion = 2U;
|
||||
|
||||
@ -37,6 +40,14 @@ struct PlotPoint {
|
||||
double value = 0.0;
|
||||
};
|
||||
|
||||
struct Di1GroupedTraces {
|
||||
std::vector<PlotPoint> ch1;
|
||||
std::vector<PlotPoint> ch2;
|
||||
std::vector<PlotPoint> rss;
|
||||
};
|
||||
|
||||
std::size_t packet_frame_count(const CapturePacket& packet);
|
||||
|
||||
std::size_t packet_channel_count(const CapturePacket& packet) {
|
||||
return (packet.channel_count <= 1U) ? 1U : 2U;
|
||||
}
|
||||
@ -49,6 +60,22 @@ bool packet_has_di1_trace(const CapturePacket& packet) {
|
||||
return packet.has_di1_trace && !packet.di1.empty();
|
||||
}
|
||||
|
||||
std::vector<double> build_rss_values(const CapturePacket& packet) {
|
||||
const std::size_t frames = packet_frame_count(packet);
|
||||
std::vector<double> rss;
|
||||
if (!packet_has_ch2(packet) || (frames == 0U)) {
|
||||
return rss;
|
||||
}
|
||||
|
||||
rss.reserve(frames);
|
||||
for (std::size_t i = 0; i < frames; ++i) {
|
||||
const double ch1 = packet.ch1[i];
|
||||
const double ch2 = packet.ch2[i];
|
||||
rss.push_back(std::sqrt((ch1 * ch1) + (ch2 * ch2)));
|
||||
}
|
||||
return rss;
|
||||
}
|
||||
|
||||
std::size_t packet_frame_count(const CapturePacket& packet) {
|
||||
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)) {
|
||||
@ -97,6 +124,74 @@ std::vector<PlotPoint> build_min_max_trace(const std::vector<double>& data, std:
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<PlotPoint> build_full_trace(const std::vector<double>& data) {
|
||||
std::vector<PlotPoint> result;
|
||||
result.reserve(data.size());
|
||||
for (std::size_t i = 0; i < data.size(); ++i) {
|
||||
result.push_back({i, data[i]});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void append_offset_trace(std::vector<PlotPoint>& dst,
|
||||
const std::vector<PlotPoint>& src,
|
||||
std::size_t sample_offset) {
|
||||
dst.reserve(dst.size() + src.size());
|
||||
for (const auto& point : src) {
|
||||
dst.push_back({sample_offset + point.sample_index, point.value});
|
||||
}
|
||||
}
|
||||
|
||||
Di1GroupedTraces build_di1_grouped_traces(const std::vector<double>& ch1,
|
||||
const std::vector<double>& ch2,
|
||||
const std::vector<uint8_t>& di1,
|
||||
bool has_ch2) {
|
||||
Di1GroupedTraces traces;
|
||||
const std::size_t frames = std::min(ch1.size(), di1.size());
|
||||
if (frames == 0U) {
|
||||
return traces;
|
||||
}
|
||||
|
||||
traces.ch1.reserve(frames / 2U + 1U);
|
||||
if (has_ch2) {
|
||||
traces.ch2.reserve(frames / 2U + 1U);
|
||||
traces.rss.reserve(frames / 2U + 1U);
|
||||
}
|
||||
|
||||
std::size_t begin = 0U;
|
||||
while (begin < frames) {
|
||||
const uint8_t level = di1[begin];
|
||||
std::size_t end = begin + 1U;
|
||||
while ((end < frames) && (di1[end] == level)) {
|
||||
++end;
|
||||
}
|
||||
|
||||
double sum1 = 0.0;
|
||||
double sum2 = 0.0;
|
||||
for (std::size_t i = begin; i < end; ++i) {
|
||||
sum1 += ch1[i];
|
||||
if (has_ch2 && (i < ch2.size())) {
|
||||
sum2 += ch2[i];
|
||||
}
|
||||
}
|
||||
|
||||
const double count = static_cast<double>(end - begin);
|
||||
const std::size_t mid = begin + ((end - begin - 1U) / 2U);
|
||||
const double avg1 = sum1 / count;
|
||||
traces.ch1.push_back({mid, avg1});
|
||||
|
||||
if (has_ch2) {
|
||||
const double avg2 = sum2 / count;
|
||||
traces.ch2.push_back({mid, avg2});
|
||||
traces.rss.push_back({mid, std::sqrt((avg1 * avg1) + (avg2 * avg2))});
|
||||
}
|
||||
|
||||
begin = end;
|
||||
}
|
||||
|
||||
return traces;
|
||||
}
|
||||
|
||||
std::vector<PlotPoint> build_digital_step_trace(const std::vector<uint8_t>& data) {
|
||||
std::vector<PlotPoint> result;
|
||||
if (data.empty()) {
|
||||
@ -184,6 +279,24 @@ std::string json_points(const std::vector<PlotPoint>& trace, double frame_freq_h
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::string json_packet_markers(const std::vector<std::pair<std::size_t, std::size_t>>& markers,
|
||||
double frame_freq_hz) {
|
||||
std::ostringstream out;
|
||||
out << std::fixed << std::setprecision(9);
|
||||
out << "[";
|
||||
bool first = true;
|
||||
for (const auto& marker : markers) {
|
||||
if (!first) {
|
||||
out << ",";
|
||||
}
|
||||
first = false;
|
||||
out << "{\"t\":" << (static_cast<double>(marker.first) / frame_freq_hz)
|
||||
<< ",\"label\":\"P" << marker.second << "\"}";
|
||||
}
|
||||
out << "]";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
[[noreturn]] void fail_write(const std::string& message);
|
||||
|
||||
std::string replace_extension(const std::string& path, const std::string& new_extension) {
|
||||
@ -296,22 +409,25 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
||||
<< " <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 refreshes its data script 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 a rolling continuous packet window.</div>\n"
|
||||
<< " <div class=\"controls\">\n"
|
||||
<< " <button type=\"button\" id=\"zoomXIn\">X+</button>\n"
|
||||
<< " <button type=\"button\" id=\"zoomXOut\">X-</button>\n"
|
||||
<< " <button type=\"button\" id=\"panLeft\">Left</button>\n"
|
||||
<< " <button type=\"button\" id=\"panRight\">Right</button>\n"
|
||||
<< " <button type=\"button\" id=\"zoomYIn\">Y+</button>\n"
|
||||
<< " <button type=\"button\" id=\"zoomYOut\">Y-</button>\n"
|
||||
<< " <button type=\"button\" id=\"resetZoom\">Reset</button>\n"
|
||||
<< " <label><input type=\"checkbox\" id=\"autoZoom\" checked/>Auto view</label>\n"
|
||||
<< " <span class=\"hint\">Wheel: X zoom, Shift+wheel: Y zoom, double-click: reset</span>\n"
|
||||
<< " <span class=\"hint\">Wheel: X zoom, Shift+wheel: Y zoom, Left/Right buttons or arrow keys: pan X, double-click: reset. The live view shows a rolling continuous packet window; CSV stores all samples.</span>\n"
|
||||
<< " </div>\n"
|
||||
<< " <canvas id=\"plot\" width=\"1200\" height=\"620\"></canvas>\n"
|
||||
<< " <div class=\"legend\">\n"
|
||||
<< " <span id=\"legendCh1\"><span class=\"sw\" style=\"background:#005bbb\"></span>CH1</span>\n"
|
||||
<< " <span id=\"legendCh2\"><span class=\"sw\" style=\"background:#d62828\"></span>CH2</span>\n"
|
||||
<< " <span id=\"legendDi1\"><span class=\"sw\" style=\"background:#1b8f3a\"></span>DI1</span>\n"
|
||||
<< " </div>\n"
|
||||
<< " <div class=\"legend\">\n"
|
||||
<< " <span id=\"legendCh1\"><span class=\"sw\" style=\"background:#005bbb\"></span>CH1</span>\n"
|
||||
<< " <span id=\"legendCh2\"><span class=\"sw\" style=\"background:#d62828\"></span>CH2</span>\n"
|
||||
<< " <span id=\"legendRss\"><span class=\"sw\" style=\"background:#ee8a12\"></span>sqrt(CH1^2 + CH2^2)</span>\n"
|
||||
<< " <span id=\"legendDi1\"><span class=\"sw\" style=\"background:#1b8f3a\"></span>DI1</span>\n"
|
||||
<< " </div>\n"
|
||||
<< " </div>\n"
|
||||
<< " </div>\n"
|
||||
<< " <script>\n"
|
||||
@ -323,12 +439,15 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
||||
<< " const timingInfo = document.getElementById('timingInfo');\n"
|
||||
<< " const zeroInfo = document.getElementById('zeroInfo');\n"
|
||||
<< " const zoomInfo = document.getElementById('zoomInfo');\n"
|
||||
<< " 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 packetRateInfo = document.getElementById('packetRateInfo');\n"
|
||||
<< " const updateInfo = document.getElementById('updateInfo');\n"
|
||||
<< " const legendCh2 = document.getElementById('legendCh2');\n"
|
||||
<< " const legendRss = document.getElementById('legendRss');\n"
|
||||
<< " const legendDi1 = document.getElementById('legendDi1');\n"
|
||||
<< " const zoomXIn = document.getElementById('zoomXIn');\n"
|
||||
<< " const zoomXOut = document.getElementById('zoomXOut');\n"
|
||||
<< " const panLeft = document.getElementById('panLeft');\n"
|
||||
<< " const panRight = document.getElementById('panRight');\n"
|
||||
<< " const zoomYIn = document.getElementById('zoomYIn');\n"
|
||||
<< " const zoomYOut = document.getElementById('zoomYOut');\n"
|
||||
<< " const resetZoom = document.getElementById('resetZoom');\n"
|
||||
@ -419,6 +538,30 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
||||
<< " refreshAutoCheckbox();\n"
|
||||
<< " renderPacket(latestPacket);\n"
|
||||
<< " }\n"
|
||||
<< " function applyPanX(direction) {\n"
|
||||
<< " if (!latestAutoBounds || !latestPacket) return;\n"
|
||||
<< " const current = currentViewBounds(latestAutoBounds);\n"
|
||||
<< " const totalX = Math.max(1e-9, latestAutoBounds.maxT);\n"
|
||||
<< " const span = Math.max(1e-9, current.xMax - current.xMin);\n"
|
||||
<< " if (span >= totalX) return;\n"
|
||||
<< " const shift = span * 0.25 * direction;\n"
|
||||
<< " let newMin = current.xMin + shift;\n"
|
||||
<< " let newMax = current.xMax + shift;\n"
|
||||
<< " if (newMin < 0) {\n"
|
||||
<< " newMax -= newMin;\n"
|
||||
<< " newMin = 0;\n"
|
||||
<< " }\n"
|
||||
<< " if (newMax > totalX) {\n"
|
||||
<< " newMin -= (newMax - totalX);\n"
|
||||
<< " newMax = totalX;\n"
|
||||
<< " }\n"
|
||||
<< " newMin = Math.max(0, newMin);\n"
|
||||
<< " newMax = Math.min(totalX, newMax);\n"
|
||||
<< " viewState = { auto: false, xMin: newMin, xMax: newMax, yMin: current.yMin, yMax: current.yMax };\n"
|
||||
<< " saveViewState();\n"
|
||||
<< " refreshAutoCheckbox();\n"
|
||||
<< " renderPacket(latestPacket);\n"
|
||||
<< " }\n"
|
||||
<< " function resetView() {\n"
|
||||
<< " viewState = defaultViewState();\n"
|
||||
<< " saveViewState();\n"
|
||||
@ -476,14 +619,41 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
||||
<< " });\n"
|
||||
<< " ctx.strokeStyle = '#1b8f3a'; ctx.lineWidth = 1.4; ctx.stroke();\n"
|
||||
<< " }\n"
|
||||
<< " function drawPacketMarkers(markers, box, xMin, xMax) {\n"
|
||||
<< " if (!Array.isArray(markers) || markers.length === 0) return;\n"
|
||||
<< " const spanT = Math.max(1e-9, xMax - xMin);\n"
|
||||
<< " ctx.save();\n"
|
||||
<< " ctx.setLineDash([4, 4]);\n"
|
||||
<< " ctx.strokeStyle = '#9db0c2';\n"
|
||||
<< " ctx.lineWidth = 1.0;\n"
|
||||
<< " ctx.fillStyle = '#5c6f82';\n"
|
||||
<< " ctx.font = '11px Segoe UI';\n"
|
||||
<< " markers.forEach((m) => {\n"
|
||||
<< " if (!m || !Number.isFinite(m.t) || (m.t < xMin) || (m.t > xMax)) return;\n"
|
||||
<< " const x = box.left + ((m.t - xMin) / spanT) * box.width;\n"
|
||||
<< " ctx.beginPath(); ctx.moveTo(x, box.top); ctx.lineTo(x, box.top + box.height); ctx.stroke();\n"
|
||||
<< " if (m.label) ctx.fillText(m.label, x + 4, box.top + 16);\n"
|
||||
<< " });\n"
|
||||
<< " ctx.restore();\n"
|
||||
<< " }\n"
|
||||
<< " function pickTrace(base, mid, high, maxT, view) {\n"
|
||||
<< " if (!Array.isArray(base) || base.length === 0) return [];\n"
|
||||
<< " const totalT = Math.max(1e-9, maxT || 0);\n"
|
||||
<< " const spanT = Math.max(1e-9, view.xMax - view.xMin);\n"
|
||||
<< " const ratio = spanT / totalT;\n"
|
||||
<< " if (ratio <= 0.10 && Array.isArray(high) && high.length > 0) return high;\n"
|
||||
<< " if (ratio <= 0.35 && Array.isArray(mid) && mid.length > 0) return mid;\n"
|
||||
<< " return base;\n"
|
||||
<< " }\n"
|
||||
<< " function renderWaiting(message) {\n"
|
||||
<< " packetInfo.textContent = 'Waiting for packets...';\n"
|
||||
<< " timingInfo.textContent = '';\n"
|
||||
<< " zeroInfo.textContent = '';\n"
|
||||
<< " zoomInfo.textContent = 'View: auto';\n"
|
||||
<< " packetRateInfo.textContent = '';\n"
|
||||
<< " legendCh2.style.display = '';\n"
|
||||
<< " legendDi1.style.display = 'none';\n"
|
||||
<< " packetRateInfo.textContent = '';\n"
|
||||
<< " legendCh2.style.display = '';\n"
|
||||
<< " legendRss.style.display = 'none';\n"
|
||||
<< " legendDi1.style.display = 'none';\n"
|
||||
<< " refreshAutoCheckbox();\n"
|
||||
<< " updateInfo.textContent = 'Polling every 500 ms';\n"
|
||||
<< " statusText.textContent = message;\n"
|
||||
@ -493,14 +663,31 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
||||
<< " }\n"
|
||||
<< " function renderPacket(data) {\n"
|
||||
<< " 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"
|
||||
<< " const ch1 = data.ch1 || [];\n"
|
||||
<< " const ch1Mid = data.ch1Mid || [];\n"
|
||||
<< " const ch1High = data.ch1High || [];\n"
|
||||
<< " const ch2 = data.ch2 || [];\n"
|
||||
<< " const ch2Mid = data.ch2Mid || [];\n"
|
||||
<< " const ch2High = data.ch2High || [];\n"
|
||||
<< " const rss = data.rss || [];\n"
|
||||
<< " const rssMid = data.rssMid || [];\n"
|
||||
<< " const rssHigh = data.rssHigh || [];\n"
|
||||
<< " const ch1Grouped = data.ch1Grouped || [];\n"
|
||||
<< " const ch2Grouped = data.ch2Grouped || [];\n"
|
||||
<< " const rssGrouped = data.rssGrouped || [];\n"
|
||||
<< " const di1 = data.di1 || [];\n"
|
||||
<< " const packetMarkers = data.packetMarkers || [];\n"
|
||||
<< " const di1Grouped = !!data.di1Grouped;\n"
|
||||
<< " const hasCh2 = (data.channelCount || 2) > 1 && ch2.length > 0;\n"
|
||||
<< " const hasRss = hasCh2 && rss.length > 0;\n"
|
||||
<< " const hasDi1Trace = !!data.hasDi1Trace && di1.length > 0;\n"
|
||||
<< " const values = [];\n"
|
||||
<< " const yCh1 = di1Grouped && ch1Grouped.length > 0 ? ch1Grouped : ch1;\n"
|
||||
<< " const yCh2 = di1Grouped && ch2Grouped.length > 0 ? ch2Grouped : ch2;\n"
|
||||
<< " const yRss = di1Grouped && rssGrouped.length > 0 ? rssGrouped : rss;\n"
|
||||
<< " yCh1.forEach(p => values.push(p[1]));\n"
|
||||
<< " if (hasCh2) yCh2.forEach(p => values.push(p[1]));\n"
|
||||
<< " if (hasRss) yRss.forEach(p => values.push(p[1]));\n"
|
||||
<< " let minY = Math.min(...values);\n"
|
||||
<< " let maxY = Math.max(...values);\n"
|
||||
<< " if (!Number.isFinite(minY) || !Number.isFinite(maxY) || minY === maxY) {\n"
|
||||
@ -509,25 +696,33 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
||||
<< " const pad = Math.max(0.05, (maxY - minY) * 0.08);\n"
|
||||
<< " minY -= pad; maxY += pad;\n"
|
||||
<< " }\n"
|
||||
<< " const maxT = Math.max(data.durationMs / 1000.0, 1e-9);\n"
|
||||
<< " latestAutoBounds = { minY, maxY, maxT };\n"
|
||||
<< " const view = currentViewBounds(latestAutoBounds);\n"
|
||||
<< " 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"
|
||||
<< " const maxT = Math.max(data.durationMs / 1000.0, 1e-9);\n"
|
||||
<< " latestAutoBounds = { minY, maxY, maxT };\n"
|
||||
<< " const view = currentViewBounds(latestAutoBounds);\n"
|
||||
<< " const plotCh1 = di1Grouped ? ch1Grouped : pickTrace(ch1, ch1Mid, ch1High, maxT, view);\n"
|
||||
<< " const plotCh2 = hasCh2 ? (di1Grouped ? ch2Grouped : pickTrace(ch2, ch2Mid, ch2High, maxT, view)) : [];\n"
|
||||
<< " const plotRss = hasRss ? (di1Grouped ? rssGrouped : pickTrace(rss, rssMid, rssHigh, maxT, view)) : [];\n"
|
||||
<< " const box = drawAxes(view.yMin, view.yMax, view.xMin, view.xMax);\n"
|
||||
<< " drawPacketMarkers(packetMarkers, box, view.xMin, view.xMax);\n"
|
||||
<< " drawTrace(plotCh1, '#005bbb', box, view.yMin, view.yMax, view.xMin, view.xMax);\n"
|
||||
<< " if (hasCh2) drawTrace(plotCh2, '#d62828', box, view.yMin, view.yMax, view.xMin, view.xMax);\n"
|
||||
<< " if (hasRss) drawTrace(plotRss, '#ee8a12', 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"
|
||||
<< " legendRss.style.display = hasRss ? '' : '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 = 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"
|
||||
<< " const firstPacket = data.firstPacketIndex || data.packetIndex;\n"
|
||||
<< " const lastPacket = data.lastPacketIndex || data.packetIndex;\n"
|
||||
<< " packetInfo.textContent = 'Packets #' + firstPacket + '..' + lastPacket + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);\n"
|
||||
<< " timingInfo.textContent = 'Window frames/ch: ' + data.framesPerChannel + ', window: ' + data.durationMs.toFixed(3) + ' ms, last packet: ' + data.latestPacketDurationMs.toFixed(3) + ' ms';\n"
|
||||
<< " packetRateInfo.textContent = 'Packets/s: ' + data.packetsPerSecond.toFixed(3);\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 + (data.fullResLive ? ' | adaptive live resolution on' : '') + (di1Grouped ? (' | DI1-group avg: ' + (data.groupedPointCount || 0) + ' pts') : '');\n"
|
||||
<< " statusText.textContent = 'Latest packet close reason: ' + data.closeReason + '. This page refreshes its rolling packet window every 500 ms.';\n"
|
||||
<< " }\n"
|
||||
<< " function applyLiveData(data) {\n"
|
||||
<< " if (!data || data.status === 'waiting') {\n"
|
||||
@ -554,6 +749,8 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
||||
<< " }\n"
|
||||
<< " zoomXIn.addEventListener('click', () => applyZoomX(0.5));\n"
|
||||
<< " zoomXOut.addEventListener('click', () => applyZoomX(2.0));\n"
|
||||
<< " panLeft.addEventListener('click', () => applyPanX(-1.0));\n"
|
||||
<< " panRight.addEventListener('click', () => applyPanX(1.0));\n"
|
||||
<< " zoomYIn.addEventListener('click', () => applyZoomY(0.5));\n"
|
||||
<< " zoomYOut.addEventListener('click', () => applyZoomY(2.0));\n"
|
||||
<< " resetZoom.addEventListener('click', () => resetView());\n"
|
||||
@ -578,6 +775,16 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
||||
<< " }\n"
|
||||
<< " }, { passive: false });\n"
|
||||
<< " canvas.addEventListener('dblclick', () => resetView());\n"
|
||||
<< " window.addEventListener('keydown', (event) => {\n"
|
||||
<< " if (!latestPacket) return;\n"
|
||||
<< " if (event.key === 'ArrowLeft') {\n"
|
||||
<< " event.preventDefault();\n"
|
||||
<< " applyPanX(-1.0);\n"
|
||||
<< " } else if (event.key === 'ArrowRight') {\n"
|
||||
<< " event.preventDefault();\n"
|
||||
<< " applyPanX(1.0);\n"
|
||||
<< " }\n"
|
||||
<< " });\n"
|
||||
<< " renderWaiting('Waiting for first packet...');\n"
|
||||
<< " loadLatestData();\n"
|
||||
<< " window.setInterval(loadLatestData, 500);\n"
|
||||
@ -595,11 +802,17 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
||||
CaptureFileWriter::CaptureFileWriter(std::string csv_path,
|
||||
std::string svg_path,
|
||||
std::string live_html_path,
|
||||
std::string live_json_path)
|
||||
std::string live_json_path,
|
||||
bool full_res_live,
|
||||
std::size_t live_history_packets,
|
||||
bool di1_group_average)
|
||||
: csv_path_(std::move(csv_path)),
|
||||
svg_path_(std::move(svg_path)),
|
||||
live_html_path_(std::move(live_html_path)),
|
||||
live_json_path_(std::move(live_json_path)) {}
|
||||
live_json_path_(std::move(live_json_path)),
|
||||
full_res_live_(full_res_live),
|
||||
live_history_packets_(live_history_packets),
|
||||
di1_group_average_(di1_group_average) {}
|
||||
|
||||
const std::string& CaptureFileWriter::live_html_path() const {
|
||||
return live_html_path_;
|
||||
@ -694,43 +907,142 @@ void CaptureFileWriter::append_csv_packet(const CapturePacket& packet,
|
||||
}
|
||||
}
|
||||
|
||||
void CaptureFileWriter::update_live_plot(const CapturePacket& packet,
|
||||
void CaptureFileWriter::update_live_plot(const std::deque<CapturePacket>& packets,
|
||||
std::size_t packets_seen,
|
||||
double packets_per_second,
|
||||
double frame_freq_hz,
|
||||
const std::string& close_reason,
|
||||
std::size_t zeroed_samples,
|
||||
std::size_t stored_samples) const {
|
||||
const std::size_t frames = packet_frame_count(packet);
|
||||
const double duration_ms = (frames == 0U) ? 0.0 : (1000.0 * static_cast<double>(frames) / frame_freq_hz);
|
||||
if (packets.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t begin_index =
|
||||
((live_history_packets_ != 0U) && (packets.size() > live_history_packets_))
|
||||
? (packets.size() - live_history_packets_)
|
||||
: 0U;
|
||||
const CapturePacket& latest_packet = packets.back();
|
||||
const std::size_t latest_frames = packet_frame_count(latest_packet);
|
||||
const double latest_duration_ms =
|
||||
(latest_frames == 0U) ? 0.0 : (1000.0 * static_cast<double>(latest_frames) / frame_freq_hz);
|
||||
const double zeroed_percent = (stored_samples == 0U)
|
||||
? 0.0
|
||||
: (100.0 * static_cast<double>(zeroed_samples) / static_cast<double>(stored_samples));
|
||||
const auto trace1 = build_min_max_trace(packet.ch1, kLivePlotMaxColumns);
|
||||
const auto trace2 = build_min_max_trace(packet.ch2, kLivePlotMaxColumns);
|
||||
const auto di1_trace = build_digital_step_trace(packet.di1);
|
||||
|
||||
std::size_t total_frames = 0;
|
||||
std::vector<std::pair<std::size_t, std::size_t>> packet_markers;
|
||||
std::vector<double> ch1_values;
|
||||
std::vector<double> ch2_values;
|
||||
std::vector<uint8_t> di1_values;
|
||||
|
||||
for (std::size_t i = begin_index; i < packets.size(); ++i) {
|
||||
total_frames += packet_frame_count(packets[i]);
|
||||
}
|
||||
|
||||
ch1_values.reserve(total_frames);
|
||||
if (packet_has_ch2(latest_packet)) {
|
||||
ch2_values.reserve(total_frames);
|
||||
}
|
||||
if (packet_has_di1_trace(latest_packet)) {
|
||||
di1_values.reserve(total_frames);
|
||||
}
|
||||
|
||||
std::size_t frame_offset = 0;
|
||||
bool first_packet = true;
|
||||
for (std::size_t i = begin_index; i < packets.size(); ++i) {
|
||||
const CapturePacket& packet = packets[i];
|
||||
const std::size_t frames = packet_frame_count(packet);
|
||||
if (frames == 0U) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!first_packet) {
|
||||
packet_markers.push_back({frame_offset, packet.packet_index});
|
||||
}
|
||||
first_packet = false;
|
||||
|
||||
ch1_values.insert(ch1_values.end(), packet.ch1.begin(), packet.ch1.begin() + static_cast<std::ptrdiff_t>(frames));
|
||||
if (packet_has_ch2(packet)) {
|
||||
ch2_values.insert(ch2_values.end(), packet.ch2.begin(), packet.ch2.begin() + static_cast<std::ptrdiff_t>(frames));
|
||||
}
|
||||
if (packet_has_di1_trace(packet)) {
|
||||
di1_values.insert(di1_values.end(), packet.di1.begin(), packet.di1.begin() + static_cast<std::ptrdiff_t>(frames));
|
||||
}
|
||||
frame_offset += frames;
|
||||
}
|
||||
|
||||
const double duration_ms = (total_frames == 0U) ? 0.0 : (1000.0 * static_cast<double>(total_frames) / frame_freq_hz);
|
||||
std::vector<double> rss_values;
|
||||
if (packet_has_ch2(latest_packet)) {
|
||||
rss_values.reserve(total_frames);
|
||||
for (std::size_t i = 0; i < total_frames; ++i) {
|
||||
const double ch1 = ch1_values[i];
|
||||
const double ch2 = ch2_values[i];
|
||||
rss_values.push_back(std::sqrt((ch1 * ch1) + (ch2 * ch2)));
|
||||
}
|
||||
}
|
||||
const auto trace1 = build_min_max_trace(ch1_values, kLivePlotMaxColumns);
|
||||
const auto trace2 = build_min_max_trace(ch2_values, kLivePlotMaxColumns);
|
||||
const auto rss_trace = build_min_max_trace(rss_values, kLivePlotMaxColumns);
|
||||
const auto di1_trace = build_digital_step_trace(di1_values);
|
||||
const bool di1_grouped = di1_group_average_ && !di1_values.empty();
|
||||
const auto grouped_traces = di1_grouped
|
||||
? build_di1_grouped_traces(ch1_values, ch2_values, di1_values, packet_has_ch2(latest_packet))
|
||||
: Di1GroupedTraces{};
|
||||
const auto trace1_mid = full_res_live_ ? build_min_max_trace(ch1_values, kLivePlotMidColumns) : std::vector<PlotPoint>{};
|
||||
const auto trace2_mid = full_res_live_ ? build_min_max_trace(ch2_values, kLivePlotMidColumns) : std::vector<PlotPoint>{};
|
||||
const auto rss_trace_mid = full_res_live_ ? build_min_max_trace(rss_values, kLivePlotMidColumns) : std::vector<PlotPoint>{};
|
||||
const auto trace1_high = full_res_live_
|
||||
? ((total_frames <= kLivePlotRawFrameLimit) ? build_full_trace(ch1_values) : build_min_max_trace(ch1_values, kLivePlotHighColumns))
|
||||
: std::vector<PlotPoint>{};
|
||||
const auto trace2_high = full_res_live_
|
||||
? ((total_frames <= kLivePlotRawFrameLimit) ? build_full_trace(ch2_values) : build_min_max_trace(ch2_values, kLivePlotHighColumns))
|
||||
: std::vector<PlotPoint>{};
|
||||
const auto rss_trace_high = full_res_live_
|
||||
? ((total_frames <= kLivePlotRawFrameLimit) ? build_full_trace(rss_values) : build_min_max_trace(rss_values, kLivePlotHighColumns))
|
||||
: std::vector<PlotPoint>{};
|
||||
|
||||
std::ostringstream json;
|
||||
json << std::fixed << std::setprecision(9);
|
||||
json << "{\n"
|
||||
<< " \"status\": \"packet\",\n"
|
||||
<< " \"packetIndex\": " << packet.packet_index << ",\n"
|
||||
<< " \"channelCount\": " << packet_channel_count(packet) << ",\n"
|
||||
<< " \"packetIndex\": " << latest_packet.packet_index << ",\n"
|
||||
<< " \"firstPacketIndex\": " << packets[begin_index].packet_index << ",\n"
|
||||
<< " \"lastPacketIndex\": " << latest_packet.packet_index << ",\n"
|
||||
<< " \"windowPacketCount\": " << (packets.size() - begin_index) << ",\n"
|
||||
<< " \"channelCount\": " << packet_channel_count(latest_packet) << ",\n"
|
||||
<< " \"packetsSeen\": " << packets_seen << ",\n"
|
||||
<< " \"packetsPerSecond\": " << packets_per_second << ",\n"
|
||||
<< " \"framesPerChannel\": " << frames << ",\n"
|
||||
<< " \"framesPerChannel\": " << total_frames << ",\n"
|
||||
<< " \"latestPacketFrames\": " << latest_frames << ",\n"
|
||||
<< " \"frameFreqHz\": " << frame_freq_hz << ",\n"
|
||||
<< " \"durationMs\": " << duration_ms << ",\n"
|
||||
<< " \"latestPacketDurationMs\": " << latest_duration_ms << ",\n"
|
||||
<< " \"closeReason\": \"" << close_reason << "\",\n"
|
||||
<< " \"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"
|
||||
<< " \"fullResLive\": " << (full_res_live_ ? "true" : "false") << ",\n"
|
||||
<< " \"di1Grouped\": " << (di1_grouped ? "true" : "false") << ",\n"
|
||||
<< " \"groupedPointCount\": " << grouped_traces.ch1.size() << ",\n"
|
||||
<< " \"hasDi1Trace\": " << (packet_has_di1_trace(latest_packet) ? "true" : "false") << ",\n"
|
||||
<< " \"di1Frames\": " << di1_values.size() << ",\n"
|
||||
<< " \"updatedAt\": \"packet " << latest_packet.packet_index << "\",\n"
|
||||
<< " \"ch1\": " << json_points(trace1, frame_freq_hz) << ",\n"
|
||||
<< " \"ch1Mid\": " << json_points(trace1_mid, frame_freq_hz) << ",\n"
|
||||
<< " \"ch1High\": " << json_points(trace1_high, frame_freq_hz) << ",\n"
|
||||
<< " \"ch2\": " << json_points(trace2, frame_freq_hz) << ",\n"
|
||||
<< " \"di1\": " << json_points(di1_trace, frame_freq_hz) << "\n"
|
||||
<< " \"ch2Mid\": " << json_points(trace2_mid, frame_freq_hz) << ",\n"
|
||||
<< " \"ch2High\": " << json_points(trace2_high, frame_freq_hz) << ",\n"
|
||||
<< " \"rss\": " << json_points(rss_trace, frame_freq_hz) << ",\n"
|
||||
<< " \"rssMid\": " << json_points(rss_trace_mid, frame_freq_hz) << ",\n"
|
||||
<< " \"rssHigh\": " << json_points(rss_trace_high, frame_freq_hz) << ",\n"
|
||||
<< " \"ch1Grouped\": " << json_points(grouped_traces.ch1, frame_freq_hz) << ",\n"
|
||||
<< " \"ch2Grouped\": " << json_points(grouped_traces.ch2, frame_freq_hz) << ",\n"
|
||||
<< " \"rssGrouped\": " << json_points(grouped_traces.rss, frame_freq_hz) << ",\n"
|
||||
<< " \"di1\": " << json_points(di1_trace, frame_freq_hz) << ",\n"
|
||||
<< " \"packetMarkers\": " << json_packet_markers(packet_markers, frame_freq_hz) << "\n"
|
||||
<< "}\n";
|
||||
|
||||
const std::string json_text = json.str();
|
||||
@ -777,7 +1089,7 @@ void CaptureFileWriter::finalize_csv_from_spool(double frame_freq_hz) const {
|
||||
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 << ",ch2_v,rss_v";
|
||||
}
|
||||
if (has_di1_trace || (file_header.has_di1_trace != 0U)) {
|
||||
csv << ",di1";
|
||||
@ -816,7 +1128,8 @@ void CaptureFileWriter::finalize_csv_from_spool(double frame_freq_hz) const {
|
||||
out << packet_index << "," << i << "," << global_frame_index << ","
|
||||
<< time_s << "," << packet_time_s << "," << ch1[i];
|
||||
if (channel_count >= 2U) {
|
||||
out << "," << ch2[i];
|
||||
const double rss = std::sqrt((ch1[i] * ch1[i]) + (ch2[i] * ch2[i]));
|
||||
out << "," << ch2[i] << "," << rss;
|
||||
}
|
||||
if (has_di1_trace || (file_header.has_di1_trace != 0U)) {
|
||||
out << "," << (has_di1_trace ? static_cast<unsigned>(di1[i]) : 0U);
|
||||
@ -830,7 +1143,7 @@ void CaptureFileWriter::finalize_csv_from_spool(double frame_freq_hz) const {
|
||||
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 << ",ch2_v,rss_v";
|
||||
}
|
||||
if (file_header.has_di1_trace != 0U) {
|
||||
csv << ",di1";
|
||||
@ -866,6 +1179,9 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
|
||||
if (packet_has_ch2(packet)) {
|
||||
min_y = std::min(min_y, packet.ch2[i]);
|
||||
max_y = std::max(max_y, packet.ch2[i]);
|
||||
const double rss = std::sqrt((packet.ch1[i] * packet.ch1[i]) + (packet.ch2[i] * packet.ch2[i]));
|
||||
min_y = std::min(min_y, rss);
|
||||
max_y = std::max(max_y, rss);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -944,13 +1260,25 @@ 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 rss_values = build_rss_values(packet);
|
||||
const auto trace_rss = build_min_max_trace(rss_values, 1200);
|
||||
const bool di1_grouped = di1_group_average_ && packet_has_di1_trace(packet);
|
||||
const auto grouped_traces = di1_grouped
|
||||
? build_di1_grouped_traces(packet.ch1, packet.ch2, packet.di1, packet_has_ch2(packet))
|
||||
: Di1GroupedTraces{};
|
||||
const auto trace_di1 = build_digital_step_trace(packet.di1);
|
||||
const auto& plot_trace1 = di1_grouped ? grouped_traces.ch1 : trace1;
|
||||
const auto& plot_trace2 = di1_grouped ? grouped_traces.ch2 : trace2;
|
||||
const auto& plot_trace_rss = di1_grouped ? grouped_traces.rss : trace_rss;
|
||||
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)
|
||||
<< polyline_points(plot_trace1, frame_offset, total_frames, min_y, max_y, left, top, plot_w, plot_h)
|
||||
<< "\"/>\n";
|
||||
if (packet_has_ch2(packet)) {
|
||||
file << " <polyline fill=\"none\" stroke=\"#d62828\" stroke-width=\"1.2\" points=\""
|
||||
<< polyline_points(trace2, frame_offset, total_frames, min_y, max_y, left, top, plot_w, plot_h)
|
||||
<< polyline_points(plot_trace2, frame_offset, total_frames, min_y, max_y, left, top, plot_w, plot_h)
|
||||
<< "\"/>\n";
|
||||
file << " <polyline fill=\"none\" stroke=\"#ee8a12\" stroke-width=\"1.2\" points=\""
|
||||
<< polyline_points(plot_trace_rss, frame_offset, total_frames, min_y, max_y, left, top, plot_w, plot_h)
|
||||
<< "\"/>\n";
|
||||
}
|
||||
if (packet_has_di1_trace(packet)) {
|
||||
@ -974,6 +1302,9 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
|
||||
if (has_di1_trace) {
|
||||
file << ", DI1 trace";
|
||||
}
|
||||
if (di1_group_average_ && has_di1_trace) {
|
||||
file << ", averaged by DI1 runs";
|
||||
}
|
||||
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";
|
||||
@ -1007,6 +1338,11 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
|
||||
<< "\" stroke=\"#d62828\" stroke-width=\"3\"/>\n";
|
||||
file << " <text x=\"" << (width - 110) << "\" y=\"" << (legend_y + 4)
|
||||
<< "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">CH2</text>\n";
|
||||
file << " <line x1=\"" << (width - 360) << "\" y1=\"" << legend_y
|
||||
<< "\" x2=\"" << (width - 320) << "\" y2=\"" << legend_y
|
||||
<< "\" stroke=\"#ee8a12\" stroke-width=\"3\"/>\n";
|
||||
file << " <text x=\"" << (width - 310) << "\" y=\"" << (legend_y + 4)
|
||||
<< "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">sqrt(CH1^2+CH2^2)</text>\n";
|
||||
}
|
||||
if (has_di1_trace) {
|
||||
file << " <line x1=\"" << (width - 80) << "\" y1=\"" << legend_y
|
||||
|
||||
Reference in New Issue
Block a user