dstar_gateway_core/
header.rs

1//! D-STAR radio header (41 bytes on the wire with CRC-CCITT).
2//!
3//! The header is transmitted at the start of every D-STAR voice
4//! stream. It contains routing information (repeater callsigns,
5//! destination, origin) and 3 flag bytes for control signaling.
6//!
7//! # Wire format (per JARL D-STAR specification)
8//!
9//! ```text
10//! Offset  Length  Field
11//! 0       1       Flag 1 (control)
12//! 1       1       Flag 2 (reserved)
13//! 2       1       Flag 3 (reserved)
14//! 3       8       RPT2 callsign (space-padded)
15//! 11      8       RPT1 callsign (space-padded)
16//! 19      8       YOUR callsign (space-padded)
17//! 27      8       MY callsign (space-padded)
18//! 35      4       MY suffix (space-padded)
19//! 39      2       CRC-CCITT (little-endian)
20//! ```
21//!
22//! # CRC-CCITT
23//!
24//! Reflected polynomial 0x8408, initial value 0xFFFF, final XOR
25//! 0xFFFF. Computed over bytes 0-38, stored little-endian at 39-40.
26//!
27//! See `ircDDBGateway/Common/HeaderData.cpp:637-684` (`getDPlusData`)
28//! and `ircDDBGateway/Common/CCITTChecksum.cpp` for the reference
29//! implementation this module mirrors.
30
31use crate::types::{Callsign, Suffix};
32
33/// Size of the encoded header on the wire (including CRC).
34pub const ENCODED_LEN: usize = 41;
35
36/// D-STAR radio header.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct DStarHeader {
39    /// Control flag byte 1.
40    pub flag1: u8,
41    /// Reserved flag byte 2.
42    pub flag2: u8,
43    /// Reserved flag byte 3.
44    pub flag3: u8,
45    /// Repeater 2 callsign (gateway).
46    pub rpt2: Callsign,
47    /// Repeater 1 callsign (access).
48    pub rpt1: Callsign,
49    /// Destination callsign (YOUR).
50    pub ur_call: Callsign,
51    /// Origin callsign (MY).
52    pub my_call: Callsign,
53    /// Origin suffix.
54    pub my_suffix: Suffix,
55}
56
57impl DStarHeader {
58    /// Encode the header into 41 bytes with CRC.
59    #[must_use]
60    pub fn encode(&self) -> [u8; ENCODED_LEN] {
61        let mut buf = [0u8; ENCODED_LEN];
62        if let Some(b) = buf.get_mut(0) {
63            *b = self.flag1;
64        }
65        if let Some(b) = buf.get_mut(1) {
66            *b = self.flag2;
67        }
68        if let Some(b) = buf.get_mut(2) {
69            *b = self.flag3;
70        }
71        if let Some(s) = buf.get_mut(3..11) {
72            s.copy_from_slice(self.rpt2.as_bytes());
73        }
74        if let Some(s) = buf.get_mut(11..19) {
75            s.copy_from_slice(self.rpt1.as_bytes());
76        }
77        if let Some(s) = buf.get_mut(19..27) {
78            s.copy_from_slice(self.ur_call.as_bytes());
79        }
80        if let Some(s) = buf.get_mut(27..35) {
81            s.copy_from_slice(self.my_call.as_bytes());
82        }
83        if let Some(s) = buf.get_mut(35..39) {
84            s.copy_from_slice(self.my_suffix.as_bytes());
85        }
86
87        let crc = crc_ccitt(buf.get(..39).unwrap_or(&[]));
88        if let Some(b) = buf.get_mut(39) {
89            *b = (crc & 0xFF) as u8;
90        }
91        if let Some(b) = buf.get_mut(40) {
92            *b = (crc >> 8) as u8;
93        }
94        buf
95    }
96
97    /// Encode the header for embedding in a DSVT voice header packet.
98    ///
99    /// Identical to [`Self::encode`] except the three flag bytes are
100    /// forced to zero BEFORE CRC computation. Matches
101    /// `ircDDBGateway/Common/HeaderData.cpp:665-667` (`getDPlusData`).
102    ///
103    /// DCS voice packets carry real flag bytes — use [`Self::encode`]
104    /// for those.
105    #[must_use]
106    pub fn encode_for_dsvt(&self) -> [u8; ENCODED_LEN] {
107        let mut h = *self;
108        h.flag1 = 0;
109        h.flag2 = 0;
110        h.flag3 = 0;
111        h.encode()
112    }
113
114    /// Decode a 41-byte header.
115    ///
116    /// **Infallible.** Mirrors `ircDDBGateway`'s `setDPlusData` /
117    /// `setDExtraData` / `setDCSData` reference implementations,
118    /// which do raw `memcpy` of the callsign fields with zero
119    /// validation and skip the CRC check. Real reflectors emit
120    /// headers with bad CRCs and non-printable callsign bytes; a
121    /// strict decoder would silently drop real-world traffic.
122    #[must_use]
123    pub fn decode(data: &[u8; ENCODED_LEN]) -> Self {
124        let mut rpt2_bytes = [0u8; 8];
125        if let Some(s) = data.get(3..11) {
126            rpt2_bytes.copy_from_slice(s);
127        }
128        let mut rpt1_bytes = [0u8; 8];
129        if let Some(s) = data.get(11..19) {
130            rpt1_bytes.copy_from_slice(s);
131        }
132        let mut ur_bytes = [0u8; 8];
133        if let Some(s) = data.get(19..27) {
134            ur_bytes.copy_from_slice(s);
135        }
136        let mut my_bytes = [0u8; 8];
137        if let Some(s) = data.get(27..35) {
138            my_bytes.copy_from_slice(s);
139        }
140        let mut suffix_bytes = [0u8; 4];
141        if let Some(s) = data.get(35..39) {
142            suffix_bytes.copy_from_slice(s);
143        }
144
145        Self {
146            flag1: *data.first().unwrap_or(&0),
147            flag2: *data.get(1).unwrap_or(&0),
148            flag3: *data.get(2).unwrap_or(&0),
149            rpt2: Callsign::from_wire_bytes(rpt2_bytes),
150            rpt1: Callsign::from_wire_bytes(rpt1_bytes),
151            ur_call: Callsign::from_wire_bytes(ur_bytes),
152            my_call: Callsign::from_wire_bytes(my_bytes),
153            my_suffix: Suffix::from_wire_bytes(suffix_bytes),
154        }
155    }
156}
157
158/// CRC-CCITT (reflected polynomial 0x8408, init 0xFFFF, final XOR 0xFFFF).
159///
160/// Per `g4klx/MMDVMHost` `DSTARCRC.cpp` and JARL D-STAR specification.
161#[must_use]
162pub fn crc_ccitt(data: &[u8]) -> u16 {
163    let mut crc: u16 = 0xFFFF;
164    for &byte in data {
165        crc ^= u16::from(byte);
166        for _ in 0..8 {
167            if crc & 1 != 0 {
168                crc = (crc >> 1) ^ 0x8408;
169            } else {
170                crc >>= 1;
171            }
172        }
173    }
174    crc ^ 0xFFFF
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    const fn cs(bytes: [u8; 8]) -> Callsign {
182        Callsign::from_wire_bytes(bytes)
183    }
184
185    fn test_header() -> DStarHeader {
186        DStarHeader {
187            flag1: 0x00,
188            flag2: 0x00,
189            flag3: 0x00,
190            rpt2: cs(*b"REF030 G"),
191            rpt1: cs(*b"REF030 C"),
192            ur_call: cs(*b"CQCQCQ  "),
193            my_call: cs(*b"W1AW    "),
194            my_suffix: Suffix::EMPTY,
195        }
196    }
197
198    #[test]
199    fn encode_decode_roundtrip() {
200        let header = test_header();
201        let encoded = header.encode();
202        assert_eq!(encoded.len(), ENCODED_LEN);
203        let decoded = DStarHeader::decode(&encoded);
204        assert_eq!(decoded, header);
205    }
206
207    #[test]
208    fn decode_accepts_bad_crc() {
209        // Per ircDDBGateway/Common/DPlusProtocolHandler.cpp:172
210        // ("DPlus checksums are unreliable") the receive path skips
211        // CRC checks. We mirror that — decode is infallible.
212        let header = test_header();
213        let mut encoded = header.encode();
214        if let Some(byte) = encoded.get_mut(40) {
215            *byte ^= 0xFF;
216        }
217        let decoded = DStarHeader::decode(&encoded);
218        assert_eq!(decoded.my_call, header.my_call);
219    }
220
221    #[test]
222    fn decode_accepts_non_ascii_callsign_verbatim() {
223        // Real-world reflector traffic includes non-printable bytes
224        // in callsign fields. Lenient receive — bytes preserved.
225        let header = test_header();
226        let mut encoded = header.encode();
227        if let Some(byte) = encoded.get_mut(27) {
228            *byte = 0xC3;
229        }
230        let decoded = DStarHeader::decode(&encoded);
231        assert_eq!(decoded.my_call.as_bytes()[0], 0xC3);
232    }
233
234    #[test]
235    fn encode_for_dsvt_zeros_flag_bytes_before_crc() {
236        let hdr = DStarHeader {
237            flag1: 0xAA,
238            flag2: 0xBB,
239            flag3: 0xCC,
240            ..test_header()
241        };
242        let dsvt = hdr.encode_for_dsvt();
243        assert_eq!(dsvt[0], 0, "flag1 zeroed in DSVT encoding");
244        assert_eq!(dsvt[1], 0, "flag2 zeroed in DSVT encoding");
245        assert_eq!(dsvt[2], 0, "flag3 zeroed in DSVT encoding");
246    }
247
248    #[test]
249    fn crc_ccitt_known_vector_w1aw_header() {
250        // Canonical 39-byte header body for the W1AW CQ via REF030 C
251        // example. Cross-checked against ircDDBGateway's
252        // CCITTChecksum.cpp table-based impl.
253        let mut body = [0u8; 39];
254        if let Some(s) = body.get_mut(3..11) {
255            s.copy_from_slice(b"REF030 G");
256        }
257        if let Some(s) = body.get_mut(11..19) {
258            s.copy_from_slice(b"REF030 C");
259        }
260        if let Some(s) = body.get_mut(19..27) {
261            s.copy_from_slice(b"CQCQCQ  ");
262        }
263        if let Some(s) = body.get_mut(27..35) {
264            s.copy_from_slice(b"W1AW    ");
265        }
266        if let Some(s) = body.get_mut(35..39) {
267            s.copy_from_slice(b"    ");
268        }
269        let crc = crc_ccitt(&body);
270        assert_eq!(crc, 0x1073);
271    }
272
273    #[test]
274    fn suffix_roundtrip_nonempty() {
275        let hdr = DStarHeader {
276            my_suffix: Suffix::from_wire_bytes(*b"ECHO"),
277            ..test_header()
278        };
279        let encoded = hdr.encode();
280        let decoded = DStarHeader::decode(&encoded);
281        assert_eq!(decoded.my_suffix.as_bytes(), b"ECHO");
282    }
283}