Compare commits
10 Commits
b5a76d1f7c
...
f0301ba4af
| Author | SHA1 | Date | |
|---|---|---|---|
| f0301ba4af | |||
| e09d3ca64f | |||
| 94deb6c881 | |||
| 5fa0b9ac39 | |||
| f916aa25c3 | |||
| 28001aca35 | |||
| 7cf94c1512 | |||
| 52b5ef1a85 | |||
| 887e2f6730 | |||
| 8710ed8b39 |
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# Build artifacts
|
||||
*.exe
|
||||
*.obj
|
||||
*.pdb
|
||||
*.ilk
|
||||
*.idb
|
||||
|
||||
# Generated capture outputs
|
||||
*.capture_spool.bin
|
||||
*.csv
|
||||
*.svg
|
||||
*.json
|
||||
*.html
|
||||
live_*.js
|
||||
20
Makefile
Normal file
@ -0,0 +1,20 @@
|
||||
TARGET = main.exe
|
||||
BUILDDIR = build
|
||||
SOURCES = main.cpp capture_file_writer.cpp tty_protocol_writer.cpp
|
||||
OBJS = $(BUILDDIR)\main.obj $(BUILDDIR)\capture_file_writer.obj $(BUILDDIR)\tty_protocol_writer.obj
|
||||
CXXFLAGS = /nologo /std:c++17 /EHsc /O2 /I.
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SOURCES)
|
||||
if not exist $(BUILDDIR) mkdir $(BUILDDIR)
|
||||
cl $(CXXFLAGS) /Fo$(BUILDDIR)\ /Fd$(BUILDDIR)\ /Fe$(TARGET) $(SOURCES)
|
||||
|
||||
rebuild: clean all
|
||||
|
||||
clean:
|
||||
-del /Q $(TARGET) 2>nul
|
||||
-if exist $(BUILDDIR) rmdir /S /Q $(BUILDDIR)
|
||||
|
||||
run-help: $(TARGET)
|
||||
$(TARGET) help
|
||||
@ -1,14 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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 {
|
||||
@ -16,15 +20,22 @@ public:
|
||||
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);
|
||||
|
||||
void write(const std::vector<CapturePacket>& packets,
|
||||
double frame_freq_hz,
|
||||
double nominal_range_v) const;
|
||||
|
||||
void initialize_live_plot() 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;
|
||||
|
||||
void update_live_plot(const CapturePacket& packet,
|
||||
void update_live_plot(const std::deque<CapturePacket>& packets,
|
||||
std::size_t packets_seen,
|
||||
double packets_per_second,
|
||||
double frame_freq_hz,
|
||||
@ -36,9 +47,7 @@ public:
|
||||
const std::string& live_json_path() const;
|
||||
|
||||
private:
|
||||
void write_csv(const std::vector<CapturePacket>& packets,
|
||||
double frame_freq_hz) const;
|
||||
|
||||
void finalize_csv_from_spool(double frame_freq_hz) const;
|
||||
void write_svg(const std::vector<CapturePacket>& packets,
|
||||
double frame_freq_hz,
|
||||
double nominal_range_v) const;
|
||||
@ -47,4 +56,7 @@ private:
|
||||
std::string svg_path_;
|
||||
std::string live_html_path_;
|
||||
std::string live_json_path_;
|
||||
bool full_res_live_ = false;
|
||||
std::size_t live_history_packets_ = 8;
|
||||
bool di1_group_average_ = false;
|
||||
};
|
||||
|
||||
BIN
capture_file_writer.obj
Normal file
BIN
gate_capture.exe
1001
internal_default.csv
Normal file
58
internal_default.svg
Normal file
|
After Width: | Height: | Size: 38 KiB |
426
live_plot.html
Normal file
@ -0,0 +1,426 @@
|
||||
<!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 a rolling continuous packet window.</div>
|
||||
<div class="controls">
|
||||
<button type="button" id="zoomXIn">X+</button>
|
||||
<button type="button" id="zoomXOut">X-</button>
|
||||
<button type="button" id="panLeft">Left</button>
|
||||
<button type="button" id="panRight">Right</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, 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>
|
||||
</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="legendRss"><span class="sw" style="background:#ee8a12"></span>sqrt(CH1^2 + CH2^2)</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 legendRss = document.getElementById('legendRss');
|
||||
const legendDi1 = document.getElementById('legendDi1');
|
||||
const zoomXIn = document.getElementById('zoomXIn');
|
||||
const zoomXOut = document.getElementById('zoomXOut');
|
||||
const panLeft = document.getElementById('panLeft');
|
||||
const panRight = document.getElementById('panRight');
|
||||
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 applyPanX(direction) {
|
||||
if (!latestAutoBounds || !latestPacket) return;
|
||||
const current = currentViewBounds(latestAutoBounds);
|
||||
const totalX = Math.max(1e-9, latestAutoBounds.maxT);
|
||||
const span = Math.max(1e-9, current.xMax - current.xMin);
|
||||
if (span >= totalX) return;
|
||||
const shift = span * 0.25 * direction;
|
||||
let newMin = current.xMin + shift;
|
||||
let newMax = current.xMax + shift;
|
||||
if (newMin < 0) {
|
||||
newMax -= newMin;
|
||||
newMin = 0;
|
||||
}
|
||||
if (newMax > totalX) {
|
||||
newMin -= (newMax - totalX);
|
||||
newMax = totalX;
|
||||
}
|
||||
newMin = Math.max(0, newMin);
|
||||
newMax = Math.min(totalX, newMax);
|
||||
viewState = { auto: false, xMin: newMin, xMax: newMax, yMin: current.yMin, yMax: current.yMax };
|
||||
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 drawPacketMarkers(markers, box, xMin, xMax) {
|
||||
if (!Array.isArray(markers) || markers.length === 0) return;
|
||||
const spanT = Math.max(1e-9, xMax - xMin);
|
||||
ctx.save();
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.strokeStyle = '#9db0c2';
|
||||
ctx.lineWidth = 1.0;
|
||||
ctx.fillStyle = '#5c6f82';
|
||||
ctx.font = '11px Segoe UI';
|
||||
markers.forEach((m) => {
|
||||
if (!m || !Number.isFinite(m.t) || (m.t < xMin) || (m.t > xMax)) return;
|
||||
const x = box.left + ((m.t - xMin) / spanT) * box.width;
|
||||
ctx.beginPath(); ctx.moveTo(x, box.top); ctx.lineTo(x, box.top + box.height); ctx.stroke();
|
||||
if (m.label) ctx.fillText(m.label, x + 4, box.top + 16);
|
||||
});
|
||||
ctx.restore();
|
||||
}
|
||||
function pickTrace(base, mid, high, maxT, view) {
|
||||
if (!Array.isArray(base) || base.length === 0) return [];
|
||||
const totalT = Math.max(1e-9, maxT || 0);
|
||||
const spanT = Math.max(1e-9, view.xMax - view.xMin);
|
||||
const ratio = spanT / totalT;
|
||||
if (ratio <= 0.10 && Array.isArray(high) && high.length > 0) return high;
|
||||
if (ratio <= 0.35 && Array.isArray(mid) && mid.length > 0) return mid;
|
||||
return base;
|
||||
}
|
||||
function renderWaiting(message) {
|
||||
packetInfo.textContent = 'Waiting for packets...';
|
||||
timingInfo.textContent = '';
|
||||
zeroInfo.textContent = '';
|
||||
zoomInfo.textContent = 'View: auto';
|
||||
packetRateInfo.textContent = '';
|
||||
legendCh2.style.display = '';
|
||||
legendRss.style.display = 'none';
|
||||
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 ch1Mid = data.ch1Mid || [];
|
||||
const ch1High = data.ch1High || [];
|
||||
const ch2 = data.ch2 || [];
|
||||
const ch2Mid = data.ch2Mid || [];
|
||||
const ch2High = data.ch2High || [];
|
||||
const rss = data.rss || [];
|
||||
const rssMid = data.rssMid || [];
|
||||
const rssHigh = data.rssHigh || [];
|
||||
const ch1Grouped = data.ch1Grouped || [];
|
||||
const ch2Grouped = data.ch2Grouped || [];
|
||||
const rssGrouped = data.rssGrouped || [];
|
||||
const di1 = data.di1 || [];
|
||||
const packetMarkers = data.packetMarkers || [];
|
||||
const di1Grouped = !!data.di1Grouped;
|
||||
const hasCh2 = (data.channelCount || 2) > 1 && ch2.length > 0;
|
||||
const hasRss = hasCh2 && rss.length > 0;
|
||||
const hasDi1Trace = !!data.hasDi1Trace && di1.length > 0;
|
||||
const values = [];
|
||||
const yCh1 = di1Grouped && ch1Grouped.length > 0 ? ch1Grouped : ch1;
|
||||
const yCh2 = di1Grouped && ch2Grouped.length > 0 ? ch2Grouped : ch2;
|
||||
const yRss = di1Grouped && rssGrouped.length > 0 ? rssGrouped : rss;
|
||||
yCh1.forEach(p => values.push(p[1]));
|
||||
if (hasCh2) yCh2.forEach(p => values.push(p[1]));
|
||||
if (hasRss) yRss.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 plotCh1 = di1Grouped ? ch1Grouped : pickTrace(ch1, ch1Mid, ch1High, maxT, view);
|
||||
const plotCh2 = hasCh2 ? (di1Grouped ? ch2Grouped : pickTrace(ch2, ch2Mid, ch2High, maxT, view)) : [];
|
||||
const plotRss = hasRss ? (di1Grouped ? rssGrouped : pickTrace(rss, rssMid, rssHigh, maxT, view)) : [];
|
||||
const box = drawAxes(view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
drawPacketMarkers(packetMarkers, box, view.xMin, view.xMax);
|
||||
drawTrace(plotCh1, '#005bbb', box, view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
if (hasCh2) drawTrace(plotCh2, '#d62828', box, view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
if (hasRss) drawTrace(plotRss, '#ee8a12', box, view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
if (hasDi1Trace) drawDigitalTrace(di1, box, view.xMin, view.xMax);
|
||||
legendCh2.style.display = hasCh2 ? '' : 'none';
|
||||
legendRss.style.display = hasRss ? '' : 'none';
|
||||
legendDi1.style.display = hasDi1Trace ? '' : 'none';
|
||||
refreshAutoCheckbox();
|
||||
const firstPacket = data.firstPacketIndex || data.packetIndex;
|
||||
const lastPacket = data.lastPacketIndex || data.packetIndex;
|
||||
packetInfo.textContent = 'Packets #' + firstPacket + '..' + lastPacket + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);
|
||||
timingInfo.textContent = 'Window frames/ch: ' + data.framesPerChannel + ', window: ' + data.durationMs.toFixed(3) + ' ms, last packet: ' + data.latestPacketDurationMs.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 + (data.fullResLive ? ' | adaptive live resolution on' : '') + (di1Grouped ? (' | DI1-group avg: ' + (data.groupedPointCount || 0) + ' pts') : '');
|
||||
statusText.textContent = 'Latest packet close reason: ' + data.closeReason + '. This page refreshes its rolling packet window 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));
|
||||
panLeft.addEventListener('click', () => applyPanX(-1.0));
|
||||
panRight.addEventListener('click', () => applyPanX(1.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());
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (!latestPacket) return;
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
applyPanX(-1.0);
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
applyPanX(1.0);
|
||||
}
|
||||
});
|
||||
renderWaiting('Waiting for first packet...');
|
||||
loadLatestData();
|
||||
window.setInterval(loadLatestData, 500);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
40
live_plot.js
Normal file
39
live_plot.json
Normal file
5001
live_test.csv
Normal file
147
live_test.html
Normal file
15
live_test.json
Normal file
59
live_test.svg
Normal file
|
After Width: | Height: | Size: 66 KiB |
25001
overflow_test.csv
Normal file
152
overflow_test.html
Normal file
16
overflow_test.json
Normal file
80
overflow_test.svg
Normal file
|
After Width: | Height: | Size: 325 KiB |
1279
packets.svg
Normal file
|
After Width: | Height: | Size: 22 MiB |
113704
packets_test.csv
Normal file
70
packets_test.svg
Normal file
|
After Width: | Height: | Size: 231 KiB |
10001
pps_test.csv
Normal file
152
pps_test.html
Normal file
16
pps_test.json
Normal file
65
pps_test.svg
Normal file
|
After Width: | Height: | Size: 134 KiB |
5001
quick_test.csv
Normal file
60
quick_test.svg
Normal file
|
After Width: | Height: | Size: 70 KiB |
1
run exmple.txt
Normal file
@ -0,0 +1 @@
|
||||
.\main.exe 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.svg
|
||||
48
test.svg
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 115 KiB |
128
tty_protocol_writer.cpp
Normal file
@ -0,0 +1,128 @@
|
||||
#include "tty_protocol_writer.h"
|
||||
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
TtyProtocolWriter::TtyProtocolWriter(std::string path) : path_(std::move(path)) {
|
||||
throw std::runtime_error("tty output is supported only on Linux/POSIX");
|
||||
}
|
||||
|
||||
TtyProtocolWriter::~TtyProtocolWriter() = default;
|
||||
|
||||
TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept = default;
|
||||
|
||||
TtyProtocolWriter& TtyProtocolWriter::operator=(TtyProtocolWriter&& other) noexcept = default;
|
||||
|
||||
void TtyProtocolWriter::emit_packet_start() const {}
|
||||
|
||||
void TtyProtocolWriter::emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) const {
|
||||
(void) index;
|
||||
(void) ch1_avg;
|
||||
(void) ch2_avg;
|
||||
}
|
||||
|
||||
const std::string& TtyProtocolWriter::path() const {
|
||||
return path_;
|
||||
}
|
||||
|
||||
void TtyProtocolWriter::write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const {
|
||||
(void) word0;
|
||||
(void) word1;
|
||||
(void) word2;
|
||||
(void) word3;
|
||||
}
|
||||
|
||||
void TtyProtocolWriter::close_fd() noexcept {}
|
||||
|
||||
#else
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <sstream>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string io_error(const std::string& action, const std::string& path) {
|
||||
std::ostringstream out;
|
||||
out << action << " '" << path << "': " << std::strerror(errno);
|
||||
return out.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TtyProtocolWriter::TtyProtocolWriter(std::string path) : path_(std::move(path)) {
|
||||
fd_ = ::open(path_.c_str(), O_WRONLY | O_NOCTTY);
|
||||
if (fd_ < 0) {
|
||||
throw std::runtime_error(io_error("Cannot open tty output", path_));
|
||||
}
|
||||
}
|
||||
|
||||
TtyProtocolWriter::~TtyProtocolWriter() {
|
||||
close_fd();
|
||||
}
|
||||
|
||||
TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept
|
||||
: path_(std::move(other.path_)),
|
||||
fd_(other.fd_) {
|
||||
other.fd_ = -1;
|
||||
}
|
||||
|
||||
TtyProtocolWriter& TtyProtocolWriter::operator=(TtyProtocolWriter&& other) noexcept {
|
||||
if (this != &other) {
|
||||
close_fd();
|
||||
path_ = std::move(other.path_);
|
||||
fd_ = other.fd_;
|
||||
other.fd_ = -1;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void TtyProtocolWriter::emit_packet_start() const {
|
||||
write_frame(0x000A, 0xFFFF, 0xFFFF, 0xFFFF);
|
||||
}
|
||||
|
||||
void TtyProtocolWriter::emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) const {
|
||||
write_frame(0x000A,
|
||||
index,
|
||||
static_cast<uint16_t>(ch1_avg),
|
||||
static_cast<uint16_t>(ch2_avg));
|
||||
}
|
||||
|
||||
const std::string& TtyProtocolWriter::path() const {
|
||||
return path_;
|
||||
}
|
||||
|
||||
void TtyProtocolWriter::write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const {
|
||||
const uint16_t frame[4] = {word0, word1, word2, word3};
|
||||
const std::uint8_t* bytes = reinterpret_cast<const std::uint8_t*>(frame);
|
||||
std::size_t remaining = sizeof(frame);
|
||||
|
||||
while (remaining != 0U) {
|
||||
const ssize_t written = ::write(fd_, bytes, remaining);
|
||||
if (written < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
throw std::runtime_error(io_error("Cannot write tty frame to", path_));
|
||||
}
|
||||
if (written == 0) {
|
||||
throw std::runtime_error("tty write returned 0 bytes for '" + path_ + "'");
|
||||
}
|
||||
bytes += static_cast<std::size_t>(written);
|
||||
remaining -= static_cast<std::size_t>(written);
|
||||
}
|
||||
}
|
||||
|
||||
void TtyProtocolWriter::close_fd() noexcept {
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
29
tty_protocol_writer.h
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
class TtyProtocolWriter {
|
||||
public:
|
||||
explicit TtyProtocolWriter(std::string path);
|
||||
~TtyProtocolWriter();
|
||||
|
||||
TtyProtocolWriter(const TtyProtocolWriter&) = delete;
|
||||
TtyProtocolWriter& operator=(const TtyProtocolWriter&) = delete;
|
||||
TtyProtocolWriter(TtyProtocolWriter&& other) noexcept;
|
||||
TtyProtocolWriter& operator=(TtyProtocolWriter&& other) noexcept;
|
||||
|
||||
void emit_packet_start() const;
|
||||
void emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) const;
|
||||
|
||||
const std::string& path() const;
|
||||
|
||||
private:
|
||||
void write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const;
|
||||
void close_fd() noexcept;
|
||||
|
||||
std::string path_;
|
||||
#ifndef _WIN32
|
||||
int fd_ = -1;
|
||||
#endif
|
||||
};
|
||||