Neda/call/lib/core/framing/framer.dart

102 lines
4.0 KiB
Dart

import 'dart:typed_data';
import '../crypto/aes_cipher.dart';
import '../crypto/key_manager.dart';
import '../fec/reed_solomon.dart';
import '../modem/fsk_modulator.dart';
import '../../utils/audio_math.dart';
import '../../utils/constants.dart';
/// Transmit pipeline: voice bytes → packet → encrypt → RS FEC → FSK audio.
///
/// Call [frameAndModulate] with each LPC super-frame (9 bytes) to get back
/// a Float64List of audio samples ready for AudioTrack playback.
class Framer {
final KeyManager _keys;
final Uint8List _sessionId; // 4-byte session identifier
final FskModulator _modem = FskModulator();
int _seq = 0; // rolling 16-bit sequence counter
Framer(this._keys, this._sessionId);
// ── Public API ─────────────────────────────────────────────────────
/// Full transmit pipeline for one 9-byte LPC payload.
///
/// Steps:
/// 1. Pad payload to 11 bytes (add seq-high + type tag).
/// 2. Encrypt with AES-256-CTR.
/// 3. Build raw packet (SYNC + SEQ + TYPE + LEN + encrypted payload + CRC).
/// 4. Apply RS(15,11) FEC to the 11-byte encrypted payload → 15 bytes.
/// 5. Re-assemble the on-wire frame with FEC-encoded payload.
/// 6. Modulate through 4-FSK → audio samples.
Float64List frameAndModulate(Uint8List lpcPayload) {
assert(lpcPayload.length == 9, 'Framer: LPC payload must be 9 bytes');
final seq = _seq & 0xFFFF;
_seq = (_seq + 1) & 0xFFFF;
// ── Step 1: Pad payload → 11 bytes ─────────────────────────────
// Layout: [lpc(9)] [seq_low(1)] [type(1)]
final plaintext = Uint8List(C.pktPayloadLen); // 11 bytes
plaintext.setRange(0, 9, lpcPayload);
plaintext[9] = seq & 0xFF;
plaintext[10] = C.pkTypeVoice;
// ── Step 2: AES-256-CTR encryption ────────────────────────────
final nonce = _keys.buildNonce(_sessionId, seq);
final encrypted = AesCipher.encrypt(plaintext, _keys.key, nonce);
// ── Step 4: RS FEC — encode the 11-byte encrypted payload only ──
final rsEncoded = ReedSolomon.encodeBlock(encrypted); // 11 B → 15 B
// ── Step 5: Assemble on-wire frame (23 bytes) ──────────────────
// [SYNC:2][SEQ:2][TYPE:1][RS_PAYLOAD:15][CRC:2] = 22 bytes
// Recompute CRC over the RS-encoded block for the wire frame.
final wire = Uint8List(22);
wire[0] = C.syncWord[0];
wire[1] = C.syncWord[1];
wire[2] = (seq >> 8) & 0xFF;
wire[3] = seq & 0xFF;
wire[4] = C.pkTypeVoice;
wire.setRange(5, 20, rsEncoded); // 15 bytes RS-encoded payload
final wireCrc = AudioMath.crc16(wire.sublist(2, 20)); // CRC over [2..19]
wire[20] = (wireCrc >> 8) & 0xFF;
wire[21] = wireCrc & 0xFF;
// ── Step 6: 4-FSK modulation ────────────────────────────────────
return _modem.modulatePacket(wire);
}
/// Modulate a raw control packet (handshake, ping, etc.).
Float64List frameControl(int type, Uint8List payload) {
final seq = _seq & 0xFFFF;
_seq = (_seq + 1) & 0xFFFF;
final padded = Uint8List(C.pktPayloadLen);
padded.setRange(0, payload.length.clamp(0, C.pktPayloadLen), payload);
final nonce = _keys.buildNonce(_sessionId, seq);
final encrypted = AesCipher.encrypt(padded, _keys.key, nonce);
final rsEncoded = ReedSolomon.encodeBlock(encrypted);
final wire = Uint8List(22);
wire[0] = C.syncWord[0];
wire[1] = C.syncWord[1];
wire[2] = (seq >> 8) & 0xFF;
wire[3] = seq & 0xFF;
wire[4] = type;
wire.setRange(5, 20, rsEncoded);
final wireCrc = AudioMath.crc16(wire.sublist(2, 20));
wire[20] = (wireCrc >> 8) & 0xFF;
wire[21] = wireCrc & 0xFF;
return _modem.modulatePacket(wire);
}
void reset() {
_seq = 0;
_modem.reset();
}
}