trackaudio 0.2.2

A high-level async client for the TrackAudio WebSocket API, enabling programmatic control, automation, and custom integrations for VATSIM voice communication.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
//! Events emitted by TrackAudio.
//!
//! This module contains all events that are emitted by TrackAudio after state changes or user
//! interaction.
//!
//! # Overview
//!
//! Events are sent by TrackAudio as JSON messages via its WebSocket API and typically either
//! indicate an external change (e.g., a third party starts transmitting on frequency), the result
//! of a user interaction, or a direct response to a [`Command`]. The main [`Event`] enum contains
//! all available events, including associated payload structs.
//!
//! # External documentation
//!
//! For more details on TrackAudio's event protocol, see the
//! [SDK documentation](https://github.com/pierr3/TrackAudio/wiki/SDK-documentation#outgoing-messages)
//! as well as the [respective implementation](https://github.com/pierr3/TrackAudio/blob/main/backend/include/sdkWebsocketMessage.hpp).

use crate::{Command, Frequency};
use serde::Deserialize;
use std::time::Duration;

/// Represents an event received from the TrackAudio instance.
///
/// These messages are sent by TrackAudio to all clients connected to the WebSocket API and
/// represent state changes or other events that occur (either due to user interaction or
/// internal changes).
///
/// Additionally, the `ClientEvent` variant can be used to capture events that occur on the
/// [`TrackAudioClient`](crate::TrackAudioClient) side, such as connection failures or errors.
///
/// # Deserialization
///
/// Events are deserialized from JSON strings sent by TrackAudio with a `type` field indicating the
/// variant name and a `value` field containing the variant's data.
///
/// # Notes
///
/// - TrackAudio's outgoing messages SDK documentation can be found on
///   [GitHub](https://github.com/pierr3/TrackAudio/wiki/SDK-documentation#outgoing-messages).
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum Event {
    /// Voice connection state changed.
    ///
    /// Emitted when the connection to the voice server is established or lost.
    #[serde(rename = "kVoiceConnectedState")]
    VoiceConnectedState(VoiceConnectedState),

    /// Station added.
    ///
    /// Emitted when a new station is successfully added to TrackAudio, e.g., as a response to
    /// [`Command::AddStation`].
    #[serde(rename = "kStationAdded")]
    StationAdded(StationAdded),

    /// A (monitored) station's state has been updated.
    ///
    /// Emitted when any property of a station changes (e.g., rx/tx/xc state, volume, etc.), or
    /// after a station is added or removed from the instance. Emitted as a response to
    /// [`Command::AddStation`], including the info whether the station was found.
    #[serde(rename = "kStationStateUpdate")]
    StationStateUpdate(StationState),

    /// An (unassociated) Frequency has been removed.
    ///
    /// Emitted when a manually tuned frequency (without a station) is removed from TrackAudio.
    #[serde(rename = "kFrequencyRemoved")]
    FrequencyRemoved(FrequencyRemoved),

    /// Full state snapshot of all stations.
    ///
    /// Emitted as a response to [`Command::GetStationState`], containing a list of all stations
    /// currently monitored by TrackAudio.
    #[serde(rename = "kStationStates")]
    StationStates(StationStates),

    /// Transmission started on one or more frequencies.
    ///
    /// Emitted when the user begins transmitting (either by pressing their PTT button or as
    /// a response to a [`Command::PttPressed`]).
    #[serde(rename = "kTxBegin")]
    TxBegin(TxBegin),

    /// Transmission ended on one or more frequencies.
    ///
    /// Emitted when the user finishes transmitting (either by releasing their PTT button or as
    /// a response to [`Command::PttReleased`]).
    #[serde(rename = "kTxEnd")]
    TxEnd(TxEnd),

    /// Started receiving transmission on one or more frequencies.
    ///
    /// Emitted when another station begins transmitting on a monitored frequency.
    #[serde(rename = "kRxBegin")]
    RxBegin(RxBegin),

    /// Stopped receiving transmission on one or more frequencies.
    ///
    /// Emitted when another station stops transmitting on a monitored frequency. Contains a list of
    /// stations still transmitting on frequency (in the case of simultaneous transmissions).
    #[serde(rename = "kRxEnd")]
    RxEnd(RxEnd),

    /// The main volume level changed.
    ///
    /// Emitted when the user adjusts the main volume (either by using the volume slider in the
    /// client or as a response to [`Command::ChangeMainVolume`]).
    #[serde(rename = "kMainVolumeChange")]
    MainVolumeChange(MainVolumeChange),

    /// Frequency state update (deprecated).
    ///
    /// # Deprecated
    ///
    /// This event is deprecated by TrackAudio and only emitted for backwards
    /// compatibility. Use [`Event::StationStateUpdate`] instead.
    #[serde(rename = "kFrequencyStateUpdate")]
    #[deprecated(
        since = "0.1.0",
        note = "This event is deprecated by TrackAudio and only emitted for backwards compatibility. Use StationStateUpdate instead."
    )]
    #[allow(deprecated)]
    FrequencyStateUpdate(FrequencyStateUpdate),

    /// Client-side event not received from TrackAudio.
    ///
    /// These events are generated locally and not deserialized from JSON, but are used to
    /// communicate the [`TrackAudioClient`](crate::TrackAudioClient)'s current (internal) state.
    #[serde(skip)]
    Client(ClientEvent),

    /// Unknown or unrecognized event type.
    ///
    /// Used as a fallback for forward compatibility when new event types are added.
    #[serde(other)]
    Unknown,
}

/// Voice connection state payload.
///
/// Indicates whether TrackAudio is currently connected to the voice server.
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct VoiceConnectedState {
    /// Whether the voice connection is established.
    pub connected: bool,
}

/// Information about a newly added station.
///
/// Indicates a station was successfully added to TrackAudio.
///
/// Emitted in response to [`Command::AddStation`].
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct StationAdded {
    /// The callsign of the station.
    pub callsign: String,

    /// The frequency the station is tuned to.
    pub frequency: Frequency,
}

/// Station state information.
///
/// Contains the current state of a monitored radio station, including its frequency,
/// transmission/reception status, and audio settings.
///
/// Emitted in response to [`Command::GetStationState`], [`Command::SetStationState`],
/// [`Command::AddStation`] and [`Command::ChangeStationVolume`].
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StationState {
    /// The callsign of the station.
    ///
    /// When adding a station, this will be the callsign added (as for most other requests).
    ///
    /// When manually tuning a frequency (not available via API), `callsign` will be `None`. All
    /// later updates will have the callsign `Some("MANUAL")` for manually tuned frequencies.
    pub callsign: Option<String>,

    /// Whether the station is available (found in the VATSIM audio database). If `false`, all
    /// other information will be `None`.
    pub is_available: bool,

    /// The frequency the station is tuned to.
    ///
    /// When adding a station, this value is only available if the station was found and
    /// successfully added.
    ///
    /// When manually tuning a frequency (not available via API), this will be the frequency added,
    /// but its `callsign` will be `None`.
    pub frequency: Option<Frequency>,

    /// Whether the station is routed to the headset audio device only (`true`) or output to both
    /// speaker and headset (`false`).
    pub headset: Option<bool>,

    /// Whether the station's audio output is muted.
    pub is_output_muted: Option<bool>,

    /// The station's audio output volume level in the range 0..=100.
    pub output_volume: Option<f32>,

    /// Whether the station is set to receive (RX).
    pub rx: Option<bool>,

    /// Whether the station is set to transmit (TX).
    pub tx: Option<bool>,

    /// Whether the station has cross-couple (XC) enabled.
    pub xc: Option<bool>,

    /// Whether the station has cross-couple across (XCA) enabled.
    pub xca: Option<bool>,
}

/// Information about a manually tuned frequency that was removed.
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct FrequencyRemoved {
    /// The frequency that was removed.
    pub frequency: Frequency,
}

/// Envelope structure for station state updates.
///
/// Used internally by TrackAudio to wrap individual station state updates with type information.
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct StationStateEnvelope {
    /// The message type identifier (should always be "kStationStateUpdate").
    #[serde(rename = "type")]
    pub msg_type: String,

    /// The station state data.
    pub value: StationState,
}

/// Collection of all monitored station states.
///
/// Emitted in response to [`Command::GetStationState`] queries.
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct StationStates {
    pub stations: Vec<StationStateEnvelope>,
}

/// Transmission begin event payload.
///
/// Currently contains no additional data. The event itself indicates that
/// the local user has started transmitting.
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct TxBegin {}

/// Transmission end event payload.
///
/// Currently contains no additional data. The event itself indicates that
/// the local user has stopped transmitting.
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct TxEnd {}

/// Reception begin event payload.
///
/// Indicates that a remote station has started transmitting on a monitored frequency.
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct RxBegin {
    /// The callsign of the station that started transmitting.
    pub callsign: String,

    /// The frequency on which the transmission is occurring.
    #[serde(rename = "pFrequencyHz")]
    pub frequency: Frequency,
}

/// Reception end event payload.
///
/// Indicates that a remote station has stopped transmitting on a monitored frequency.
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct RxEnd {
    /// The callsign of the station that stopped transmitting.
    pub callsign: String,

    /// The frequency on which the transmission was occurring.
    #[serde(rename = "pFrequencyHz")]
    pub frequency: Frequency,

    /// List of callsigns still transmitting on this frequency, if any.
    ///
    /// Used to handle cases of simultaneous transmissions on the same frequency.
    #[serde(default, rename = "activeTransmitters")]
    pub active_transmitters: Option<Vec<String>>,
}

/// Main volume change event payload.
///
/// Indicates that the main volume level has been adjusted.
///
/// Emitted in response to [`Command::ChangeMainVolume`].
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct MainVolumeChange {
    /// The main audio volume level in the range 0..=100.
    pub volume: f32,
}

/// Deprecated frequency state update payload.
///
/// # Deprecated
///
/// This payload is deprecated by TrackAudio. Use [`StationState`] instead.
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[allow(dead_code)]
#[deprecated(
    since = "0.1.0",
    note = "This payload is deprecated by TrackAudio. Use StationState instead."
)]
pub struct FrequencyStateUpdate {
    /// Stations currently set to receive.
    #[allow(deprecated)]
    rx: Vec<FrequencyState>,

    /// Stations currently set to transmit.
    #[allow(deprecated)]
    tx: Vec<FrequencyState>,

    /// Stations currently set to cross-couple.
    #[allow(deprecated)]
    xc: Vec<FrequencyState>,
}

/// Deprecated frequency state information.
///
/// # Deprecated
///
/// This payload is deprecated by TrackAudio. Use [`StationState`] instead.
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[deprecated(
    since = "0.1.0",
    note = "This payload is deprecated by TrackAudio. Use StationState instead."
)]
pub struct FrequencyState {
    /// The callsign of the station.
    #[serde(rename = "pCallsign")]
    pub callsign: String,

    /// The frequency the station is tuned to.
    #[serde(rename = "pFrequencyHz")]
    pub frequency: Frequency,
}

/// Reason for disconnection from TrackAudio.
#[derive(Debug, Clone, PartialEq)]
pub enum DisconnectReason {
    /// User requested shutdown.
    Shutdown,

    /// User requested manual reconnection.
    ManualReconnect,

    /// Failed to send ping to keep connection alive.
    PingFailed(String),

    /// Failed to send command over WebSocket.
    CommandSendFailed(String),

    /// Failed to send pong response.
    PongFailed(String),

    /// WebSocket connection was closed by the peer.
    ClosedByPeer {
        /// Close frame code and reason, if provided.
        code: Option<u16>,
        reason: Option<String>,
    },

    /// WebSocket error occurred.
    WebSocketError(String),

    /// WebSocket stream ended unexpectedly.
    StreamEnded,

    /// Initial connection failed.
    ConnectionFailed(String),
}

impl std::fmt::Display for DisconnectReason {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Shutdown => write!(f, "Shutdown requested"),
            Self::ManualReconnect => write!(f, "Manual reconnection requested"),
            Self::PingFailed(err) => write!(f, "Failed to send ping: {err}"),
            Self::CommandSendFailed(err) => write!(f, "Failed to send command: {err}"),
            Self::PongFailed(err) => write!(f, "Failed to send pong: {err}"),
            Self::ClosedByPeer { code, reason } => {
                write!(f, "WebSocket closed by peer")?;
                if let Some(code) = code {
                    write!(f, " (code: {code})")?;
                }
                if let Some(reason) = reason {
                    if !reason.is_empty() {
                        write!(f, ": {reason}")?;
                    }
                }
                Ok(())
            }
            Self::WebSocketError(err) => write!(f, "WebSocket error: {err}"),
            Self::StreamEnded => write!(f, "WebSocket stream ended unexpectedly"),
            Self::ConnectionFailed(err) => write!(f, "Connection failed: {err}"),
        }
    }
}

/// Connection state of the TrackAudio client.
#[derive(Debug, Clone, PartialEq)]
pub enum ConnectionState {
    /// The client is attempting to connect to TrackAudio.
    Connecting {
        /// The connection attempt number (1-indexed).
        attempt: usize,
    },

    /// The client has successfully connected to TrackAudio.
    Connected,

    /// The client has been disconnected from TrackAudio.
    Disconnected {
        /// The reason for the disconnection.
        reason: DisconnectReason,
    },

    /// The client is attempting to reconnect to TrackAudio.
    Reconnecting {
        /// The reconnection attempt number (1-indexed).
        attempt: usize,

        /// The delay before the next reconnection attempt.
        next_delay: Duration,
    },

    /// The client has exhausted all reconnection attempts.
    ReconnectFailed {
        /// The total number of reconnection attempts made.
        attempts: usize,
    },
}

/// Client-side event variants.
///
/// These events are generated locally by the TrackAudio client and do not originate
/// from the TrackAudio instance. They represent client-side state changes or errors.
#[derive(Debug, Clone, PartialEq)]
pub enum ClientEvent {
    /// The connection state has changed.
    ConnectionStateChanged(ConnectionState),

    /// A command failed to send to TrackAudio.
    CommandSendFailed {
        /// The command that failed to send.
        command: Command,

        /// The error message describing the failure.
        error: String,
    },

    /// An event from TrackAudio could not be deserialized.
    EventDeserializationFailed {
        /// The raw JSON string that failed to parse.
        raw: String,

        /// The error message describing the deserialization failure.
        error: String,
    },
}