This commit is contained in:
2026-06-06 22:13:00 +08:00
parent ed8682a1e2
commit 091b358705
27 changed files with 3001 additions and 102 deletions
+178
View File
@@ -0,0 +1,178 @@
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;
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) {
// Buffer overflow — reset and resync
this.len = 0;
}
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 payloadLen = this.readUint32LE(offset + 6);
if (payloadLen > MAX_PAYLOAD_SIZE) {
// Bogus length — skip this magic and search again
offset += 4;
continue;
}
const totalLen = 10 + payloadLen;
if (this.len - offset < totalLen) break; // wait for more data
const payload = new Uint8Array(totalLen - 10);
payload.set(this.buf.subarray(offset + 10, offset + totalLen));
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 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
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; // int32 per sample
const samplesPerChannel = Math.floor(totalSamples / channelNum);
const view = new DataView(payload.buffer, payload.byteOffset + META_SIZE, sampleDataLen);
const ampRatio = payload[9]; // ampRatio in metadata at offset 9 from payload start...
// actually re-derive from parseMetadata
const gain = AMP_GAIN[payload[9 + 1]] ?? 1; // byte 10 of payload (after devId+utc+lon+lat+alt+height+sdGps = 1+4+8+8+4+4+1 = 30)
// Correct: ampRatio is at offset 31 in payload (1+4+8+8+4+4+1 = 30, then ampRatio at 30)
const correctGain = AMP_GAIN[payload[30]] ?? 1;
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 = (ch * samplesPerChannel + i) * 4;
const val = view.getInt32(idx, true);
raw[i] = val;
uv[i] = val / (accNum || 1) / correctGain;
}
adcRaw.push(raw);
adcUV.push(uv);
}
return { adcRaw, adcUV };
}
// Parse a generic ACK packet (0x81–0x84)
export function parseAck(payload: Uint8Array): AckPacket {
const result = payload[0] ?? 0x02;
let reason = '';
// reason string starts at offset 22 in the payload, max 32 chars
const reasonOffset = 22;
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 };
}