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}