From 4f4c1a8f64e26b662a79ea57cea54e5d630aaead Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Tue, 14 Apr 2026 16:00:14 -0400 Subject: [PATCH] debug: figuring out whats wrong with waveform viewer --- CLAUDE.md | 8 ++ minimateplus/client.py | 16 +++- sfm/waveform_viewer.html | 182 ++++++++++++++++++++------------------- 3 files changed, 118 insertions(+), 88 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a90d4b3..399fc3f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `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) After streaming all waveform chunks, the device sends exactly **1 raw byte** in response to diff --git a/minimateplus/client.py b/minimateplus/client.py index 4604a42..c108b95 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -1395,6 +1395,18 @@ def _decode_a5_waveform( total_samples = struct.unpack_from(">H", strt, 14)[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. # 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 @@ -1412,8 +1424,10 @@ def _decode_a5_waveform( log.debug( "_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, + strt[0:21].hex(' '), ) # ── Collect per-frame waveform bytes with global offset tracking ───────── diff --git a/sfm/waveform_viewer.html b/sfm/waveform_viewer.html index a2880c8..877a259 100644 --- a/sfm/waveform_viewer.html +++ b/sfm/waveform_viewer.html @@ -573,95 +573,103 @@ renderData = plotSamples.filter((_, i) => i % step === 0); } - const chart = new Chart(canvas, { - type: 'line', - data: { - labels: renderTimes, - datasets: [{ - data: renderData, - borderColor: color, - borderWidth: 1, - pointRadius: 0, - tension: 0, + let chart; + try { + chart = new Chart(canvas, { + type: 'line', + data: { + labels: renderTimes, + datasets: [{ + data: renderData, + borderColor: color, + 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: { - 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. - ...(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; + }); + } catch (err) { + console.error(`Chart.js error for channel ${ch}:`, err); + canvasWrap.innerHTML = `

Chart error: ${err.message}

`; + } - // 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(); - }, - }], - }); - - charts[ch] = chart; + if (chart) charts[ch] = chart; } }