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 (0x81–0x84) 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 }; }