♻️ refactor(bin-loader): 重构二进制帧格式以匹配设备协议
- 将 TEMF v1 格式替换为设备 0x85 数据帧格式(10 字节帧头 + 54 字节元数据 + ADC 数据) - 重写 encodeBin/decodeBin 函数以支持交织排列的 ADC 采样数据 - 更新记录页面使用设备配置中的 sampleFreqCode 替代从二进制文件解析的值 - 在导入项目时从设备配置获取 accNum 以适配新格式
This commit is contained in:
parent
e1ca0d8512
commit
9b1ce66f4b
@ -192,6 +192,8 @@ export default function RecordsScreen() {
|
|||||||
await resumeSession(newSessionId, projectId);
|
await resumeSession(newSessionId, projectId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sampleFreqCode = useDeviceStore((s) => s.config.sampleFreq);
|
||||||
|
|
||||||
const handleFramePress = async (pf: PersistedFrame) => {
|
const handleFramePress = async (pf: PersistedFrame) => {
|
||||||
if (!pf.binPath) return;
|
if (!pf.binPath) return;
|
||||||
const loaded = await BinLoader.loadBinFile(pf.binPath);
|
const loaded = await BinLoader.loadBinFile(pf.binPath);
|
||||||
@ -202,7 +204,7 @@ export default function RecordsScreen() {
|
|||||||
adcUV: loaded.adcUV,
|
adcUV: loaded.adcUV,
|
||||||
accNum: pf.accNum,
|
accNum: pf.accNum,
|
||||||
gain: pf.gain,
|
gain: pf.gain,
|
||||||
sampleFreqCode: loaded.sampleFreqCode,
|
sampleFreqCode,
|
||||||
timestamp: pf.timestamp,
|
timestamp: pf.timestamp,
|
||||||
frameId: pf.frameId,
|
frameId: pf.frameId,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,42 +1,43 @@
|
|||||||
/**
|
/**
|
||||||
* TEMF v1 binary format
|
* 设备原始帧格式 — 与设备 0x85/0x95 数据帧完全一致
|
||||||
*
|
*
|
||||||
* Header (64 bytes):
|
* 帧头 (10 字节):
|
||||||
* [0-3] u8[4] MAGIC = "TEMF" (0x54 0x45 0x4D 0x46)
|
* [0-3] u8[4] MAGIC = 0x68 0x68 0xFF 0xFF
|
||||||
* [4] u8 version = 0x01
|
* [4] u8 flag (0xFE=4字节长度, 0xFF=2字节长度)
|
||||||
* [5] u8 channelNum
|
* [5] u8 func (0x85=数据帧, 0x95=组合源数据帧)
|
||||||
* [6-7] u16 LE sampleDepth
|
* [6-9] u32 LE payloadLen (flag=0xFE时) 或 u16 LE + 0x68 0x68 (flag=0xFF时)
|
||||||
* [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)
|
|
||||||
*
|
*
|
||||||
* Data (after header):
|
* 元数据 (54 字节, 从 offset 10 开始):
|
||||||
* int32 LE, row-major: channelNum rows × sampleDepth columns
|
* [10] u8 devId
|
||||||
* Values are raw ADC counts; calibrated μV = raw / accNum / AMP_GAIN[ampRatio]
|
* [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 * as FileSystem from 'expo-file-system/legacy';
|
||||||
import { AMP_GAIN } from '../protocol/constants';
|
import { AMP_GAIN } from '../protocol/constants';
|
||||||
import type { MeasurementFrame } from '../protocol/types';
|
import type { MeasurementFrame } from '../protocol/types';
|
||||||
|
|
||||||
const MAGIC = [0x54, 0x45, 0x4d, 0x46] as const; // "TEMF"
|
const FRAME_HEADER_SIZE = 10;
|
||||||
const VERSION = 0x01;
|
const META_SIZE = 54;
|
||||||
export const HEADER_SIZE = 64;
|
const DATA_OFFSET = FRAME_HEADER_SIZE + META_SIZE; // 64
|
||||||
|
|
||||||
// ── Public types ────────────────────────────────────────────────────────────
|
// ── Public types ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -83,92 +84,114 @@ export function base64ToU8(b64: string): Uint8Array {
|
|||||||
|
|
||||||
// ── Pure encode / decode ────────────────────────────────────────────────────
|
// ── 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 {
|
export function encodeBin(frame: MeasurementFrame): Uint8Array {
|
||||||
const m = frame.meta;
|
const m = frame.meta;
|
||||||
const ch = frame.adcRaw.length;
|
const ch = frame.adcRaw.length;
|
||||||
const spc = frame.adcRaw[0]?.length ?? 0;
|
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);
|
const dv = new DataView(buf);
|
||||||
|
|
||||||
MAGIC.forEach((b, i) => dv.setUint8(i, b));
|
// 10-byte frame header (device format, 4-byte length mode)
|
||||||
dv.setUint8(4, VERSION);
|
dv.setUint8(0, 0x68);
|
||||||
dv.setUint8(5, ch);
|
dv.setUint8(1, 0x68);
|
||||||
dv.setUint16(6, spc, true);
|
dv.setUint8(2, 0xff);
|
||||||
dv.setUint8(8, frame.sampleFreqCode);
|
dv.setUint8(3, 0xff);
|
||||||
dv.setUint16(9, frame.accNum, true);
|
dv.setUint8(4, 0xfe); // flag: 4-byte length
|
||||||
dv.setUint8(11, m.ampRatio);
|
dv.setUint8(5, 0x85); // func: DATA_ACK
|
||||||
dv.setUint8(12, m.sourceMode);
|
dv.setUint32(6, payloadLen, true);
|
||||||
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
|
|
||||||
|
|
||||||
let off = HEADER_SIZE;
|
// 54-byte metadata (offset 10-63)
|
||||||
for (const c of frame.adcRaw) {
|
let o = FRAME_HEADER_SIZE;
|
||||||
for (let i = 0; i < c.length; i++) { dv.setInt32(off, c[i], true); off += 4; }
|
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);
|
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 {
|
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);
|
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||||
|
|
||||||
if (MAGIC.some((b, i) => dv.getUint8(i) !== b)) return null;
|
// Validate frame header magic
|
||||||
if (dv.getUint8(4) !== VERSION) return null;
|
if (dv.getUint8(0) !== 0x68 || dv.getUint8(1) !== 0x68 ||
|
||||||
|
dv.getUint8(2) !== 0xff || dv.getUint8(3) !== 0xff) return null;
|
||||||
|
|
||||||
const channelNum = dv.getUint8(5);
|
// Parse metadata (offset 10-63)
|
||||||
const sampleDepth = dv.getUint16(6, true);
|
let o = FRAME_HEADER_SIZE;
|
||||||
const sampleFreqCode = dv.getUint8(8);
|
const devId = dv.getUint8(o); o += 1;
|
||||||
const accNum = dv.getUint16(9, true);
|
const utc = dv.getUint32(o, true); o += 4;
|
||||||
const ampRatio = dv.getUint8(11);
|
const longitude = dv.getFloat64(o, true); o += 8;
|
||||||
const srcMode = dv.getUint8(12);
|
const latitude = dv.getFloat64(o, true); o += 8;
|
||||||
const timestamp = dv.getFloat64(13, true);
|
const altitude = dv.getFloat32(o, true); o += 4;
|
||||||
const latitude = dv.getFloat64(21, true);
|
const height = dv.getFloat32(o, true); o += 4;
|
||||||
const longitude = dv.getFloat64(29, true);
|
const sdGps = dv.getUint8(o); o += 1;
|
||||||
const utc = dv.getUint32(37, true);
|
const ampRatio = dv.getUint8(o); o += 1;
|
||||||
const altitude = dv.getFloat32(41, true);
|
const roll = dv.getFloat32(o, true); o += 4;
|
||||||
const batteryVolt = dv.getUint16(45, true);
|
const pitch = dv.getFloat32(o, true); o += 4;
|
||||||
const temperature = dv.getInt16(47, true);
|
const yaw = dv.getFloat32(o, true); o += 4;
|
||||||
const current = dv.getUint16(49, true);
|
const channelNum = dv.getUint8(o); o += 1;
|
||||||
const gpsStatus = dv.getUint8(51);
|
const current = dv.getUint32(o, true); o += 4;
|
||||||
const sdStatus = dv.getUint8(52);
|
const temperature = dv.getUint16(o, true); o += 2;
|
||||||
const roll = dv.getInt16(53, true) / 100;
|
const batteryVolt = dv.getUint16(o, true); o += 2;
|
||||||
const pitch = dv.getInt16(55, true) / 100;
|
const srcMode = dv.getUint8(o); o += 1;
|
||||||
const yaw = dv.getInt16(57, true) / 10;
|
|
||||||
|
|
||||||
const expected = HEADER_SIZE + channelNum * sampleDepth * 4;
|
const gpsStatus = (sdGps >> 4) & 0x0f;
|
||||||
if (bytes.length < expected) return null;
|
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 gain = AMP_GAIN[ampRatio] ?? 1;
|
||||||
const ADC_UV_SCALE = (5.0 * 1e6) / 0x7fffffff;
|
const ADC_UV_SCALE = (5.0 * 1e6) / 0x7fffffff;
|
||||||
const scale = ADC_UV_SCALE / gain;
|
const scale = ADC_UV_SCALE / gain;
|
||||||
const adcUV: Float64Array[] = [];
|
const adcUV: Float64Array[] = [];
|
||||||
let off = HEADER_SIZE;
|
|
||||||
for (let ch = 0; ch < channelNum; ch++) {
|
for (let c = 0; c < channelNum; c++) {
|
||||||
const channel = new Float64Array(sampleDepth);
|
adcUV.push(new Float64Array(sampleDepth));
|
||||||
|
}
|
||||||
|
|
||||||
|
let off = DATA_OFFSET;
|
||||||
for (let i = 0; i < sampleDepth; i++) {
|
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;
|
off += 4;
|
||||||
}
|
}
|
||||||
adcUV.push(channel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
adcUV, sampleDepth, channelNum, sampleFreqCode,
|
adcUV, sampleDepth, channelNum, sampleFreqCode: 0,
|
||||||
accNum, ampRatio, srcMode, timestamp,
|
accNum: 0, ampRatio, srcMode, timestamp: 0,
|
||||||
latitude, longitude, utc, altitude,
|
latitude, longitude, utc, altitude,
|
||||||
batteryVolt, temperature, current,
|
batteryVolt, temperature, current,
|
||||||
gpsStatus, sdStatus, roll, pitch, yaw,
|
gpsStatus, sdStatus, roll, pitch, yaw,
|
||||||
|
|||||||
@ -522,6 +522,10 @@ export async function importProject(
|
|||||||
const meta = BinLoader.decodeBin(binBytes);
|
const meta = BinLoader.decodeBin(binBytes);
|
||||||
if (!meta) { onProgress?.(++done, totalFrames); continue; }
|
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
|
// Write bin file to DATA_DIR
|
||||||
const fname = `${newSessionId}_f${String(frameId).padStart(6, '0')}.bin`;
|
const fname = `${newSessionId}_f${String(frameId).padStart(6, '0')}.bin`;
|
||||||
const binPath = DATA_DIR + fname;
|
const binPath = DATA_DIR + fname;
|
||||||
@ -537,7 +541,7 @@ export async function importProject(
|
|||||||
[
|
[
|
||||||
frameId, newSessionId, meta.timestamp,
|
frameId, newSessionId, meta.timestamp,
|
||||||
meta.utc, meta.longitude, meta.latitude, meta.altitude,
|
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.gpsStatus, meta.sdStatus, meta.ampRatio,
|
||||||
meta.current, meta.temperature, meta.batteryVolt,
|
meta.current, meta.temperature, meta.batteryVolt,
|
||||||
meta.roll, meta.pitch, meta.yaw, meta.srcMode, binPath,
|
meta.roll, meta.pitch, meta.yaw, meta.srcMode, binPath,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user