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); } }