♻️ refactor(bin-loader): 重构二进制帧格式以匹配设备协议

- 将 TEMF v1 格式替换为设备 0x85 数据帧格式(10 字节帧头 + 54 字节元数据 + ADC 数据)
- 重写 encodeBin/decodeBin 函数以支持交织排列的 ADC 采样数据
- 更新记录页面使用设备配置中的 sampleFreqCode 替代从二进制文件解析的值
- 在导入项目时从设备配置获取 accNum 以适配新格式
This commit is contained in:
zhoujie 2026-06-21 17:34:58 +08:00
parent e1ca0d8512
commit 9b1ce66f4b
3 changed files with 121 additions and 92 deletions

View File

@ -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,
};

View File

@ -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 c = 0; c < channelNum; c++) {
adcUV.push(new Float64Array(sampleDepth));
}
let off = DATA_OFFSET;
for (let i = 0; i < sampleDepth; i++) {
channel[i] = dv.getInt32(off, true) * scale;
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,

View File

@ -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,