ntrip_client/
config.rs

1//! NTRIP client configuration objects
2
3use std::str::FromStr;
4
5use strum::{Display, EnumString, VariantNames};
6
7use crate::NtripClientError;
8
9/// NTRIP (Networked Transport of RTCM via Internet Protocol) configuration
10#[derive(Clone, PartialEq, Debug)]
11#[cfg_attr(feature = "clap", derive(clap::Parser))]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct NtripConfig {
14    /// Host name or IP address of the NTRIP server
15    #[cfg_attr(
16        feature = "clap",
17        clap(long = "ntrip-host", env = "NTRIP_HOST", default_value = "rtk2go.com")
18    )]
19    pub host: String,
20
21    /// Port number of the NTRIP server
22    #[cfg_attr(
23        feature = "clap",
24        clap(long = "ntrip-port", env = "NTRIP_PORT", default_value_t = 2101)
25    )]
26    pub port: u16,
27
28    /// Use TLS / SSL for the NTRIP connection
29    #[cfg_attr(
30        feature = "clap",
31        clap(long = "ntrip-use-tls", env = "NTRIP_USE_TLS", default_value_t = false)
32    )]
33    pub use_tls: bool,
34}
35
36impl Default for NtripConfig {
37    /// Builds a default [NtripConfig] ready to connect to [RtcmProvider::Centipede] network.
38    /// The network does not requires SSL.
39    fn default() -> Self {
40        Self::from_provider(RtcmProvider::Centipede)
41    }
42}
43
44impl NtripConfig {
45    /// Generate a connection URL ("host:port") from the NtripConfig
46    pub fn to_url(&self) -> String {
47        format!("{}:{}", self.host, self.port)
48    }
49
50    /// Prepares an [NtripConfig] for one of our predefined [RtcmProvider]s
51    pub fn from_provider(network: RtcmProvider) -> Self {
52        Self {
53            host: network.host().to_string(),
54            port: network.port(),
55            use_tls: network.uses_tls(),
56        }
57    }
58
59    /// Copies and returns [NtripConfig] with updated "host" IP address
60    pub fn with_host(&self, address: &str) -> Self {
61        let mut s = self.clone();
62        s.host = address.to_string();
63        s
64    }
65
66    /// Copies and returns [NtripConfig] with updated port number
67    pub fn with_port(&self, port: u16) -> Self {
68        let mut s = self.clone();
69        s.port = port;
70        s
71    }
72
73    /// Copies and returns [NtripConfig] with TLS/SSL active
74    pub fn with_tls(&self) -> Self {
75        let mut s = self.clone();
76        s.use_tls = true;
77        s
78    }
79
80    /// Copies and returns [NtripConfig] without TLS/SSL active
81    pub fn without_tls(&self) -> Self {
82        let mut s = self.clone();
83        s.use_tls = false;
84        s
85    }
86}
87
88/// Credentials for an NTRIP (RTCM) service
89#[derive(Clone, Default, PartialEq, Debug)]
90#[cfg_attr(feature = "clap", derive(clap::Parser))]
91pub struct NtripCredentials {
92    /// Username for the NTRIP service
93    #[cfg_attr(feature = "clap", clap(long = "ntrip-user", env = "NTRIP_USER"))]
94    pub user: String,
95
96    /// Password for the NTRIP service
97    #[cfg_attr(
98        feature = "clap",
99        clap(long = "ntrip-pass", env = "NTRIP_PASS", default_value = "")
100    )]
101    pub pass: String,
102}
103
104impl NtripCredentials {
105    /// Copies and returns updated [NtripCredentials]
106    pub fn with_username(&self, username: &str) -> Self {
107        let mut s = self.clone();
108        s.user = username.to_string();
109        s
110    }
111
112    /// Copies and returns updated [NtripCredentials]
113    pub fn with_password(&self, password: &str) -> Self {
114        let mut s = self.clone();
115        s.pass = password.to_string();
116        s
117    }
118}
119
120/// Common RTCM data providers
121#[derive(Clone, PartialEq, Debug, EnumString, Display, VariantNames)]
122pub enum RtcmProvider {
123    /// Land Information New Zealand
124    ///
125    /// Note: requires credentials
126    #[strum(serialize = "linz")]
127    Linz,
128    /// RTK2GO.com free service
129    #[strum(serialize = "rtk2go")]
130    Rtk2Go,
131    /// Positioning Australia
132    ///
133    /// Note: requires credentials and TLS
134    #[strum(serialize = "posau")]
135    PosAu,
136    /// Centipede FR
137    #[strum(serialize = "centipede")]
138    Centipede,
139}
140
141impl RtcmProvider {
142    /// Fetch the hostname for the provider
143    pub fn host(&self) -> &str {
144        match self {
145            RtcmProvider::Linz => "positionz-rt.linz.govt.nz",
146            RtcmProvider::Rtk2Go => "rtk2go.com",
147            RtcmProvider::PosAu => "ntrip.data.gnss.ga.gov.au",
148            RtcmProvider::Centipede => "caster.centipede.fr",
149        }
150    }
151
152    /// Fetch the TCP port for the provider
153    pub fn port(&self) -> u16 {
154        match self {
155            RtcmProvider::Linz => 2101,
156            RtcmProvider::Rtk2Go => 2101,
157            RtcmProvider::PosAu => 443,
158            RtcmProvider::Centipede => 2101,
159        }
160    }
161
162    /// Returns true if this [RtcmProvider] requires TLS/SSL.
163    pub fn uses_tls(&self) -> bool {
164        match self {
165            RtcmProvider::Linz => false,
166            RtcmProvider::Rtk2Go => false,
167            RtcmProvider::PosAu => true,
168            RtcmProvider::Centipede => false,
169        }
170    }
171}
172
173/// Parse an [NtripConfig] from a URL string
174///
175/// For example:
176/// ```
177/// # use ntrip_client::config::NtripConfig;
178///
179/// let cfg = "ntrip://rtk2go.com:2101".parse::<NtripConfig>().unwrap();
180///
181/// assert_eq!(cfg.host, "rtk2go.com");
182/// assert_eq!(cfg.port, 2101);
183/// assert_eq!(cfg.use_tls, false);
184/// ```
185///
186/// This also matches on [RtcmProvider]'s for convenience.
187/// ```
188/// # use ntrip_client::config::NtripConfig;
189///
190/// let cfg = "linz".parse::<NtripConfig>().unwrap();
191///
192/// assert_eq!(cfg.host, "positionz-rt.linz.govt.nz");
193/// assert_eq!(cfg.port, 2101);
194/// assert_eq!(cfg.use_tls, false);
195/// ```
196impl FromStr for NtripConfig {
197    type Err = NtripClientError;
198
199    /// Parse an [NtripConfig] from a URL string
200    fn from_str(s: &str) -> Result<Self, Self::Err> {
201        // Match on known providers
202        if let Ok(provider) = RtcmProvider::from_str(s) {
203            return Ok(NtripConfig::from_provider(provider));
204        }
205
206        // Strip protocol if present
207        let proto = if s.starts_with("http://") {
208            "http"
209        } else if s.starts_with("https://") {
210            "https"
211        } else if s.starts_with("ntrip://") {
212            "ntrip"
213        } else {
214            "unknown"
215        };
216        let s = s.trim_start_matches(&format!("{proto}://"));
217
218        // Split host and port
219        let parts: Vec<&str> = s.split(':').collect();
220        if parts.is_empty() {
221            return Err(NtripClientError::InvalidUrl);
222        }
223        let host = parts[0].to_string();
224
225        // Parse port or use default
226        let port = if parts.len() > 1 {
227            parts[1]
228                .parse::<u16>()
229                .map_err(|_| NtripClientError::InvalidPort)?
230        } else if proto == "https" {
231            443
232        } else {
233            2101
234        };
235        Ok(NtripConfig {
236            host,
237            port,
238            use_tls: port == 443,
239        })
240    }
241}