TriloopTem_App/src/hooks/useWaveform.ts
2026-06-19 22:08:10 +08:00

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]);
}