A Rust client library for TrackAudio, a modern voice communication application for VATSIM air traffic controllers.
This crate provides a high-level, async API for controlling TrackAudio programmatically via its WebSocket interface, enabling custom integrations, automation tools, and alternative user interfaces.
- π Async/await API β Built on Tokio for efficient async I/O
- π Type-safe β Strongly-typed commands and events with full deserialization
- π Request-response pattern β High-level API for commands that expect responses
- π‘ Event streaming β Subscribe to real-time events from TrackAudio
- π Automatic reconnection β Resilient WebSocket connections with exponential backoff
- π§΅ Thread-safe β Client can be safely shared across threads
- π Tracing support β Optional integration with the
tracingcrate
Add the dependency:
cargo add trackaudioConnect to the default instance locally and listen for events:
use trackaudio::{Command, Event, TrackAudioClient};
#[tokio::main]
async fn main() -> trackaudio::Result<()> {
// Connect to the local TrackAudio instance (ws://127.0.0.1:49080/ws)
let client = TrackAudioClient::connect_default().await?;
// Subscribe to event stream
let mut events = client.subscribe();
// Send a command
client.send(Command::PttPressed).await?;
// Handle incoming events
while let Ok(event) = events.recv().await {
match event {
Event::TxBegin(_) => println!("Started transmitting"),
Event::RxBegin(rx) => println!("Receiving from {}", rx.callsign),
_ => {}
}
}
Ok(())
}For common operations, use the TrackAudioApi wrapper:
use std::time::Duration;
use trackaudio::TrackAudioClient;
#[tokio::main]
async fn main() -> trackaudio::Result<()> {
let client = TrackAudioClient::connect_default().await?;
let api = client.api();
// Add a station and wait for TrackAudio's response
let station = api.add_station("LOVV_CTR", Some(Duration::from_secs(5))).await?;
println!("Added station: {station:?}");
// Change main volume
let vol = api.change_main_volume(-20, None).await?;
println!("Volume changed to {vol}");
Ok(())
}- Use
TrackAudioClientif you want full control (raw events, raw commands). - Use
TrackAudioApiif you want convenience with timeout-guarded request/response helpers.
For more detailed examples and API docs, check out the documentation.
use trackaudio::{Command, Frequency, TrackAudioClient};
use trackaudio::messages::commands::{BoolOrToggle, SetStationState};
use std::time::Duration;
#[tokio::main]
async fn main() -> trackaudio::Result<()> {
let client = TrackAudioClient::connect_default().await?;
let api = client.api();
// Add a station
let station = api.add_station("LOVV_CTR", Some(Duration::from_secs(5))).await?;
if station.is_available {
println!("β Station available on {}", station.frequency.unwrap());
// Enable RX and TX on the station
client.send(Command::SetStationState(SetStationState {
frequency: station.frequency.unwrap(),
rx: Some(BoolOrToggle::Bool(true)),
tx: Some(BoolOrToggle::Bool(true)),
xca: None,
is_output_muted: None,
headset: None,
})).await?;
} else {
println!("β Station not available");
}
Ok(())
}use trackaudio::Frequency;
// Create frequencies in different units
let freq_hz = Frequency::from_hz(132_600_000);
let freq_khz = Frequency::from_khz(132_600);
let freq_mhz = Frequency::from_mhz(132.600);
assert_eq!(freq_hz, freq_khz);
assert_eq!(freq_khz, freq_mhz);
// Convert between units
println!("Frequency: {} MHz", freq_hz.as_mhz());
println!("Frequency: {} kHz", freq_hz.as_khz());
println!("Frequency: {} Hz", freq_hz.as_hz());
// Convenient conversions
let freq: Frequency = 132.600_f64.into(); // from MHz
let freq: Frequency = 132_600_000_u64.into(); // from Hzuse trackaudio::{Event, TrackAudioClient};
#[tokio::main]
async fn main() -> trackaudio::Result<()> {
let client = TrackAudioClient::connect_default().await?;
let mut events = client.subscribe();
println!("Monitoring radio activity...");
while let Ok(event) = events.recv().await {
match event {
Event::RxBegin(rx) => {
println!("π» RX Start: {} on {}", rx.callsign, rx.frequency);
}
Event::RxEnd(rx) => {
println!("π» RX End: {} on {}", rx.callsign, rx.frequency);
if let Some(active) = rx.active_transmitters {
if !active.is_empty() {
println!(" Still transmitting: {}", active.join(", "));
}
}
}
Event::TxBegin(_) => {
println!("ποΈ TX Start");
}
Event::TxEnd(_) => {
println!("ποΈ TX End");
}
Event::StationStateUpdate(state) => {
println!("π‘ Station update: {}", state.callsign);
}
_ => {}
}
}
Ok(())
}use trackaudio::TrackAudioClient;
use trackaudio::messages::commands::{GetStationState, GetStationStates};
use std::time::Duration;
#[tokio::main]
async fn main() -> trackaudio::Result<()> {
let client = TrackAudioClient::connect_default().await?;
// Request a specific station's state
let station = client
.request(
GetStationState {
callsign: "LOVV_CTR".to_string(),
},
Some(Duration::from_secs(5)),
)
.await?;
println!("Station: {:?}", station);
// Request all station states
let states = client
.request(GetStationStates, Some(Duration::from_secs(5)))
.await?;
println!("Total stations: {}", states.len());
Ok(())
}use trackaudio::{TrackAudioClient, TrackAudioConfig};
use std::time::Duration;
#[tokio::main]
async fn main() -> trackaudio::Result<()> {
let config = TrackAudioConfig::new("192.168.1.69:49080")?
.with_capacity(512, 512) // Increase channel capacities
.with_ping_interval(Duration::from_secs(10)); // Adjust keepalive
let client = TrackAudioClient::connect(config).await?;
// Use client...
Ok(())
}The client automatically reconnects on connection loss with exponential backoff:
use trackaudio::{TrackAudioClient, TrackAudioConfig, ConnectionState, ClientEvent, Event};
use std::time::Duration;
#[tokio::main]
async fn main() -> trackaudio::Result<()> {
let config = TrackAudioConfig::default()
.with_auto_reconnect(true) // Enabled by default
.with_max_reconnect_attempts(Some(10)) // None = infinite retries
.with_backoff_config(
Duration::from_secs(1), // Initial backoff
Duration::from_secs(60), // Max backoff
2.0, // Multiplier
);
let client = TrackAudioClient::connect(config).await?;
// `connect` returns before the connection is up; wait for it if you need it ready
client.wait_connected(Some(Duration::from_secs(5))).await?;
// Monitor connection state changes
let mut events = client.subscribe();
tokio::spawn(async move {
while let Ok(event) = events.recv().await {
if let Event::Client(ClientEvent::ConnectionStateChanged(state)) = event {
match state {
ConnectionState::Disconnected { reason } => {
println!("Disconnected: {reason}");
}
ConnectionState::Reconnecting { attempt, next_delay } => {
println!("Reconnecting (attempt {attempt}) in {next_delay:?}...");
}
ConnectionState::Connected => {
println!("Connected!");
}
_ => {}
}
}
}
});
// Manually trigger reconnection if needed
client.reconnect()?;
Ok(())
}The client supports flexible URL formats for connecting to TrackAudio:
- Full WebSocket URL:
ws://127.0.0.1:49080/wsorws://192.168.1.69/ws - Host only:
127.0.0.1orlocalhost(uses default port 49080 and/wspath) - Host and port:
127.0.0.1:12345(uses/wspath)
Note: TrackAudio currently only supports IPv4 connections and the ws:// scheme (no TLS/wss:// support).
TrackAudioClientβ The core WebSocket client for connection managementTrackAudioApiβ High-level API wrapper with convenient methodsTrackAudioConfigβ Configuration for connection parametersCommandβ Commands to send to TrackAudioEventβ Events emitted by TrackAudioFrequencyβ Radio frequency with convenient unit conversions
All operations return a Result<T, TrackAudioError>:
use trackaudio::{TrackAudioClient, TrackAudioError};
match TrackAudioClient::connect_default().await {
Ok(client) => {
// Use client
}
Err(TrackAudioError::WebSocket(e)) => {
eprintln!("Connection failed: {}", e);
}
Err(TrackAudioError::Timeout) => {
eprintln!("Operation timed out");
}
Err(TrackAudioError::InvalidUrl(msg)) => {
eprintln!("Invalid URL: {}", msg);
}
Err(e) => {
eprintln!("Error: {}", e);
}
}tracing(enabled by default) β Integrate with thetracingcrate for structured loggingreconnect-jitter(enabled by default) β Add jitter to reconnection backoff to prevent thundering herd
To disable default features:
cargo add trackaudio --no-default-featuresOr, to enable just one of them:
cargo add trackaudio --no-default-features --features tracingThis crate requires Rust 1.85 or later.
Verified against TrackAudio 1.3.3 and 1.4.0, whose WebSocket SDK protocol is identical.
The trackaudio project and all its crates and packages are dual-licensed as
- Apache License, Version 2.0 (LICENSE-APACHE or https://opensource.org/license/apache-2-0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
This means you can choose to use trackaudio under either the Apache-2.0 license or the MIT license.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Contributions are welcome! Whether you've found a bug, have a feature request, or want to improve the documentation, your input is valued.
If you encounter a bug or have a feature request, please open an issue on GitHub. When reporting a bug, please include:
- A clear description of the problem
- Steps to reproduce the issue
- The version of this crate used
- Your TrackAudio version
- Your Rust version (
rustc --version) - Your operating system
- Any relevant error messages or logs
- Fork the repository and create a feature branch
- Write tests for new functionality (if applicable)
- Ensure
cargo test,cargo fmt, andcargo clippypass - Update documentation for API changes
- Submit your PR with a clear description of the changes
Note that this project uses Conventional Commits in combination with release-plz for automatic semantic versioning and release automation.
Please ensure your commits follow this format when submitting a PR.
For significant changes, please open an issue first to discuss your proposal.
This is an unofficial client library. TrackAudio is developed and maintained by Pierre.