Skip to main content

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)]
90#[cfg_attr(feature = "clap", derive(clap::Parser))]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub struct NtripCredentials {
93    /// Username for the NTRIP service
94    #[cfg_attr(feature = "clap", clap(long = "ntrip-user", env = "NTRIP_USER"))]
95    pub user: String,
96
97    /// Password for the NTRIP service
98    #[cfg_attr(
99        feature = "clap",
100        clap(long = "ntrip-pass", env = "NTRIP_PASS", default_value = "")
101    )]
102    pub pass: String,
103}
104
105impl NtripCredentials {
106    /// Copies and returns updated [NtripCredentials]
107    pub fn with_username(&self, username: &str) -> Self {
108        let mut s = self.clone();
109        s.user = username.to_string();
110        s
111    }
112
113    /// Copies and returns updated [NtripCredentials]
114    pub fn with_password(&self, password: &str) -> Self {
115        let mut s = self.clone();
116        s.pass = password.to_string();
117        s
118    }
119}
120
121impl std::fmt::Debug for NtripCredentials {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        f.debug_struct("NtripCredentials")
124            .field("user", &self.user)
125            .field("pass", &"********")
126            .finish()
127    }
128}
129
130/// Common RTCM data providers
131#[derive(Clone, PartialEq, Debug, EnumString, Display, VariantNames)]
132#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
133pub enum RtcmProvider {
134    /// Land Information New Zealand
135    ///
136    /// Note: requires credentials
137    #[strum(serialize = "linz")]
138    Linz,
139    /// RTK2GO.com free service
140    #[strum(serialize = "rtk2go")]
141    Rtk2Go,
142    /// Positioning Australia
143    ///
144    /// Note: requires credentials and TLS
145    #[strum(serialize = "posau")]
146    PosAu,
147    /// Centipede FR
148    #[strum(serialize = "centipede")]
149    Centipede,
150}
151
152impl RtcmProvider {
153    /// Fetch the hostname for the provider
154    pub fn host(&self) -> &str {
155        match self {
156            RtcmProvider::Linz => "positionz-rt.linz.govt.nz",
157            RtcmProvider::Rtk2Go => "rtk2go.com",
158            RtcmProvider::PosAu => "ntrip.data.gnss.ga.gov.au",
159            RtcmProvider::Centipede => "caster.centipede.fr",
160        }
161    }
162
163    /// Fetch the TCP port for the provider
164    pub fn port(&self) -> u16 {
165        match self {
166            RtcmProvider::Linz => 2101,
167            RtcmProvider::Rtk2Go => 2101,
168            RtcmProvider::PosAu => 443,
169            RtcmProvider::Centipede => 2101,
170        }
171    }
172
173    /// Returns true if this [RtcmProvider] requires TLS/SSL.
174    pub fn uses_tls(&self) -> bool {
175        match self {
176            RtcmProvider::Linz => false,
177            RtcmProvider::Rtk2Go => false,
178            RtcmProvider::PosAu => true,
179            RtcmProvider::Centipede => false,
180        }
181    }
182}
183
184/// Parse an [NtripConfig] from a URL string
185///
186/// For example:
187/// ```
188/// # use ntrip_client::config::NtripConfig;
189///
190/// let cfg = "ntrip://rtk2go.com:2101".parse::<NtripConfig>().unwrap();
191///
192/// assert_eq!(cfg.host, "rtk2go.com");
193/// assert_eq!(cfg.port, 2101);
194/// assert_eq!(cfg.use_tls, false);
195/// ```
196///
197/// This also matches on [RtcmProvider]'s for convenience.
198/// ```
199/// # use ntrip_client::config::NtripConfig;
200///
201/// let cfg = "linz".parse::<NtripConfig>().unwrap();
202///
203/// assert_eq!(cfg.host, "positionz-rt.linz.govt.nz");
204/// assert_eq!(cfg.port, 2101);
205/// assert_eq!(cfg.use_tls, false);
206/// ```
207impl FromStr for NtripConfig {
208    type Err = NtripClientError;
209
210    /// Parse an [NtripConfig] from a URL string
211    fn from_str(s: &str) -> Result<Self, Self::Err> {
212        // Match on known providers
213        if let Ok(provider) = RtcmProvider::from_str(s) {
214            return Ok(NtripConfig::from_provider(provider));
215        }
216
217        // Strip protocol if present
218        let proto = if s.starts_with("http://") {
219            "http"
220        } else if s.starts_with("https://") {
221            "https"
222        } else if s.starts_with("ntrip://") {
223            "ntrip"
224        } else {
225            "unknown"
226        };
227        let s = s.trim_start_matches(&format!("{proto}://"));
228
229        // Split host and port
230        let parts: Vec<&str> = s.split(':').collect();
231        if parts.is_empty() {
232            return Err(NtripClientError::InvalidUrl);
233        }
234        let host = parts[0].to_string();
235
236        // Parse port or use default
237        let port = if parts.len() > 1 {
238            parts[1]
239                .parse::<u16>()
240                .map_err(|_| NtripClientError::InvalidPort)?
241        } else if proto == "https" {
242            443
243        } else {
244            2101
245        };
246        Ok(NtripConfig {
247            host,
248            port,
249            use_tls: port == 443,
250        })
251    }
252}