102 lines
4.0 KiB
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();
|
|
}
|
|
|
|
}
|