111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
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<number, number> = {
|
|
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]);
|
|
}
|