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

114 lines
3.9 KiB
Dart

import 'dart:typed_data';
import '../../utils/audio_math.dart';
import '../../utils/constants.dart';
/// Immutable representation of one decoded packet.
class Packet {
final int seq; // 16-bit sequence number
final int type; // packet type (see C.pkType*)
final Uint8List payload; // decrypted payload bytes (≤ C.pktPayloadLen)
final bool valid; // CRC check passed
const Packet({
required this.seq,
required this.type,
required this.payload,
required this.valid,
});
bool get isVoice => type == C.pkTypeVoice;
bool get isControl => type == C.pkTypeControl;
bool get isHandshake => type == C.pkTypeHandshake;
@override
String toString() =>
'Packet(seq=$seq, type=0x${type.toRadixString(16)}, '
'len=${payload.length}, valid=$valid)';
}
/// Build and parse the raw (pre-FEC, pre-encryption) packet byte layout.
///
/// Wire layout:
/// [SYNC:2][SEQ_HI:1][SEQ_LO:1][TYPE:1][LEN:1][PAYLOAD:LEN][CRC_HI:1][CRC_LO:1]
///
/// The SYNC bytes (0x5A, 0xA5) are written but intentionally NOT included in
/// the CRC computation — they are only used by the demodulator frame detector.
/// CRC-16/CCITT covers bytes [2..end-2].
abstract final class PacketBuilder {
// ── Serialise ──────────────────────────────────────────────────────
/// Build a raw packet byte array from components.
///
/// [payload] must be ≤ C.pktPayloadLen bytes. It is zero-padded to exactly
/// C.pktPayloadLen before the CRC is computed so the on-wire size is fixed.
static Uint8List build(int seq, int type, Uint8List payload) {
assert(payload.length <= C.pktPayloadLen,
'Payload too large: ${payload.length} > ${C.pktPayloadLen}');
final buf = Uint8List(C.pktTotalBytes);
int pos = 0;
// Sync bytes.
buf[pos++] = C.syncWord[0]; // 0x5A
buf[pos++] = C.syncWord[1]; // 0xA5
// Sequence number (big-endian 16-bit).
buf[pos++] = (seq >> 8) & 0xFF;
buf[pos++] = seq & 0xFF;
// Type and length.
buf[pos++] = type & 0xFF;
buf[pos++] = C.pktPayloadLen; // always fixed length on-wire
// Payload (zero-padded to C.pktPayloadLen).
for (int i = 0; i < C.pktPayloadLen; i++) {
buf[pos++] = i < payload.length ? payload[i] : 0;
}
// CRC-16 over bytes [2..pos-1] (excludes sync bytes).
final crc = AudioMath.crc16(buf.sublist(2, pos));
buf[pos++] = (crc >> 8) & 0xFF;
buf[pos++] = crc & 0xFF;
assert(pos == C.pktTotalBytes);
return buf;
}
// ── Parse ──────────────────────────────────────────────────────────
/// Parse and CRC-validate a raw packet byte array.
///
/// Returns a [Packet] with [Packet.valid] = false if the CRC fails.
/// Does NOT decrypt — the caller must decrypt [Packet.payload] with AES.
static Packet parse(Uint8List raw) {
if (raw.length < C.pktTotalBytes) {
return Packet(seq: 0, type: 0, payload: Uint8List(0), valid: false);
}
int pos = 0;
// Skip sync (already validated by demodulator frame sync).
pos += 2;
// Sequence number.
final seq = (raw[pos] << 8) | raw[pos + 1];
pos += 2;
// Type and length.
final type = raw[pos++];
final len = raw[pos++];
final payLen = len.clamp(0, C.pktPayloadLen);
// Payload.
final payload = Uint8List.fromList(raw.sublist(pos, pos + payLen));
pos += C.pktPayloadLen; // always advance by fixed payload length
// CRC check: cover bytes [2..pos-1].
final bodyCrc = AudioMath.crc16(raw.sublist(2, pos));
final wireCrc = (raw[pos] << 8) | raw[pos + 1];
final valid = (bodyCrc == wireCrc);
return Packet(seq: seq, type: type, payload: payload, valid: valid);
}
}