dstar_gateway_core/codec/dcs/
packet.rs

1//! `DCS` packet enums.
2//!
3//! `ClientPacket` represents every packet a `DCS` client sends to a
4//! reflector. `ServerPacket` represents every packet a reflector sends
5//! to a client. The codec is symmetric — both directions are
6//! first-class.
7
8use crate::header::DStarHeader;
9use crate::types::{Callsign, Module, StreamId};
10use crate::voice::VoiceFrame;
11
12/// Packets a `DCS` client sends to a reflector.
13#[non_exhaustive]
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum ClientPacket {
16    /// 519-byte LINK request with embedded HTML client identifier.
17    Link {
18        /// Logging-in client callsign.
19        callsign: Callsign,
20        /// Client's local module letter.
21        client_module: Module,
22        /// Module on the reflector to link to.
23        reflector_module: Module,
24        /// The reflector callsign (e.g. `DCS001`).
25        reflector_callsign: Callsign,
26        /// Client gateway type (encoded in the HTML payload).
27        gateway_type: GatewayType,
28    },
29
30    /// 19-byte UNLINK packet.
31    Unlink {
32        /// Logging-out client callsign.
33        callsign: Callsign,
34        /// Client's local module letter.
35        client_module: Module,
36        /// The reflector callsign.
37        reflector_callsign: Callsign,
38    },
39
40    /// 17-byte keepalive poll request.
41    Poll {
42        /// Polling client callsign.
43        callsign: Callsign,
44        /// The reflector callsign.
45        reflector_callsign: Callsign,
46    },
47
48    /// 100-byte voice frame (header + AMBE + slow data all embedded).
49    Voice {
50        /// Decoded D-STAR header embedded at bytes `[4..43]`.
51        header: DStarHeader,
52        /// D-STAR stream id.
53        stream_id: StreamId,
54        /// Frame sequence number.
55        seq: u8,
56        /// Voice frame (9 bytes AMBE + 3 bytes slow data).
57        frame: VoiceFrame,
58        /// True if this is the last frame of the stream.
59        is_end: bool,
60    },
61}
62
63/// Packets a `DCS` reflector sends to a client.
64#[non_exhaustive]
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum ServerPacket {
67    /// 14-byte ACK reply to a LINK request.
68    ConnectAck {
69        /// Echoed callsign.
70        callsign: Callsign,
71        /// Echoed reflector module.
72        reflector_module: Module,
73    },
74
75    /// 14-byte NAK reply to a LINK request.
76    ConnectNak {
77        /// Echoed callsign.
78        callsign: Callsign,
79        /// Echoed reflector module.
80        reflector_module: Module,
81    },
82
83    /// 17-byte keepalive poll echo.
84    PollEcho {
85        /// Echoed callsign.
86        callsign: Callsign,
87        /// Echoed reflector callsign.
88        reflector_callsign: Callsign,
89    },
90
91    /// 100-byte voice frame forwarded to a connected client.
92    Voice {
93        /// Decoded D-STAR header embedded at bytes `[4..43]`.
94        header: DStarHeader,
95        /// D-STAR stream id.
96        stream_id: StreamId,
97        /// Frame sequence number.
98        seq: u8,
99        /// Voice frame (9 bytes AMBE + 3 bytes slow data).
100        frame: VoiceFrame,
101        /// True if this is the last frame of the stream.
102        is_end: bool,
103    },
104}
105
106/// Result of a `DCS` connect attempt.
107#[non_exhaustive]
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum ConnectResult {
110    /// `b"ACK"` reply.
111    Accept,
112    /// `b"NAK"` reply.
113    Reject,
114}
115
116/// Client gateway type embedded in the `DCS` LINK HTML payload.
117///
118/// Reference: `ircDDBGateway/Common/ConnectData.cpp:345-357` enumerates
119/// the four cases the reference implementation writes into the HTML
120/// template.
121#[non_exhaustive]
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum GatewayType {
124    /// Standard D-STAR repeater (default).
125    Repeater,
126    /// Client-mode hotspot.
127    Hotspot,
128    /// USB DV dongle.
129    Dongle,
130    /// `STARnet` digital voice group.
131    StarNet,
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn gateway_type_variants_distinct() {
140        assert_ne!(GatewayType::Repeater, GatewayType::Hotspot);
141        assert_ne!(GatewayType::Dongle, GatewayType::StarNet);
142        assert_ne!(GatewayType::Repeater, GatewayType::Dongle);
143    }
144
145    const CS_W1AW: Callsign = Callsign::from_wire_bytes(*b"W1AW    ");
146    const CS_DCS001: Callsign = Callsign::from_wire_bytes(*b"DCS001  ");
147
148    #[test]
149    fn client_packet_link_constructible() {
150        let p = ClientPacket::Link {
151            callsign: CS_W1AW,
152            client_module: Module::B,
153            reflector_module: Module::C,
154            reflector_callsign: CS_DCS001,
155            gateway_type: GatewayType::Hotspot,
156        };
157        assert!(matches!(p, ClientPacket::Link { .. }));
158    }
159
160    #[test]
161    fn client_packet_unlink_constructible() {
162        let p = ClientPacket::Unlink {
163            callsign: CS_W1AW,
164            client_module: Module::B,
165            reflector_callsign: CS_DCS001,
166        };
167        assert!(matches!(p, ClientPacket::Unlink { .. }));
168    }
169
170    #[test]
171    fn client_packet_poll_constructible() {
172        let p = ClientPacket::Poll {
173            callsign: CS_W1AW,
174            reflector_callsign: CS_DCS001,
175        };
176        assert!(matches!(p, ClientPacket::Poll { .. }));
177    }
178
179    #[test]
180    fn server_packet_connect_ack_carries_module() {
181        let p = ServerPacket::ConnectAck {
182            callsign: CS_DCS001,
183            reflector_module: Module::C,
184        };
185        assert!(
186            matches!(p, ServerPacket::ConnectAck { reflector_module, .. } if reflector_module == Module::C),
187            "expected ConnectAck with Module::C, got {p:?}"
188        );
189    }
190
191    #[test]
192    fn server_packet_poll_echo_constructible() {
193        let p = ServerPacket::PollEcho {
194            callsign: CS_DCS001,
195            reflector_callsign: CS_DCS001,
196        };
197        assert!(matches!(p, ServerPacket::PollEcho { .. }));
198    }
199
200    #[test]
201    fn connect_result_variants_distinct() {
202        assert_ne!(ConnectResult::Accept, ConnectResult::Reject);
203    }
204}