dstar_gateway_core/session/client/
event.rs

1//! Consumer-visible events from the client session.
2//!
3//! [`Event`] is the consumer-visible enum surfaced by the typestate
4//! session. Each variant carries the same data regardless of
5//! protocol — the `P: Protocol` parameter is carried through a
6//! hidden phantom variant so the type is generic without bloating
7//! the other variants with a `PhantomData` field each.
8
9use std::convert::Infallible;
10use std::marker::PhantomData;
11use std::net::SocketAddr;
12
13use crate::header::DStarHeader;
14use crate::types::StreamId;
15use crate::validator::Diagnostic;
16use crate::voice::VoiceFrame;
17
18use super::protocol::Protocol;
19
20/// One event surfaced by the client session machine.
21///
22/// The `P: Protocol` parameter is a phantom — every variant carries
23/// the same data regardless of protocol. The phantom is for
24/// compile-time discrimination only, confined to a hidden
25/// [`Event::__Phantom`] variant that cannot be constructed (its
26/// payload is an uninhabited [`Infallible`]).
27///
28/// All variants are populated: [`Event::Connected`], [`Event::Disconnected`],
29/// [`Event::PollEcho`], and the voice events.
30#[non_exhaustive]
31#[derive(Debug, Clone)]
32pub enum Event<P: Protocol> {
33    /// Session has transitioned to `Connected`.
34    Connected {
35        /// Peer address of the reflector.
36        peer: SocketAddr,
37    },
38
39    /// Session has transitioned to `Disconnected`.
40    Disconnected {
41        /// Why the session disconnected.
42        reason: DisconnectReason,
43    },
44
45    /// Reflector keepalive echo received.
46    PollEcho {
47        /// Peer that sent the echo.
48        peer: SocketAddr,
49    },
50
51    /// A new voice stream started.
52    VoiceStart {
53        /// Stream id.
54        stream_id: StreamId,
55        /// Decoded D-STAR header.
56        header: DStarHeader,
57        /// Diagnostics observed during header parsing.
58        diagnostics: Vec<Diagnostic>,
59    },
60
61    /// A voice data frame within an active stream.
62    VoiceFrame {
63        /// Stream id.
64        stream_id: StreamId,
65        /// Frame seq.
66        seq: u8,
67        /// Voice frame.
68        frame: VoiceFrame,
69    },
70
71    /// Voice stream ended (real EOT or synthesized after timeout).
72    VoiceEnd {
73        /// Stream id.
74        stream_id: StreamId,
75        /// Real EOT vs synthesized after inactivity.
76        reason: VoiceEndReason,
77    },
78
79    /// Hidden phantom variant that carries the `P` type parameter.
80    ///
81    /// This variant cannot be constructed because its payload is
82    /// [`Infallible`]. It exists only so the `Event<P>` type is
83    /// generic over `P` without embedding a `PhantomData` field in
84    /// every public variant.
85    #[doc(hidden)]
86    __Phantom {
87        /// Uninhabited — prevents construction of this variant.
88        never: Infallible,
89        /// Phantom marker for `P`.
90        _protocol: PhantomData<P>,
91    },
92}
93
94/// Why the session disconnected.
95#[non_exhaustive]
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum DisconnectReason {
98    /// Reflector explicitly NAK'd the connection.
99    Rejected,
100    /// Reflector acknowledged the unlink.
101    UnlinkAcked,
102    /// Local timeout — keepalive inactivity.
103    KeepaliveInactivity,
104    /// Local timeout — disconnect ACK never arrived.
105    DisconnectTimeout,
106}
107
108/// Why a voice stream ended.
109#[non_exhaustive]
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum VoiceEndReason {
112    /// Real EOT packet received.
113    Eot,
114    /// No voice frames for the protocol's inactivity window —
115    /// synthesized end.
116    Inactivity,
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::session::client::protocol::{DExtra, DPlus, Dcs};
123    use std::net::{IpAddr, Ipv4Addr};
124
125    const ADDR_DEXTRA: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 30001);
126    const ADDR_DPLUS: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 20001);
127    const ADDR_DCS: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 30051);
128
129    #[test]
130    fn event_connected_constructible_dextra() {
131        let e: Event<DExtra> = Event::Connected { peer: ADDR_DEXTRA };
132        assert!(matches!(e, Event::Connected { .. }));
133    }
134
135    #[test]
136    fn event_connected_constructible_dplus() {
137        let e: Event<DPlus> = Event::Connected { peer: ADDR_DPLUS };
138        assert!(matches!(e, Event::Connected { .. }));
139    }
140
141    #[test]
142    fn event_connected_constructible_dcs() {
143        let e: Event<Dcs> = Event::Connected { peer: ADDR_DCS };
144        assert!(matches!(e, Event::Connected { .. }));
145    }
146
147    #[test]
148    fn event_disconnected_carries_reason() {
149        let e: Event<DExtra> = Event::Disconnected {
150            reason: DisconnectReason::KeepaliveInactivity,
151        };
152        assert!(
153            matches!(e, Event::Disconnected { reason } if reason == DisconnectReason::KeepaliveInactivity),
154            "expected Disconnected/KeepaliveInactivity, got {e:?}"
155        );
156    }
157
158    #[test]
159    fn event_poll_echo_carries_peer() {
160        let e: Event<DExtra> = Event::PollEcho { peer: ADDR_DEXTRA };
161        assert!(
162            matches!(e, Event::PollEcho { peer } if peer == ADDR_DEXTRA),
163            "expected PollEcho, got {e:?}"
164        );
165    }
166
167    #[test]
168    fn disconnect_reason_variants_distinct() {
169        assert_ne!(DisconnectReason::Rejected, DisconnectReason::UnlinkAcked);
170        assert_ne!(
171            DisconnectReason::KeepaliveInactivity,
172            DisconnectReason::DisconnectTimeout
173        );
174    }
175
176    #[test]
177    fn voice_end_reason_variants_distinct() {
178        assert_ne!(VoiceEndReason::Eot, VoiceEndReason::Inactivity);
179    }
180}