debug: figuring out whats wrong with waveform viewer
This commit is contained in:
@@ -191,6 +191,14 @@ rectime_seconds = int(round((total_samples - pretrig_samples) / sample_rate))
|
|||||||
`_decode_a5_waveform` uses `sample_rate=1024` as a default; the server overrides with
|
`_decode_a5_waveform` uses `sample_rate=1024` as a default; the server overrides with
|
||||||
`compliance_config.sample_rate` when available. Do NOT use `strt[18]` for rectime.
|
`compliance_config.sample_rate` when available. Do NOT use `strt[18]` for rectime.
|
||||||
|
|
||||||
|
**Sanity check — pretrig_samples must be less than total_samples:**
|
||||||
|
If `pretrig_samples >= total_samples` the STRT parse is invalid. Possible causes:
|
||||||
|
DLE-stuffed `0x10` byte within prev_key4 or key4 shifted field offsets, or a different
|
||||||
|
STRT record variant. `_decode_a5_waveform` logs `raw strt[0:21]` at WARNING level and
|
||||||
|
clamps `pretrig_samples = 0` so the viewer renders (showing the full waveform from t=0).
|
||||||
|
Observed once (2026-04-14) with `strt[16:18] = 0x41 0x01` → pretrig=16641 (impossible).
|
||||||
|
Root cause not yet identified — capture the warning log hex dump to diagnose.
|
||||||
|
|
||||||
### SUB 5A — end-of-stream signal (confirmed 2026-04-06)
|
### SUB 5A — end-of-stream signal (confirmed 2026-04-06)
|
||||||
|
|
||||||
After streaming all waveform chunks, the device sends exactly **1 raw byte** in response to
|
After streaming all waveform chunks, the device sends exactly **1 raw byte** in response to
|
||||||
|
|||||||
+15
-1
@@ -1395,6 +1395,18 @@ def _decode_a5_waveform(
|
|||||||
total_samples = struct.unpack_from(">H", strt, 14)[0]
|
total_samples = struct.unpack_from(">H", strt, 14)[0]
|
||||||
pretrig_samples = struct.unpack_from(">H", strt, 16)[0]
|
pretrig_samples = struct.unpack_from(">H", strt, 16)[0]
|
||||||
|
|
||||||
|
# Sanity check: pretrig must be less than total_samples.
|
||||||
|
# If not, the STRT layout is suspect (DLE-stuffing shift, different record variant, etc.).
|
||||||
|
# Log the raw bytes for diagnosis and clamp pretrig to 0 so the viewer renders.
|
||||||
|
if pretrig_samples >= total_samples:
|
||||||
|
log.warning(
|
||||||
|
"_decode_a5_waveform: pretrig_samples=%d >= total_samples=%d — "
|
||||||
|
"STRT layout suspect. Raw strt[0:21]: %s "
|
||||||
|
"Clamping pretrig to 0 for rendering.",
|
||||||
|
pretrig_samples, total_samples, strt[0:21].hex(' '),
|
||||||
|
)
|
||||||
|
pretrig_samples = 0
|
||||||
|
|
||||||
# strt[18] is a record-mode/type byte, NOT rectime in seconds.
|
# strt[18] is a record-mode/type byte, NOT rectime in seconds.
|
||||||
# Confirmed from analysis of 4-9-26 ACH capture (15 distinct events):
|
# Confirmed from analysis of 4-9-26 ACH capture (15 distinct events):
|
||||||
# flags=0xFFFE (single-shot) → strt[18]=0x46 ('F') for all events regardless of duration
|
# flags=0xFFFE (single-shot) → strt[18]=0x46 ('F') for all events regardless of duration
|
||||||
@@ -1412,8 +1424,10 @@ def _decode_a5_waveform(
|
|||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
"_decode_a5_waveform: STRT total_samples=%d pretrig=%d "
|
"_decode_a5_waveform: STRT total_samples=%d pretrig=%d "
|
||||||
"strt[18]=0x%02X (mode byte, not seconds) computed_rectime=%ds",
|
"strt[18]=0x%02X (mode byte, not seconds) computed_rectime=%ds "
|
||||||
|
"raw strt[0:21]: %s",
|
||||||
total_samples, pretrig_samples, strt[18], rectime_seconds,
|
total_samples, pretrig_samples, strt[18], rectime_seconds,
|
||||||
|
strt[0:21].hex(' '),
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Collect per-frame waveform bytes with global offset tracking ─────────
|
# ── Collect per-frame waveform bytes with global offset tracking ─────────
|
||||||
|
|||||||
+95
-87
@@ -573,95 +573,103 @@
|
|||||||
renderData = plotSamples.filter((_, i) => i % step === 0);
|
renderData = plotSamples.filter((_, i) => i % step === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const chart = new Chart(canvas, {
|
let chart;
|
||||||
type: 'line',
|
try {
|
||||||
data: {
|
chart = new Chart(canvas, {
|
||||||
labels: renderTimes,
|
type: 'line',
|
||||||
datasets: [{
|
data: {
|
||||||
data: renderData,
|
labels: renderTimes,
|
||||||
borderColor: color,
|
datasets: [{
|
||||||
borderWidth: 1,
|
data: renderData,
|
||||||
pointRadius: 0,
|
borderColor: color,
|
||||||
tension: 0,
|
borderWidth: 1,
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
animation: false,
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
callbacks: {
|
||||||
|
title: items => `t = ${items[0].label} ms`,
|
||||||
|
label: item => tooltipFmt(item.raw),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'category',
|
||||||
|
ticks: {
|
||||||
|
color: '#484f58',
|
||||||
|
maxTicksLimit: 10,
|
||||||
|
maxRotation: 0,
|
||||||
|
callback: (val, i) => renderTimes[i] + ' ms',
|
||||||
|
},
|
||||||
|
grid: { color: '#21262d' },
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
// Clamp geo-channel y-axis to ±(0C peak × 1.4) so near-saturation
|
||||||
|
// decode artifacts (which inflate autoscale to full range) don't
|
||||||
|
// squash the actual blast signal into an invisible flat line.
|
||||||
|
// The 0C peak value is authoritative for the true signal amplitude.
|
||||||
|
// Guard: only apply if peak0C is a valid finite positive number.
|
||||||
|
...(isGeo && peak0C !== null && peak0C !== undefined
|
||||||
|
&& isFinite(peak0C) && peak0C > 0 ? {
|
||||||
|
min: -(peak0C * 1.4),
|
||||||
|
max: (peak0C * 1.4),
|
||||||
|
} : {}),
|
||||||
|
ticks: {
|
||||||
|
color: '#484f58',
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
callback: v => tickFmt(v),
|
||||||
|
},
|
||||||
|
grid: { color: '#21262d' },
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: yUnit,
|
||||||
|
color: '#484f58',
|
||||||
|
font: { size: 10 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [{
|
||||||
|
// Draw trigger line at t=0
|
||||||
|
id: 'triggerLine',
|
||||||
|
afterDraw(chart) {
|
||||||
|
const ctx = chart.ctx;
|
||||||
|
const xAxis = chart.scales.x;
|
||||||
|
const yAxis = chart.scales.y;
|
||||||
|
|
||||||
|
// Find index of t=0
|
||||||
|
const zeroIdx = renderTimes.findIndex(t => parseFloat(t) >= 0);
|
||||||
|
if (zeroIdx < 0) return;
|
||||||
|
|
||||||
|
const x = xAxis.getPixelForValue(zeroIdx);
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, yAxis.top);
|
||||||
|
ctx.lineTo(x, yAxis.bottom);
|
||||||
|
ctx.strokeStyle = 'rgba(248, 81, 73, 0.7)';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.setLineDash([4, 3]);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
},
|
||||||
}],
|
}],
|
||||||
},
|
});
|
||||||
options: {
|
} catch (err) {
|
||||||
animation: false,
|
console.error(`Chart.js error for channel ${ch}:`, err);
|
||||||
responsive: true,
|
canvasWrap.innerHTML = `<p style="color:#f85149;padding:8px;font-size:11px;">Chart error: ${err.message}</p>`;
|
||||||
maintainAspectRatio: false,
|
}
|
||||||
plugins: {
|
|
||||||
legend: { display: false },
|
|
||||||
tooltip: {
|
|
||||||
mode: 'index',
|
|
||||||
intersect: false,
|
|
||||||
callbacks: {
|
|
||||||
title: items => `t = ${items[0].label} ms`,
|
|
||||||
label: item => tooltipFmt(item.raw),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'category',
|
|
||||||
ticks: {
|
|
||||||
color: '#484f58',
|
|
||||||
maxTicksLimit: 10,
|
|
||||||
maxRotation: 0,
|
|
||||||
callback: (val, i) => renderTimes[i] + ' ms',
|
|
||||||
},
|
|
||||||
grid: { color: '#21262d' },
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
// Clamp geo-channel y-axis to ±(0C peak × 1.4) so near-saturation
|
|
||||||
// decode artifacts (which inflate autoscale to full range) don't
|
|
||||||
// squash the actual blast signal into an invisible flat line.
|
|
||||||
// The 0C peak value is authoritative for the true signal amplitude.
|
|
||||||
...(isGeo && peak0C !== null && peak0C > 0 ? {
|
|
||||||
min: -(peak0C * 1.4),
|
|
||||||
max: (peak0C * 1.4),
|
|
||||||
} : {}),
|
|
||||||
ticks: {
|
|
||||||
color: '#484f58',
|
|
||||||
maxTicksLimit: 5,
|
|
||||||
callback: v => tickFmt(v),
|
|
||||||
},
|
|
||||||
grid: { color: '#21262d' },
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: yUnit,
|
|
||||||
color: '#484f58',
|
|
||||||
font: { size: 10 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [{
|
|
||||||
// Draw trigger line at t=0
|
|
||||||
id: 'triggerLine',
|
|
||||||
afterDraw(chart) {
|
|
||||||
const ctx = chart.ctx;
|
|
||||||
const xAxis = chart.scales.x;
|
|
||||||
const yAxis = chart.scales.y;
|
|
||||||
|
|
||||||
// Find index of t=0
|
if (chart) charts[ch] = chart;
|
||||||
const zeroIdx = renderTimes.findIndex(t => parseFloat(t) >= 0);
|
|
||||||
if (zeroIdx < 0) return;
|
|
||||||
|
|
||||||
const x = xAxis.getPixelForValue(zeroIdx);
|
|
||||||
ctx.save();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, yAxis.top);
|
|
||||||
ctx.lineTo(x, yAxis.bottom);
|
|
||||||
ctx.strokeStyle = 'rgba(248, 81, 73, 0.7)';
|
|
||||||
ctx.lineWidth = 1.5;
|
|
||||||
ctx.setLineDash([4, 3]);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.restore();
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
|
|
||||||
charts[ch] = chart;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user