1use std::str::FromStr;
2
3#[cfg(doc)]
4use crate::prelude::DORIS;
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9use crate::{
10 constants::USO_FREQ_HZ,
11 prelude::{Matcher, ParsingError, DOMES},
12};
13
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
16#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
17pub struct GroundStation {
18 pub label: String,
20
21 pub site: String,
23
24 pub domes: DOMES,
26
27 pub beacon_revision: u8,
29
30 pub k_frequency_shift: i8,
32
33 pub(crate) code: u16,
35}
36
37impl Default for GroundStation {
38 fn default() -> Self {
41 Self {
42 label: Default::default(),
43 site: Default::default(),
44 domes: DOMES::from_str("10003S005").unwrap(),
45 beacon_revision: 3,
46 k_frequency_shift: 0,
47 code: 0,
48 }
49 }
50}
51
52impl GroundStation {
53 pub fn with_site_name(&self, name: &str) -> Self {
55 let mut s = self.clone();
56 s.site = name.to_string();
57 s
58 }
59
60 pub fn with_site_label(&self, label: &str) -> Self {
63 let mut s = self.clone();
64 s.label = label.to_string();
65 s
66 }
67
68 pub fn with_domes_str(&self, domes: &str) -> Result<Self, ParsingError> {
70 let domes = DOMES::from_str(domes)?;
71 Ok(self.with_domes(domes))
72 }
73
74 pub fn with_domes(&self, domes: DOMES) -> Self {
76 let mut s = self.clone();
77 s.domes = domes;
78 s
79 }
80
81 pub fn with_beacon_revision(&self, revision: u8) -> Self {
83 let mut s = self.clone();
84 s.beacon_revision = revision;
85 s
86 }
87
88 pub fn with_frequency_shift(&self, shift: i8) -> Self {
90 let mut s = self.clone();
91 s.k_frequency_shift = shift;
92 s
93 }
94
95 pub fn with_unique_id(&self, code: u16) -> Self {
98 let mut s = self.clone();
99 s.code = code;
100 s
101 }
102
103 pub fn matches<'a>(&self, matcher: &'a Matcher) -> bool {
105 match matcher {
106 Matcher::ID(code) => self.code == *code,
107 Matcher::Site(site) => self.site.to_uppercase() == site.to_uppercase(),
108 Matcher::DOMES(domes) => self.domes == *domes,
109 Matcher::Label(label) => self.label.to_uppercase() == label.to_uppercase(),
110 }
111 }
112
113 pub fn s1_frequency_shift(&self) -> f64 {
115 543.0
116 * USO_FREQ_HZ
117 * (3.0 / 4.0 + 87.0 * self.k_frequency_shift as f64 / 5.0 * 2.0_f64.powi(26))
118 }
119
120 pub fn u2_frequency_shift(&self) -> f64 {
122 107.0
123 * USO_FREQ_HZ
124 * (3.0 / 4.0 + 87.0 * self.k_frequency_shift as f64 / 5.0 * 2.0_f64.powi(26))
125 }
126}
127
128impl std::str::FromStr for GroundStation {
129 type Err = ParsingError;
130 fn from_str(content: &str) -> Result<Self, Self::Err> {
131 if content.len() < 40 {
132 return Err(ParsingError::GroundStation);
133 }
134
135 let content = content.split_at(1).1;
136 let (key, rem) = content.split_at(4);
137 let (label, rem) = rem.split_at(5);
138 let (name, rem) = rem.split_at(30);
139 let (domes, rem) = rem.split_at(10);
140 let (gen, rem) = rem.split_at(3);
141 let (k_factor, _) = rem.split_at(3);
142
143 Ok(GroundStation {
144 site: name.trim().to_string(),
145 label: label.trim().to_string(),
146 domes: DOMES::from_str(domes.trim())?,
147 beacon_revision: gen
148 .trim()
149 .parse::<u8>()
150 .map_err(|_| ParsingError::GroundStation)?,
151 k_frequency_shift: k_factor
152 .trim()
153 .parse::<i8>()
154 .map_err(|_| ParsingError::GroundStation)?,
155 code: key
156 .trim()
157 .parse::<u16>()
158 .map_err(|_| ParsingError::GroundStation)?,
159 })
160 }
161}
162
163impl std::fmt::Display for GroundStation {
164 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
166 write!(
167 f,
168 "Station {} ({}/{}) (rev={}) (freq={})",
169 self.label, self.site, self.domes, self.beacon_revision, self.k_frequency_shift
170 )
171 }
172}
173
174impl std::fmt::LowerHex for GroundStation {
175 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
177 write!(
178 f,
179 "D{:02} {} {:<29} {} {} {:3}",
180 self.code,
181 self.label,
182 self.site,
183 self.domes,
184 self.beacon_revision,
185 self.k_frequency_shift
186 )
187 }
188}
189
190#[cfg(test)]
191mod test {
192 use super::GroundStation;
193 use crate::prelude::{DOMESTrackingPoint, DOMES};
194 use std::str::FromStr;
195
196 #[test]
197 fn default_station() {
198 let _ = GroundStation::default();
199 }
200
201 #[test]
202 fn station_parsing() {
203 for (desc, expected) in [
204 (
205 "D01 OWFC OWENGA 50253S002 3 0",
206 GroundStation {
207 label: "OWFC".to_string(),
208 site: "OWENGA".to_string(),
209 domes: DOMES {
210 area: 502,
211 site: 53,
212 sequential: 2,
213 point: DOMESTrackingPoint::Instrument,
214 },
215 beacon_revision: 3,
216 k_frequency_shift: 0,
217 code: 1,
218 },
219 ),
220 (
221 "D17 GRFB GREENBELT 40451S178 3 0",
222 GroundStation {
223 label: "GRFB".to_string(),
224 site: "GREENBELT".to_string(),
225 domes: DOMES {
226 area: 404,
227 site: 51,
228 sequential: 178,
229 point: DOMESTrackingPoint::Instrument,
230 },
231 beacon_revision: 3,
232 k_frequency_shift: 0,
233 code: 17,
234 },
235 ),
236 (
237 "D12 GR4B GRASSE 10002S019 3 -15",
238 GroundStation {
239 label: "GR4B".to_string(),
240 site: "GRASSE".to_string(),
241 domes: DOMES {
242 area: 100,
243 site: 02,
244 sequential: 19,
245 point: DOMESTrackingPoint::Instrument,
246 },
247 beacon_revision: 3,
248 k_frequency_shift: -15,
249 code: 12,
250 },
251 ),
252 ] {
253 let station = GroundStation::from_str(desc).unwrap();
254 assert_eq!(station, expected, "station parsing error");
255
256 let formatted = format!("{:x}", station);
257 assert_eq!(formatted, desc, "station reciprocal error");
258 }
259 }
260}