init
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user