♻️ 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);
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user