2026-06-20 21:43:09 +08:00

200 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { MAX_PAYLOAD_SIZE } from './constants';
import type { RawFrame, MeasurementMeta, MeasurementFrame, AckPacket } from './types';
import { FuncCode, AMP_GAIN } from './constants';
// Streaming TCP frame parser.
// Handles packet fragmentation (TCP splits large packets across multiple reads).
export class TemFrameParser {
private buf: Uint8Array;
private len = 0;
private readonly capacity: number;
onOverflow?: () => void;
constructor(capacity = 2 * 1024 * 1024) {
this.capacity = capacity;
this.buf = new Uint8Array(capacity);
}
feed(chunk: Uint8Array | ArrayBuffer | { buffer: ArrayBuffer; byteOffset: number; byteLength: number }): RawFrame[] {
let incoming: Uint8Array;
if (chunk instanceof Uint8Array) {
incoming = chunk;
} else if (chunk instanceof ArrayBuffer) {
incoming = new Uint8Array(chunk);
} else {
incoming = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
}
if (this.len + incoming.length > this.capacity) {
if (__DEV__) console.warn(`[Parser] buffer overflow: ${this.len} + ${incoming.length} > ${this.capacity}, resetting`);
this.len = 0;
this.onOverflow?.();
}
this.buf.set(incoming, this.len);
this.len += incoming.length;
const results: RawFrame[] = [];
let offset = 0;
while (offset < this.len) {
const magic = this.findMagic(offset);
if (magic === -1) {
// No header found — keep last 3 bytes in case they start a header
const keep = Math.min(3, this.len - offset);
this.buf.copyWithin(0, this.len - keep);
this.len = keep;
return results;
}
offset = magic;
// Wait until we have the full 10-byte header
if (this.len - offset < 10) break;
const flag = this.buf[offset + 4];
const func = this.buf[offset + 5];
const is2Byte = !!(flag & 0x01);
const payloadLen = is2Byte
? this.readUint16LE(offset + 6)
: this.readUint32LE(offset + 6);
if (__DEV__) console.log(
`[Parser] header @ ${offset}: flag=0x${flag.toString(16)} func=0x${func.toString(16)} lenMode=${is2Byte ? '2B' : '4B'} payloadLen=${payloadLen} bufLen=${this.len}`,
'hdr:', Array.from(this.buf.slice(offset, offset + 10)).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' '),
);
if (payloadLen > MAX_PAYLOAD_SIZE) {
if (__DEV__) console.warn(`[Parser] bogus payloadLen ${payloadLen} > MAX ${MAX_PAYLOAD_SIZE}, skipping magic`);
offset += 4;
continue;
}
const totalLen = 10 + payloadLen;
if (this.len - offset < totalLen) {
if (__DEV__) console.log(`[Parser] incomplete frame: have ${this.len - offset}, need ${totalLen}`);
break;
}
const payload = new Uint8Array(totalLen - 10);
payload.set(this.buf.subarray(offset + 10, offset + totalLen));
if (__DEV__) console.log(`[Parser] ✓ frame complete: func=0x${func.toString(16)} payload=${payloadLen}B`);
results.push({ flag, func, payload });
offset += totalLen;
}
// Compact — move remaining bytes to front
if (offset > 0 && offset <= this.len) {
this.buf.copyWithin(0, offset, this.len);
this.len -= offset;
}
return results;
}
reset() {
this.len = 0;
}
private findMagic(start: number): number {
const b = this.buf;
const end = this.len - 3;
for (let i = start; i <= end; i++) {
if (b[i] === 0x68 && b[i + 1] === 0x68 && b[i + 2] === 0xff && b[i + 3] === 0xff) {
return i;
}
}
return -1;
}
private readUint16LE(offset: number): number {
return this.buf[offset] | (this.buf[offset + 1] << 8);
}
private readUint32LE(offset: number): number {
const b = this.buf;
return b[offset] | (b[offset + 1] << 8) | (b[offset + 2] << 16) | (b[offset + 3] * 0x1000000);
}
}
// Parse the metadata header of a 0x85 or 0x95 DATA_ACK payload
export function parseMetadata(payload: Uint8Array): MeasurementMeta | null {
if (payload.length < 54) return null;
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
let o = 0;
const devId = view.getUint8(o++);
const utc = view.getUint32(o, true); o += 4;
const longitude = view.getFloat64(o, true); o += 8;
const latitude = view.getFloat64(o, true); o += 8;
const altitude = view.getFloat32(o, true); o += 4;
const height = view.getFloat32(o, true); o += 4;
const sdGps = view.getUint8(o++);
const ampRatio = view.getUint8(o++);
const roll = view.getFloat32(o, true); o += 4;
const pitch = view.getFloat32(o, true); o += 4;
const yaw = view.getFloat32(o, true); o += 4;
const channelNum = view.getUint8(o++);
const current = view.getUint32(o, true); o += 4;
const temperature = view.getUint16(o, true); o += 2;
const batteryVolt = view.getUint16(o, true); o += 2;
const sourceMode = view.getUint8(o++);
return {
devId, utc, longitude, latitude, altitude, height,
sdStatus: sdGps & 0x0f,
gpsStatus: (sdGps >> 4) & 0x0f,
ampRatio, roll, pitch, yaw, channelNum,
current, temperature, batteryVolt, sourceMode,
};
}
// Parse ADC sample data from a 0x85/0x95 payload into per-channel arrays
const ADC_FULL_SCALE_V = 5.0;
const INT32_MAX = 0x7fffffff;
// raw → μV: (raw / 0x7FFFFFFF) * 5V / gain * 1e6
const ADC_UV_SCALE = (ADC_FULL_SCALE_V * 1e6) / INT32_MAX;
export function parseAdcData(
payload: Uint8Array,
channelNum: number,
_accNum: number,
): { adcRaw: Int32Array[]; adcUV: Float64Array[] } {
const META_SIZE = 54;
const sampleDataLen = payload.length - META_SIZE;
const totalSamples = sampleDataLen / 4;
const samplesPerChannel = Math.floor(totalSamples / channelNum);
const view = new DataView(payload.buffer, payload.byteOffset + META_SIZE, sampleDataLen);
const gain = AMP_GAIN[payload[30]] ?? 1;
const scale = ADC_UV_SCALE / gain;
const adcRaw: Int32Array[] = [];
const adcUV: Float64Array[] = [];
for (let ch = 0; ch < channelNum; ch++) {
const raw = new Int32Array(samplesPerChannel);
const uv = new Float64Array(samplesPerChannel);
for (let i = 0; i < samplesPerChannel; i++) {
const idx = (i * channelNum + ch) * 4;
const val = view.getInt32(idx, true);
raw[i] = val;
uv[i] = val * scale;
}
adcRaw.push(raw);
adcUV.push(uv);
}
return { adcRaw, adcUV };
}
// Parse a generic ACK packet (0x810x84)
export function parseAck(payload: Uint8Array): AckPacket {
const result = payload[0] ?? 0x02;
let reason = '';
const reasonOffset = 22;
if (payload.length > reasonOffset) {
for (let i = reasonOffset; i < Math.min(reasonOffset + 32, payload.length); i++) {
if (payload[i] === 0) break;
reason += String.fromCharCode(payload[i]);
}
}
return { result, reason };
}