tle/
lib.rs

1#![forbid(unsafe_code)]
2
3const DECIMAL_RADIX: u32 = 10;
4
5/// Some errors are ambiguous as to the line in which they occur.
6///
7/// Where that is the case, this enum is used to disambiguate.
8#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
9pub enum Line {
10    Line1,
11    Line2,
12}
13
14/// A description of where in the TLE the parsing and validation failed.
15#[non_exhaustive]
16#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
17pub enum Error {
18    /// In a TLE each line is required to be 69 characters in length
19    ///
20    /// For each line, this is first validation conducted
21    InvalidLineSize(Line, usize),
22    /// Certain segments of the TLE are required to be an ASCII space character. In those cases,
23    /// if another character is encountered this error will be produced with the character found,
24    /// and the position found.
25    Space(Line, char, usize),
26    /// Failed to parse the satellite catalog number in the given line
27    SatlliteCatalogNumber(Line),
28    /// The classification must be one of three characters
29    ///
30    /// 1. 'U': Unclassified
31    /// 1. 'C': Classified
32    /// 1. 'S': Secret
33    ///
34    /// If the classification field matches none of these, this error will be produced
35    Classification(char),
36    /// Represents a failure to parse the international designator's two digit launch year
37    InternationalDesignatorLaunchYear,
38    /// Represents a failure to parse the international designator's three digit launch number
39    InternationalDesignatorLaunchNumber,
40    /// Represents a failure to parse the two digit epoch year
41    EpochYear,
42    /// Represents a failure to parse the epoch day
43    EpochDay,
44    /// Represents a failure to parse the first derivative of mean motion
45    FirstDerivative,
46    /// Represents a failure to parse the second derivative of mean motion
47    SecondDerivative,
48    /// Represents a failure to parse B* drag term
49    BStar,
50    /// Represents a validation failure, the ephemeris type must be '0'
51    EphemerisType(char),
52    /// Represents a failure to parse the element set number
53    ElementSetNumber,
54    /// Represents a failure to parse the inclination
55    Inclination,
56    /// Represents a failure to parse the right ascension
57    RightAscension,
58    /// Represents a failure to parse the eccentricity
59    Eccentricty,
60    /// Represents a failure to parse the argument of perigee
61    ArgumentOfPerigee,
62    /// Represents a failure to parse the mean anomaly
63    MeanAnomaly,
64    /// Represents a failure to parse the mean motion
65    MeanMotion,
66    /// Represents a failure to parse the revolution number
67    RevolutionNumber,
68    /// Represents a failure to parse the checksum for the given line.
69    ///
70    /// This value must be a modulo 10 number
71    Checksum(Line, char),
72    /// The TLE checksum is calculated by summing all of the digits in the line, plus 1 for each '-'
73    /// character, modulo 10.
74    ///
75    /// If the calculated checksum does not match the one provided in the line, the line number,
76    /// found checksum, and calculated checksum will be returned
77    InvalidChecksum(Line, u8, u8),
78    /// The first character of each line must be the number of the line i.e. '1' and '2'.
79    ///
80    /// If that check fails this error is propagated with the character found, and the line which
81    /// failed.
82    LineNumber(Line, char),
83    /// The second sequence in each line of the TLE is the satellite catalog number, this must be
84    /// consistent across both lines
85    ///
86    /// In the event of this error the values represent the satellite catalog numbers found in each
87    /// line
88    SatelliteCatalogNumberMismatch(u32, u32),
89    /// TLEs are required to contain only valid ASCII characters
90    ContainsNonAsciiCharacter(Line),
91}
92
93#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
94pub struct InternationalDesignator {
95    pub launch_year: u8,
96    pub launch_num: u16,
97    pub launch_piece: [char; 3],
98}
99
100#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
101pub enum Classification {
102    Unclassified,
103    Classified,
104    Secret,
105}
106
107/// A parsed and validate Two Line Element Set
108///
109/// This is primarily generated via the `parse` method
110#[derive(Debug, Clone, PartialEq, PartialOrd)]
111pub struct Tle {
112    pub satellite_catalog_number: u32,
113    pub classification: Classification,
114    pub international_designator: InternationalDesignator,
115    pub epoch_year: u8,
116    pub epoch_day_and_fractional_part: f64,
117    pub first_derivative_of_mean_motion: f32,
118    pub second_derivative_of_mean_motion: f32,
119    pub b_star: f32,
120    pub element_set_number: u16,
121    checksum_1: u8,
122    pub inclination: f32,
123    pub right_ascension_of_ascending_node: f32,
124    pub eccentricity: f32,
125    pub argument_of_perigee: f32,
126    pub mean_anomaly: f32,
127    pub mean_motion: f32,
128    pub revolution_number_at_epoch: u32,
129    checksum_2: u8,
130}
131
132macro_rules! split_space {
133    ($line_num:expr, $line:ident, $pos:literal) => {{
134        let (slice, line) = $line.split_at(1);
135        if slice[0] != ' ' {
136            return Err(Error::Space($line_num, slice[0], $pos));
137        }
138
139        line
140    }};
141}
142
143impl Tle {
144    const LINE_LEN: usize = 69;
145
146    /// Parses the lines as a TLE, then performs validation on the resulting TLE instance to
147    /// ensure the checksums are calculated correctly
148    ///
149    /// TODO: Currently generate_checksum is not working correctly
150    #[allow(unused)]
151    fn parse_and_validate(line1: &[u8], line2: &[u8]) -> Result<Self, Error> {
152        let me = match Self::parse(line1, line2) {
153            Ok(me) => me,
154            Err(error) => return Err(error),
155        };
156
157        let line1 = tle_line(line1);
158        let calculated_checksum_1 = generate_checksum(line1.as_ref());
159        if me.checksum_1 != calculated_checksum_1 {
160            return Err(Error::InvalidChecksum(
161                Line::Line1,
162                me.checksum_1,
163                calculated_checksum_1,
164            ));
165        }
166
167        let line2 = tle_line(line2);
168        let calculated_checksum_2 = generate_checksum(line2.as_ref());
169        if me.checksum_2 != calculated_checksum_2 {
170            return Err(Error::InvalidChecksum(
171                Line::Line1,
172                me.checksum_2,
173                calculated_checksum_1,
174            ));
175        }
176
177        Ok(me)
178    }
179
180    pub fn parse(line1: &[u8], line2: &[u8]) -> Result<Self, Error> {
181        let line = match validate_line(line1, Line::Line1) {
182            Ok(l) => l,
183            Err(error) => return Err(error),
184        };
185
186        let (slice, line) = line.split_at(1);
187        if slice[0] != '1' {
188            return Err(Error::LineNumber(Line::Line1, line[0]));
189        }
190
191        let line = split_space!(Line::Line1, line, 1);
192
193        let (slice, line) = line.split_at(5);
194        let Some(satellite_catalog_number_1) = as_digits(slice) else {
195            return Err(Error::SatlliteCatalogNumber(Line::Line1));
196        };
197
198        let (slice, line) = line.split_at(1);
199        let classification = match slice[0] {
200            'U' => Classification::Unclassified,
201            'C' => Classification::Classified,
202            'S' => Classification::Secret,
203            found => return Err(Error::Classification(found)),
204        };
205
206        let line = split_space!(Line::Line1, line, 8);
207
208        let (slice, line) = line.split_at(2);
209        let Some(launch_year) = as_digits(slice) else {
210            return Err(Error::InternationalDesignatorLaunchYear);
211        };
212
213        let (slice, line) = line.split_at(3);
214        let Some(launch_num) = as_digits(slice) else {
215            return Err(Error::InternationalDesignatorLaunchNumber);
216        };
217
218        let (slice, line) = line.split_at(3);
219        let launch_piece = [slice[0], slice[1], slice[2]];
220        let internal_designator = InternationalDesignator {
221            launch_year: launch_year as u8,
222            launch_num: launch_num as u16,
223            launch_piece,
224        };
225
226        let line = split_space!(Line::Line1, line, 17);
227
228        let (slice, line) = line.split_at(2);
229        let Some(epoch_year) = as_digits(slice) else {
230            return Err(Error::EpochYear);
231        };
232        let (slice, line) = line.split_at(12);
233        // TODO: Make this const evalable and remove allocations
234        let Ok(epoch_day_and_fractional_part) = String::from_iter(slice).parse::<f64>() else {
235            return Err(Error::EpochDay);
236        };
237
238        let line = split_space!(Line::Line1, line, 32);
239
240        let (slice, line) = line.split_at(10);
241        let slice = trim_leading_space(slice);
242        let Ok(first_derivative_of_mean_motion) = String::from_iter(slice).parse::<f32>() else {
243            return Err(Error::FirstDerivative);
244        };
245
246        let line = split_space!(Line::Line1, line, 43);
247
248        let (slice, line) = line.split_at(8);
249        let second_derivative_of_mean_motion = match parse_tle_f32(slice) {
250            Ok(s) => s,
251            Err(e) => return Err(e),
252        };
253
254        let line = split_space!(Line::Line1, line, 52);
255
256        let (mut slice, line) = line.split_at(8);
257        let mut sign = 1.0;
258        if slice[0] == '-' {
259            sign = -1.0;
260            let (_neg_sign, s) = slice.split_first().unwrap();
261            slice = s;
262        }
263
264        let b_star = match parse_tle_f32(slice) {
265            Ok(s) => s * sign,
266            Err(e) => return Err(e),
267        };
268
269        let line = split_space!(Line::Line1, line, 61);
270        let (slice, line) = line.split_at(1);
271        if slice[0] != '0' {
272            return Err(Error::EphemerisType(slice[0]));
273        }
274
275        let line = split_space!(Line::Line1, line, 63);
276
277        let (slice, line) = line.split_at(4);
278        let slice = trim_leading_space(slice);
279
280        let Some(element_set_number) = as_digits(slice) else {
281            return Err(Error::ElementSetNumber);
282        };
283
284        let Some(checksum_1) = as_digits(line) else {
285            return Err(Error::Checksum(Line::Line1, line[0]));
286        };
287        let checksum_1 = checksum_1 as u8;
288
289        let line = match validate_line(line2, Line::Line2) {
290            Ok(l) => l,
291            Err(error) => return Err(error),
292        };
293
294        let (slice, line) = line.split_first().unwrap();
295        if *slice != '2' {
296            return Err(Error::LineNumber(Line::Line2, *slice));
297        }
298
299        let line = split_space!(Line::Line1, line, 1);
300
301        let (slice, line) = line.split_at(5);
302        let Some(satellite_catalog_number_2) = as_digits(slice) else {
303            return Err(Error::SatlliteCatalogNumber(Line::Line2));
304        };
305
306        if satellite_catalog_number_1 != satellite_catalog_number_2 {
307            return Err(Error::SatelliteCatalogNumberMismatch(
308                satellite_catalog_number_1,
309                satellite_catalog_number_2,
310            ));
311        }
312        let satellite_catalog_number = satellite_catalog_number_1;
313
314        let line = split_space!(Line::Line2, line, 7);
315
316        let (slice, line) = line.split_at(8);
317        let slice = trim_leading_space(slice);
318        let Ok(inclination) = String::from_iter(slice).parse::<f32>() else {
319            return Err(Error::Inclination);
320        };
321
322        let line = split_space!(Line::Line2, line, 16);
323
324        let (slice, line) = line.split_at(8);
325        let Ok(right_ascension_of_ascending_node) = String::from_iter(slice).parse::<f32>() else {
326            return Err(Error::RightAscension);
327        };
328
329        let line = split_space!(Line::Line2, line, 25);
330
331        let (slice, line) = line.split_at(7);
332        let Some(eccentricity) = as_digits(slice) else {
333            return Err(Error::Eccentricty);
334        };
335        let Some(dig) = eccentricity.checked_ilog10() else {
336            return Err(Error::Eccentricty);
337        };
338        let leading_zeroes = dig as i32 - slice.len() as i32;
339        let eccentricity = (eccentricity as f32).powi(leading_zeroes);
340
341        let line = split_space!(Line::Line2, line, 33);
342
343        let (slice, line) = line.split_at(8);
344        let Ok(argument_of_perigee) = String::from_iter(slice).parse::<f32>() else {
345            return Err(Error::ArgumentOfPerigee);
346        };
347
348        let line = split_space!(Line::Line2, line, 42);
349
350        let (slice, line) = line.split_at(8);
351        let Ok(mean_anomaly) = String::from_iter(slice).parse::<f32>() else {
352            return Err(Error::MeanAnomaly);
353        };
354
355        let line = split_space!(Line::Line2, line, 51);
356
357        let (slice, line) = line.split_at(11);
358        let Ok(mean_motion) = String::from_iter(slice).parse::<f32>() else {
359            return Err(Error::MeanAnomaly);
360        };
361
362        let (slice, line) = line.split_at(5);
363        let Some(revolution_number_at_epoch) = as_digits(slice) else {
364            return Err(Error::MeanMotion);
365        };
366
367        let Some(checksum_2) = as_digits(line) else {
368            return Err(Error::Checksum(Line::Line2, line[0]));
369        };
370        let checksum_2 = checksum_2 as u8;
371
372        let me = Tle {
373            satellite_catalog_number,
374            classification,
375            international_designator: internal_designator,
376            epoch_year: epoch_year as u8,
377            epoch_day_and_fractional_part,
378            first_derivative_of_mean_motion,
379            second_derivative_of_mean_motion,
380            b_star,
381            element_set_number: element_set_number as u16,
382            checksum_1,
383            inclination,
384            right_ascension_of_ascending_node,
385            eccentricity,
386            argument_of_perigee,
387            mean_anomaly,
388            mean_motion,
389            revolution_number_at_epoch,
390            checksum_2,
391        };
392
393        Ok(me)
394    }
395}
396
397const fn trim_leading_space(line: &[char]) -> &[char] {
398    let mut blank = 0;
399    while blank <= line.len() {
400        if line[blank] == ' ' {
401            blank += 1;
402        } else {
403            break;
404        }
405    }
406    let (_trimmmed, slice) = line.split_at(blank);
407    slice
408}
409
410const fn generate_checksum(line: &[char]) -> u8 {
411    let mut i = 0;
412    let mut sum = 0;
413    while i < line.len() {
414        if line[i] == '-' {
415            sum += 1;
416        } else if let Some(dig) = line[i].to_digit(DECIMAL_RADIX) {
417            sum += dig;
418        }
419
420        i += 1;
421    }
422
423    (sum % 10) as u8
424}
425
426fn parse_tle_f32(line: &[char]) -> Result<f32, Error> {
427    let trimmed = trim_leading_space(line);
428
429    let mut idx = None;
430    let mut i = 0;
431    while i < trimmed.len() {
432        if trimmed[i] == '-' {
433            if idx.is_none() {
434                idx = Some(i);
435            } else {
436                return Err(Error::SecondDerivative);
437            }
438        }
439        i += 1;
440    }
441
442    let Some(idx) = idx else {
443        return Err(Error::SecondDerivative);
444    };
445    let (num, exp) = trimmed.split_at(idx);
446    let Some(num) = as_digits(num) else {
447        return Err(Error::SecondDerivative);
448    };
449    let Some((neg, exp)) = exp.split_first() else {
450        return Err(Error::SecondDerivative);
451    };
452    assert_eq!(*neg, '-');
453    let Some(exp) = as_digits(exp) else {
454        return Err(Error::SecondDerivative);
455    };
456
457    let val = (num as f32).powi(-(exp as i32));
458
459    Ok(val)
460}
461
462const fn validate_line(line: &[u8], line_num: Line) -> Result<[char; Tle::LINE_LEN], Error> {
463    if line.len() != Tle::LINE_LEN {
464        return Err(Error::InvalidLineSize(line_num, line.len()));
465    }
466
467    if !line.is_ascii() {
468        return Err(Error::ContainsNonAsciiCharacter(line_num));
469    }
470
471    Ok(tle_line(line))
472}
473
474/// # Panics
475///
476/// Panics if the line is less than the Tle::LINE_LEN
477const fn tle_line(line: &[u8]) -> [char; Tle::LINE_LEN] {
478    let mut arr = [char::MAX; Tle::LINE_LEN];
479    let mut i = 0;
480    while i < Tle::LINE_LEN {
481        arr[i] = line[i] as char;
482        i += 1;
483    }
484    arr
485}
486
487const fn as_digits(chars: &[char]) -> Option<u32> {
488    if chars.len() == 0 {
489        return None;
490    }
491
492    let mut val: u32 = 0;
493
494    let mut i = 0;
495
496    while i < chars.len() {
497        let Some(ascii_val) = chars[chars.len() - 1 - i].to_digit(DECIMAL_RADIX) else {
498            return None;
499        };
500        val += ascii_val * 10u32.pow(i as u32);
501        i += 1;
502    }
503
504    Some(val)
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510
511    #[test]
512    fn tle_test() {
513        // ISS
514        let line1 = b"1 25544U 98067A   08264.51782528 -.00002182  00000-0 -11606-4 0  2927";
515        let line2 = b"2 25544  51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
516        let _ = Tle::parse(line1, line2).unwrap();
517        // NOAA 14
518        let line1 = b"1 23455U 94089A   97320.90946019  .00000140  00000-0  10191-3 0  2621";
519        let line2 = b"2 23455  99.0090 272.6745 0008546 223.1686 136.8816 14.11711747148495";
520        let _ = Tle::parse(line1, line2).unwrap();
521    }
522
523    #[test]
524    fn as_digits_is_valid() {
525        let x = ['1', '2', '3', '4'];
526        assert_eq!(as_digits(&x), Some(1234_u32));
527        let x = ['0', '2', '3', '4'];
528        assert_eq!(as_digits(&x), Some(234_u32));
529        let x = ['9', '0', '0', '9'];
530        assert_eq!(as_digits(&x), Some(9009_u32));
531        let x = ['8'];
532        assert_eq!(as_digits(&x), Some(8_u32));
533    }
534}