114 lines
3.9 KiB
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);
|
|
}
|
|
}
|