dstar_gateway_core/codec/dplus/
packet.rs

1//! `DPlus` packet enums.
2//!
3//! `ClientPacket` represents every packet a `DPlus` client sends to a
4//! reflector. `ServerPacket` represents every packet a reflector
5//! sends to a client. The codec is symmetric — both directions are
6//! first-class.
7
8use crate::header::DStarHeader;
9use crate::types::{Callsign, StreamId};
10use crate::voice::VoiceFrame;
11
12/// Packets the **client** sends (and the server receives).
13#[non_exhaustive]
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum ClientPacket {
16    /// 5-byte LINK1 connect request `[0x05, 0x00, 0x18, 0x00, 0x01]`.
17    Link1,
18
19    /// 28-byte LINK2 login with callsign at `[4..]` and `b"DV019999"` at `[20..28]`.
20    Link2 {
21        /// Logging-in client callsign.
22        callsign: Callsign,
23    },
24
25    /// 5-byte unlink `[0x05, 0x00, 0x18, 0x00, 0x00]`.
26    Unlink,
27
28    /// 3-byte keepalive poll `[0x03, 0x60, 0x00]`.
29    Poll,
30
31    /// 58-byte voice header (DSVT framed).
32    VoiceHeader {
33        /// D-STAR stream id.
34        stream_id: StreamId,
35        /// Decoded D-STAR header (lenient — bytes preserved verbatim).
36        header: DStarHeader,
37    },
38
39    /// 29-byte voice data (DSVT framed).
40    VoiceData {
41        /// D-STAR stream id.
42        stream_id: StreamId,
43        /// Frame sequence number (0..21 cycle).
44        seq: u8,
45        /// 9 AMBE bytes + 3 slow data bytes.
46        frame: VoiceFrame,
47    },
48
49    /// 32-byte voice EOT (DSVT framed, AMBE silence + `END_PATTERN`).
50    VoiceEot {
51        /// D-STAR stream id.
52        stream_id: StreamId,
53        /// Final seq value (0x40 bit will be OR'd in by the encoder).
54        seq: u8,
55    },
56}
57
58/// Packets the **server** sends (and the client receives).
59#[non_exhaustive]
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum ServerPacket {
62    /// 5-byte LINK1 ACK echo (server replies to client's LINK1).
63    Link1Ack,
64
65    /// 8-byte LINK2 reply: OKRW or BUSY/banned/unknown.
66    Link2Reply {
67        /// Tag at offsets `[4..8]`.
68        result: Link2Result,
69    },
70
71    /// 28-byte LINK2 echo form some servers send instead of OKRW.
72    Link2Echo {
73        /// Echoed callsign.
74        callsign: Callsign,
75    },
76
77    /// 5-byte UNLINK ACK echo.
78    UnlinkAck,
79
80    /// 3-byte poll echo.
81    PollEcho,
82
83    /// 58-byte voice header forwarded to a connected client.
84    VoiceHeader {
85        /// D-STAR stream id.
86        stream_id: StreamId,
87        /// Decoded D-STAR header.
88        header: DStarHeader,
89    },
90
91    /// 29-byte voice data.
92    VoiceData {
93        /// D-STAR stream id.
94        stream_id: StreamId,
95        /// Frame sequence number.
96        seq: u8,
97        /// 9 AMBE bytes + 3 slow data bytes.
98        frame: VoiceFrame,
99    },
100
101    /// 32-byte voice EOT.
102    VoiceEot {
103        /// D-STAR stream id.
104        stream_id: StreamId,
105        /// Final seq value.
106        seq: u8,
107    },
108}
109
110/// Result of a `DPlus` LINK2 reply.
111#[non_exhaustive]
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum Link2Result {
114    /// Server returned `b"OKRW"` — login accepted.
115    Accept,
116    /// Server returned `b"BUSY"` — login refused.
117    Busy,
118    /// Server returned a 4-byte tag that doesn't match any known reply.
119    Unknown {
120        /// The 4-byte tag, typically interpreted as ASCII.
121        reply: [u8; 4],
122    },
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn client_packet_link1_constructible() {
131        let p = ClientPacket::Link1;
132        assert!(matches!(p, ClientPacket::Link1));
133    }
134
135    #[test]
136    fn client_packet_link2_carries_callsign() {
137        let cs = Callsign::from_wire_bytes(*b"W1AW    ");
138        let p = ClientPacket::Link2 { callsign: cs };
139        assert!(
140            matches!(&p, ClientPacket::Link2 { callsign } if callsign.as_str() == "W1AW"),
141            "expected Link2 with W1AW, got {p:?}"
142        );
143    }
144
145    #[test]
146    fn server_packet_link2_reply_accept() {
147        let p = ServerPacket::Link2Reply {
148            result: Link2Result::Accept,
149        };
150        assert!(
151            matches!(p, ServerPacket::Link2Reply { result } if result == Link2Result::Accept),
152            "expected Link2Reply/Accept, got {p:?}"
153        );
154    }
155
156    #[test]
157    fn link2_result_unknown_carries_reply_bytes() {
158        let r = Link2Result::Unknown { reply: *b"FAIL" };
159        assert!(
160            matches!(r, Link2Result::Unknown { reply } if reply == *b"FAIL"),
161            "expected Unknown with FAIL, got {r:?}"
162        );
163    }
164}