import { useMemo } from 'react'; import { useDataStore } from '../stores/dataStore'; import { useDeviceStore } from '../stores/deviceStore'; import type { MeasurementFrame } from '../protocol/types'; const MAX_DISPLAY_POINTS = 512; // sampleFreq code → Hz (from SAMPLE_FREQ_TABLE) const SAMPLE_FREQ_HZ: Record = { 0x00: 250000, 0x01: 125000, 0x02: 62500, 0x03: 31250, 0x04: 15600, 0x05: 7800, 0x06: 3900, 0x07: 1950, 0x08: 977, 0x09: 488, 0x0a: 244, 0x0b: 122, 0x0c: 61, }; // Peak-hold downsample: preserves signal envelope when reducing points. function downsample(data: Float64Array, targetLen: number): Float64Array { if (data.length <= targetLen) return data; const ratio = data.length / targetLen; const out = new Float64Array(targetLen); for (let i = 0; i < targetLen; i++) { const start = Math.floor(i * ratio); const end = Math.min(Math.floor((i + 1) * ratio), data.length); let maxAbs = 0; let maxVal = 0; for (let j = start; j < end; j++) { if (Math.abs(data[j]) > maxAbs) { maxAbs = Math.abs(data[j]); maxVal = data[j]; } } out[i] = maxVal; } return out; } export interface WaveformData { channels: Float64Array[]; // downsampled μV per channel timeMs: Float64Array; // time axis in ms (physical) totalTimeMs: number; // total window duration in ms minLogUV: number; // log10(min |value|) for log scale maxLogUV: number; // log10(max |value|) for log scale minUV: number; // actual min value (signed) for linear scale maxUV: number; // actual max value (signed) for linear scale sampleDepth: number; } export function computeWaveformData( frame: MeasurementFrame, visibleChannels: boolean[], sampleFreqCode: number, ): WaveformData | null { const { adcUV } = frame; const sampleDepth = adcUV[0]?.length ?? 0; if (sampleDepth === 0) return null; const sampleHz = SAMPLE_FREQ_HZ[sampleFreqCode] ?? 31250; const displayLen = Math.min(sampleDepth, MAX_DISPLAY_POINTS); // Physical time axis const timeMs = new Float64Array(displayLen); for (let i = 0; i < displayLen; i++) { timeMs[i] = (i / sampleHz) * 1000; } const totalTimeMs = ((displayLen - 1) / sampleHz) * 1000; let logMin = Infinity; let logMax = -Infinity; let linMin = Infinity; let linMax = -Infinity; const channels: Float64Array[] = adcUV.map((ch, idx) => { if (!visibleChannels[idx]) return new Float64Array(displayLen); const ds = downsample(ch, displayLen); for (let i = 0; i < ds.length; i++) { const v = ds[i]; const absV = Math.abs(v); if (absV > 1e-9) { logMin = Math.min(logMin, absV); logMax = Math.max(logMax, absV); } linMin = Math.min(linMin, v); linMax = Math.max(linMax, v); } return ds; }); if (!isFinite(logMin)) logMin = 1e-3; if (!isFinite(logMax)) logMax = 1e6; if (!isFinite(linMin)) linMin = -1; if (!isFinite(linMax)) linMax = 1; // Symmetric linear range for cleaner display const linPeak = Math.max(Math.abs(linMin), Math.abs(linMax), 1); linMin = -linPeak; linMax = linPeak; return { channels, timeMs, totalTimeMs, minLogUV: Math.log10(logMin), maxLogUV: Math.log10(logMax), minUV: linMin, maxUV: linMax, sampleDepth, }; } export function useWaveform(visibleChannels: boolean[]): WaveformData | null { const frame = useDataStore((s) => s.currentFrame); const sampleFreqCode = useDeviceStore((s) => s.config.sampleFreq); return useMemo(() => { if (!frame) return null; return computeWaveformData(frame, visibleChannels, sampleFreqCode); }, [frame, visibleChannels, sampleFreqCode]); }