126 lines
4.2 KiB
Dart
126 lines
4.2 KiB
Dart
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<Uint8List> 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].
|
|
}
|
|
}
|