import 'dart:typed_data'; import '../crypto/aes_cipher.dart'; import '../crypto/key_manager.dart'; import '../fec/reed_solomon.dart'; import '../modem/fsk_demodulator.dart'; import 'packet.dart'; import '../../utils/audio_math.dart'; import '../../utils/constants.dart'; /// Receive pipeline: FSK audio → demodulate → RS FEC → decrypt → packet. /// /// Feed audio chunks from the microphone via [pushAudio]. Decoded voice /// super-frames (9-byte LPC payloads) are queued in [voiceQueue]. class Deframer { final KeyManager _keys; final Uint8List _sessionId; final FskDemodulator _demod = FskDemodulator(); // Decoded LPC payloads waiting to be synthesised. final List voiceQueue = []; // Stats for UI display. int rxPackets = 0; int rxErrors = 0; int rxCorrected = 0; Deframer(this._keys, this._sessionId); // ── Public API ───────────────────────────────────────────────────── /// Push a chunk of Float64 audio samples from the microphone. /// /// Internally demodulates FSK, assembles wire frames, applies FEC, /// decrypts, and enqueues decoded voice payloads into [voiceQueue]. void pushAudio(Float64List samples) { final rawFrames = _demod.pushSamples(samples); for (final raw in rawFrames) { _processWireFrame(raw); } } /// Push raw Int16 LE PCM bytes (as received from AudioRecord). void pushPcmBytes(Uint8List pcmBytes) { pushAudio(AudioMath.int16BytesToFloat(pcmBytes)); } void reset() { _demod.reset(); voiceQueue.clear(); rxPackets = 0; rxErrors = 0; rxCorrected = 0; } // ── Private pipeline ─────────────────────────────────────────────── /// Process one wire frame (22 bytes) through FEC → CRC → decrypt → queue. void _processWireFrame(Uint8List wire) { if (wire.length < 22) { rxErrors++; return; } // ── Wire layout: [SYNC:2][SEQ:2][TYPE:1][RS_PAYLOAD:15][CRC:2] ── final seq = (wire[2] << 8) | wire[3]; final type = wire[4]; // Extract the 15-byte RS-encoded block. final rsBlock = Uint8List.fromList(wire.sublist(5, 20)); // CRC check over bytes [2..19]. final expectedCrc = AudioMath.crc16(wire.sublist(2, 20)); final wireCrc = (wire[20] << 8) | wire[21]; if (expectedCrc != wireCrc) { rxErrors++; return; // frame corrupted beyond CRC recovery } // ── RS(15,11) FEC decode (corrects up to 2 nibble errors) ──────── final rsOk = ReedSolomon.decodeBlock(rsBlock); if (!rsOk) { rxErrors++; return; } // Detect whether RS actually corrected any bytes (compare before/after). bool wasCorrected = false; for (int i = 0; i < rsBlock.length; i++) { if (rsBlock[i] != wire[5 + i]) { wasCorrected = true; break; } } if (wasCorrected) rxCorrected++; // Extract the 11-byte data payload. final encrypted = ReedSolomon.extractData(rsBlock); // ── AES-256-CTR decrypt ─────────────────────────────────────────── final nonce = _keys.buildNonce(_sessionId, seq); final plaintext = AesCipher.decrypt(encrypted, _keys.key, nonce); // ── Parse inner packet structure ────────────────────────────────── // plaintext layout: [lpc(9)][seq_low(1)][type_tag(1)] // Validate inner type tag matches outer type. final innerType = plaintext[10]; if (innerType != type && type == C.pkTypeVoice) { rxErrors++; return; } rxPackets++; // Build a Packet for higher-level handlers. final pkt = Packet( seq: seq, type: type, payload: plaintext, valid: true, ); if (pkt.isVoice) { // First 9 bytes are the LPC super-frame. voiceQueue.add(Uint8List.fromList(plaintext.sublist(0, 9))); } // Control packets (handshake, ping) are ignored here; // higher layers can subscribe via [SecureChannel]. } }