diff --git a/app/(tabs)/records.tsx b/app/(tabs)/records.tsx index 626fe5f..8e3d999 100644 --- a/app/(tabs)/records.tsx +++ b/app/(tabs)/records.tsx @@ -192,6 +192,8 @@ export default function RecordsScreen() { await resumeSession(newSessionId, projectId); }; + const sampleFreqCode = useDeviceStore((s) => s.config.sampleFreq); + const handleFramePress = async (pf: PersistedFrame) => { if (!pf.binPath) return; const loaded = await BinLoader.loadBinFile(pf.binPath); @@ -202,7 +204,7 @@ export default function RecordsScreen() { adcUV: loaded.adcUV, accNum: pf.accNum, gain: pf.gain, - sampleFreqCode: loaded.sampleFreqCode, + sampleFreqCode, timestamp: pf.timestamp, frameId: pf.frameId, }; diff --git a/src/services/BinLoader.ts b/src/services/BinLoader.ts index 4a2c017..67c17f9 100644 --- a/src/services/BinLoader.ts +++ b/src/services/BinLoader.ts @@ -1,42 +1,43 @@ /** - * TEMF v1 binary format + * 设备原始帧格式 — 与设备 0x85/0x95 数据帧完全一致 * - * Header (64 bytes): - * [0-3] u8[4] MAGIC = "TEMF" (0x54 0x45 0x4D 0x46) - * [4] u8 version = 0x01 - * [5] u8 channelNum - * [6-7] u16 LE sampleDepth - * [8] u8 sampleFreqCode - * [9-10] u16 LE accNum - * [11] u8 ampRatio - * [12] u8 srcMode - * [13-20] f64 LE timestamp (ms, local) - * [21-28] f64 LE latitude (degrees WGS-84) - * [29-36] f64 LE longitude (degrees WGS-84) - * [37-40] u32 LE utc (seconds since Unix epoch) - * [41-44] f32 LE altitude (meters) - * [45-46] u16 LE batteryVolt (raw ADC) - * [47-48] i16 LE temperature (raw ADC) - * [49-50] u16 LE current (raw ADC) - * [51] u8 gpsStatus - * [52] u8 sdStatus - * [53-54] i16 LE roll (degrees × 100) - * [55-56] i16 LE pitch (degrees × 100) - * [57-58] i16 LE yaw (degrees × 10) - * [59-63] u8[5] reserved (zero) + * 帧头 (10 字节): + * [0-3] u8[4] MAGIC = 0x68 0x68 0xFF 0xFF + * [4] u8 flag (0xFE=4字节长度, 0xFF=2字节长度) + * [5] u8 func (0x85=数据帧, 0x95=组合源数据帧) + * [6-9] u32 LE payloadLen (flag=0xFE时) 或 u16 LE + 0x68 0x68 (flag=0xFF时) * - * Data (after header): - * int32 LE, row-major: channelNum rows × sampleDepth columns - * Values are raw ADC counts; calibrated μV = raw / accNum / AMP_GAIN[ampRatio] + * 元数据 (54 字节, 从 offset 10 开始): + * [10] u8 devId + * [11-14] u32 LE utcSecond + * [15-22] f64 LE longitude + * [23-30] f64 LE latitude + * [31-34] f32 LE altitude + * [35-38] f32 LE height + * [39] u8 sdGps (高4位=gpsStatus, 低4位=sdStatus) + * [40] u8 ampRatio + * [41-44] f32 LE roll + * [45-48] f32 LE pitch + * [49-52] f32 LE yaw + * [53] u8 channelNum + * [54-57] u32 LE current + * [58-59] u16 LE temperature + * [60-61] u16 LE batteryVolt + * [62] u8 sourceMode + * [63] u8 reserved + * + * ADC 数据 (从 offset 64 开始): + * int32 LE, 交织排列 (sample-major): + * [S0_CH0][S0_CH1]...[S0_CHn][S1_CH0]... */ import * as FileSystem from 'expo-file-system/legacy'; import { AMP_GAIN } from '../protocol/constants'; import type { MeasurementFrame } from '../protocol/types'; -const MAGIC = [0x54, 0x45, 0x4d, 0x46] as const; // "TEMF" -const VERSION = 0x01; -export const HEADER_SIZE = 64; +const FRAME_HEADER_SIZE = 10; +const META_SIZE = 54; +const DATA_OFFSET = FRAME_HEADER_SIZE + META_SIZE; // 64 // ── Public types ──────────────────────────────────────────────────────────── @@ -83,92 +84,114 @@ export function base64ToU8(b64: string): Uint8Array { // ── Pure encode / decode ──────────────────────────────────────────────────── -/** Encode a MeasurementFrame into TEMF v1 bytes. No IO. */ +/** Encode a MeasurementFrame into a complete device-compatible frame (header + meta + ADC). */ export function encodeBin(frame: MeasurementFrame): Uint8Array { const m = frame.meta; const ch = frame.adcRaw.length; const spc = frame.adcRaw[0]?.length ?? 0; - const buf = new ArrayBuffer(HEADER_SIZE + ch * spc * 4); + const payloadLen = META_SIZE + ch * spc * 4; + const totalLen = FRAME_HEADER_SIZE + payloadLen; + const buf = new ArrayBuffer(totalLen); const dv = new DataView(buf); - MAGIC.forEach((b, i) => dv.setUint8(i, b)); - dv.setUint8(4, VERSION); - dv.setUint8(5, ch); - dv.setUint16(6, spc, true); - dv.setUint8(8, frame.sampleFreqCode); - dv.setUint16(9, frame.accNum, true); - dv.setUint8(11, m.ampRatio); - dv.setUint8(12, m.sourceMode); - dv.setFloat64(13, frame.timestamp, true); - dv.setFloat64(21, m.latitude, true); - dv.setFloat64(29, m.longitude, true); - dv.setUint32(37, m.utc, true); - dv.setFloat32(41, m.altitude, true); - dv.setUint16(45, m.batteryVolt, true); - dv.setInt16(47, m.temperature, true); - dv.setUint16(49, m.current, true); - dv.setUint8(51, m.gpsStatus); - dv.setUint8(52, m.sdStatus); - dv.setInt16(53, Math.round(m.roll * 100), true); - dv.setInt16(55, Math.round(m.pitch * 100), true); - dv.setInt16(57, Math.round(m.yaw * 10), true); - // [59-63] reserved — zero by default + // 10-byte frame header (device format, 4-byte length mode) + dv.setUint8(0, 0x68); + dv.setUint8(1, 0x68); + dv.setUint8(2, 0xff); + dv.setUint8(3, 0xff); + dv.setUint8(4, 0xfe); // flag: 4-byte length + dv.setUint8(5, 0x85); // func: DATA_ACK + dv.setUint32(6, payloadLen, true); - let off = HEADER_SIZE; - for (const c of frame.adcRaw) { - for (let i = 0; i < c.length; i++) { dv.setInt32(off, c[i], true); off += 4; } + // 54-byte metadata (offset 10-63) + let o = FRAME_HEADER_SIZE; + dv.setUint8(o, m.devId ?? 0); o += 1; + dv.setUint32(o, m.utc, true); o += 4; + dv.setFloat64(o, m.longitude, true); o += 8; + dv.setFloat64(o, m.latitude, true); o += 8; + dv.setFloat32(o, m.altitude, true); o += 4; + dv.setFloat32(o, m.height ?? 0, true); o += 4; + dv.setUint8(o, ((m.gpsStatus & 0x0f) << 4) | (m.sdStatus & 0x0f)); o += 1; + dv.setUint8(o, m.ampRatio); o += 1; + dv.setFloat32(o, m.roll, true); o += 4; + dv.setFloat32(o, m.pitch, true); o += 4; + dv.setFloat32(o, m.yaw, true); o += 4; + dv.setUint8(o, m.channelNum); o += 1; + dv.setUint32(o, m.current, true); o += 4; + dv.setUint16(o, m.temperature, true); o += 2; + dv.setUint16(o, m.batteryVolt, true); o += 2; + dv.setUint8(o, m.sourceMode); o += 1; + dv.setUint8(o, 0); o += 1; + + // ADC data (offset 64+), interleaved (sample-major) + let off = DATA_OFFSET; + for (let i = 0; i < spc; i++) { + for (let c = 0; c < ch; c++) { + dv.setInt32(off, frame.adcRaw[c][i], true); + off += 4; + } } + return new Uint8Array(buf); } -/** Decode TEMF v1 bytes into a LoadedBin. Returns null if invalid. No IO. */ +/** Decode a device-compatible frame binary into a LoadedBin. */ export function decodeBin(bytes: Uint8Array): LoadedBin | null { - if (bytes.length < HEADER_SIZE) return null; + if (bytes.length < DATA_OFFSET) return null; const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - if (MAGIC.some((b, i) => dv.getUint8(i) !== b)) return null; - if (dv.getUint8(4) !== VERSION) return null; + // Validate frame header magic + if (dv.getUint8(0) !== 0x68 || dv.getUint8(1) !== 0x68 || + dv.getUint8(2) !== 0xff || dv.getUint8(3) !== 0xff) return null; - const channelNum = dv.getUint8(5); - const sampleDepth = dv.getUint16(6, true); - const sampleFreqCode = dv.getUint8(8); - const accNum = dv.getUint16(9, true); - const ampRatio = dv.getUint8(11); - const srcMode = dv.getUint8(12); - const timestamp = dv.getFloat64(13, true); - const latitude = dv.getFloat64(21, true); - const longitude = dv.getFloat64(29, true); - const utc = dv.getUint32(37, true); - const altitude = dv.getFloat32(41, true); - const batteryVolt = dv.getUint16(45, true); - const temperature = dv.getInt16(47, true); - const current = dv.getUint16(49, true); - const gpsStatus = dv.getUint8(51); - const sdStatus = dv.getUint8(52); - const roll = dv.getInt16(53, true) / 100; - const pitch = dv.getInt16(55, true) / 100; - const yaw = dv.getInt16(57, true) / 10; + // Parse metadata (offset 10-63) + let o = FRAME_HEADER_SIZE; + const devId = dv.getUint8(o); o += 1; + const utc = dv.getUint32(o, true); o += 4; + const longitude = dv.getFloat64(o, true); o += 8; + const latitude = dv.getFloat64(o, true); o += 8; + const altitude = dv.getFloat32(o, true); o += 4; + const height = dv.getFloat32(o, true); o += 4; + const sdGps = dv.getUint8(o); o += 1; + const ampRatio = dv.getUint8(o); o += 1; + const roll = dv.getFloat32(o, true); o += 4; + const pitch = dv.getFloat32(o, true); o += 4; + const yaw = dv.getFloat32(o, true); o += 4; + const channelNum = dv.getUint8(o); o += 1; + const current = dv.getUint32(o, true); o += 4; + const temperature = dv.getUint16(o, true); o += 2; + const batteryVolt = dv.getUint16(o, true); o += 2; + const srcMode = dv.getUint8(o); o += 1; - const expected = HEADER_SIZE + channelNum * sampleDepth * 4; - if (bytes.length < expected) return null; + const gpsStatus = (sdGps >> 4) & 0x0f; + const sdStatus = sdGps & 0x0f; + + // Parse ADC data (offset 64+), interleaved + const dataLen = bytes.length - DATA_OFFSET; + const totalSamples = dataLen / 4; + const sampleDepth = channelNum > 0 ? Math.floor(totalSamples / channelNum) : 0; + if (sampleDepth === 0) return null; const gain = AMP_GAIN[ampRatio] ?? 1; const ADC_UV_SCALE = (5.0 * 1e6) / 0x7fffffff; const scale = ADC_UV_SCALE / gain; const adcUV: Float64Array[] = []; - let off = HEADER_SIZE; - for (let ch = 0; ch < channelNum; ch++) { - const channel = new Float64Array(sampleDepth); - for (let i = 0; i < sampleDepth; i++) { - channel[i] = dv.getInt32(off, true) * scale; + + for (let c = 0; c < channelNum; c++) { + adcUV.push(new Float64Array(sampleDepth)); + } + + let off = DATA_OFFSET; + for (let i = 0; i < sampleDepth; i++) { + for (let c = 0; c < channelNum; c++) { + adcUV[c][i] = dv.getInt32(off, true) * scale; off += 4; } - adcUV.push(channel); } return { - adcUV, sampleDepth, channelNum, sampleFreqCode, - accNum, ampRatio, srcMode, timestamp, + adcUV, sampleDepth, channelNum, sampleFreqCode: 0, + accNum: 0, ampRatio, srcMode, timestamp: 0, latitude, longitude, utc, altitude, batteryVolt, temperature, current, gpsStatus, sdStatus, roll, pitch, yaw, diff --git a/src/services/StorageService.ts b/src/services/StorageService.ts index c5f5cd9..303e766 100644 --- a/src/services/StorageService.ts +++ b/src/services/StorageService.ts @@ -522,6 +522,10 @@ export async function importProject( const meta = BinLoader.decodeBin(binBytes); if (!meta) { onProgress?.(++done, totalFrames); continue; } + // accNum/sampleFreq not in device frame — get from manifest config + const cfg = session.device_config ?? manifest.device_config; + const accNum = cfg.accNum ?? 1; + // Write bin file to DATA_DIR const fname = `${newSessionId}_f${String(frameId).padStart(6, '0')}.bin`; const binPath = DATA_DIR + fname; @@ -537,7 +541,7 @@ export async function importProject( [ frameId, newSessionId, meta.timestamp, meta.utc, meta.longitude, meta.latitude, meta.altitude, - meta.channelNum, meta.accNum, AMP_GAIN[meta.ampRatio] ?? 1, + meta.channelNum, accNum, AMP_GAIN[meta.ampRatio] ?? 1, meta.gpsStatus, meta.sdStatus, meta.ampRatio, meta.current, meta.temperature, meta.batteryVolt, meta.roll, meta.pitch, meta.yaw, meta.srcMode, binPath,