Skip to main content

sidereon_core/rtcm/
ephemeris.rs

1//! RTCM 3 broadcast ephemeris messages 1019 (GPS), 1020 (GLONASS),
2//! 1042 (BeiDou), 1044 (QZSS), and 1045/1046 (Galileo).
3//!
4//! Message 1019 (RTCM 10403.3 Table 3.5-21) carries one complete set of GPS
5//! LNAV ephemeris and clock parameters; message 1020 (Table 3.5-22) carries one
6//! GLONASS satellite's ephemeris. The GLONASS message stores its orbit terms in
7//! sign-and-magnitude form (the leading bit is the sign), which the bit reader's
8//! [`super::bits::BitReader::ism`] handles.
9//!
10//! Every field is stored as its raw transmitted integer (the `DFxxx` quantity),
11//! preserving the integer-vs-sign-magnitude distinction exactly, so the body
12//! round-trips byte-for-byte. The standard per-field scale factors are noted in
13//! the struct docs; applying them yields the engineering-unit ephemeris that
14//! [`crate::broadcast`] consumes.
15
16use crate::astro::time::model::{GnssWeekTow, TimeScale};
17use crate::broadcast::{ClockPolynomial, KeplerianElements};
18use crate::constants::SECONDS_PER_HOUR;
19use crate::error::{Error, Result};
20use crate::id::{GnssSatelliteId, GnssSystem};
21use crate::rinex_nav::{
22    gps_fit_interval_from_flag, gps_ura_index_to_meters, BroadcastGroupDelays, BroadcastIssue,
23    BroadcastRecord, NavMessage,
24};
25
26use super::bits::{BitReader, BitWriter};
27use super::DecodeResult;
28
29const SEMICIRCLE_TO_RAD: f64 = core::f64::consts::PI;
30const GALILEO_WEEK_OFFSET_TO_GPS: u32 = 1024;
31
32fn scaled_i(value: impl Into<i64>, exponent: i32) -> f64 {
33    (value.into() as f64) * 2.0_f64.powi(exponent)
34}
35
36fn scaled_u(value: u64, exponent: i32) -> f64 {
37    (value as f64) * 2.0_f64.powi(exponent)
38}
39
40fn scaled_semicircle(value: impl Into<i64>, exponent: i32) -> f64 {
41    scaled_i(value, exponent) * SEMICIRCLE_TO_RAD
42}
43
44fn gnss_week_tow(
45    system: TimeScale,
46    week: u32,
47    tow_s: f64,
48    field: &'static str,
49) -> Result<GnssWeekTow> {
50    GnssWeekTow::new(system, week, tow_s)
51        .and_then(GnssWeekTow::normalized)
52        .map_err(|_| Error::InvalidInput(format!("RTCM broadcast {field} is not representable")))
53}
54
55fn galileo_sisa_m(index: u8) -> f64 {
56    match index {
57        0..=49 => f64::from(index) * 0.01,
58        50..=74 => 0.50 + f64::from(index - 50) * 0.02,
59        75..=99 => 1.00 + f64::from(index - 75) * 0.04,
60        100..=125 => 2.00 + f64::from(index - 100) * 0.16,
61        _ => 8192.0,
62    }
63}
64
65fn ura_or_wide(index: u8) -> f64 {
66    gps_ura_index_to_meters(i64::from(index)).unwrap_or(8192.0)
67}
68
69fn raw_health(healthy: bool) -> f64 {
70    if healthy {
71        0.0
72    } else {
73        1.0
74    }
75}
76
77/// A decoded GPS broadcast ephemeris (message 1019).
78///
79/// Angular quantities are in semicircles (scale noted per field), harmonic
80/// correction terms in radians, distances in meters, and clock terms in
81/// seconds, each recovered by multiplying the raw integer by its scale factor.
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub struct GpsEphemeris {
84    /// GPS satellite PRN (DF009).
85    pub satellite_id: u8,
86    /// GPS week number (DF076, 10 bits).
87    pub week_number: u16,
88    /// SV accuracy / URA index (DF077, 4 bits).
89    pub sv_accuracy: u8,
90    /// Code on L2 (DF078, 2 bits).
91    pub code_on_l2: u8,
92    /// Rate of inclination angle IDOT (DF079, int14, scale 2^-43 semicircles/s).
93    pub idot: i32,
94    /// Issue of data, ephemeris (DF071, 8 bits).
95    pub iode: u8,
96    /// Clock data reference time t_oc (DF081, uint16, scale 2^4 s).
97    pub t_oc: u16,
98    /// Clock drift rate a_f2 (DF082, int8, scale 2^-55 s/s^2).
99    pub a_f2: i16,
100    /// Clock drift a_f1 (DF083, int16, scale 2^-43 s/s).
101    pub a_f1: i32,
102    /// Clock bias a_f0 (DF084, int22, scale 2^-31 s).
103    pub a_f0: i32,
104    /// Issue of data, clock (DF085, 10 bits).
105    pub iodc: u16,
106    /// Orbit-radius sine correction C_rs (DF086, int16, scale 2^-5 m).
107    pub c_rs: i32,
108    /// Mean-motion difference dn (DF087, int16, scale 2^-43 semicircles/s).
109    pub delta_n: i32,
110    /// Mean anomaly at reference time M_0 (DF088, int32, scale 2^-31 semicircles).
111    pub m0: i64,
112    /// Latitude-argument cosine correction C_uc (DF089, int16, scale 2^-29 rad).
113    pub c_uc: i32,
114    /// Eccentricity e (DF090, uint32, scale 2^-33).
115    pub eccentricity: u64,
116    /// Latitude-argument sine correction C_us (DF091, int16, scale 2^-29 rad).
117    pub c_us: i32,
118    /// Square root of the semi-major axis sqrt(A) (DF092, uint32, scale 2^-19).
119    pub sqrt_a: u64,
120    /// Ephemeris reference time t_oe (DF093, uint16, scale 2^4 s).
121    pub t_oe: u16,
122    /// Inclination cosine correction C_ic (DF094, int16, scale 2^-29 rad).
123    pub c_ic: i32,
124    /// Longitude of ascending node Omega_0 (DF095, int32, scale 2^-31 semicircles).
125    pub omega0: i64,
126    /// Inclination sine correction C_is (DF096, int16, scale 2^-29 rad).
127    pub c_is: i32,
128    /// Inclination at reference time i_0 (DF097, int32, scale 2^-31 semicircles).
129    pub i0: i64,
130    /// Orbit-radius cosine correction C_rc (DF098, int16, scale 2^-5 m).
131    pub c_rc: i32,
132    /// Argument of perigee omega (DF099, int32, scale 2^-31 semicircles).
133    pub omega: i64,
134    /// Rate of right ascension Omega-dot (DF100, int24, scale 2^-43 semicircles/s).
135    pub omega_dot: i32,
136    /// Group delay differential t_GD (DF101, int8, scale 2^-31 s).
137    pub t_gd: i16,
138    /// SV health (DF102, 6 bits).
139    pub sv_health: u8,
140    /// L2 P-data flag (DF103).
141    pub l2_p_data_flag: bool,
142    /// Fit-interval flag (DF137).
143    pub fit_interval: bool,
144}
145
146impl GpsEphemeris {
147    /// The satellite identifier for this ephemeris.
148    pub fn satellite(&self) -> Result<GnssSatelliteId> {
149        GnssSatelliteId::new(GnssSystem::Gps, self.satellite_id)
150            .map_err(|e| Error::Parse(format!("invalid GPS PRN in 1019: {e}")))
151    }
152
153    /// Decode a message 1019 body (without the transport frame).
154    pub fn decode(body: &[u8]) -> Result<Self> {
155        Self::decode_inner(body).map_err(Into::into)
156    }
157
158    pub(crate) fn decode_inner(body: &[u8]) -> DecodeResult<Self> {
159        let mut r = BitReader::new(body);
160        let message_number = r.u(12)? as u16;
161        if message_number != 1019 {
162            return Err(Error::Parse(format!(
163                "message {message_number} is not GPS ephemeris 1019"
164            ))
165            .into());
166        }
167        Ok(Self {
168            satellite_id: r.u(6)? as u8,
169            week_number: r.u(10)? as u16,
170            sv_accuracy: r.u(4)? as u8,
171            code_on_l2: r.u(2)? as u8,
172            idot: r.i(14)? as i32,
173            iode: r.u(8)? as u8,
174            t_oc: r.u(16)? as u16,
175            a_f2: r.i(8)? as i16,
176            a_f1: r.i(16)? as i32,
177            a_f0: r.i(22)? as i32,
178            iodc: r.u(10)? as u16,
179            c_rs: r.i(16)? as i32,
180            delta_n: r.i(16)? as i32,
181            m0: r.i(32)?,
182            c_uc: r.i(16)? as i32,
183            eccentricity: r.u(32)?,
184            c_us: r.i(16)? as i32,
185            sqrt_a: r.u(32)?,
186            t_oe: r.u(16)? as u16,
187            c_ic: r.i(16)? as i32,
188            omega0: r.i(32)?,
189            c_is: r.i(16)? as i32,
190            i0: r.i(32)?,
191            c_rc: r.i(16)? as i32,
192            omega: r.i(32)?,
193            omega_dot: r.i(24)? as i32,
194            t_gd: r.i(8)? as i16,
195            sv_health: r.u(6)? as u8,
196            l2_p_data_flag: r.flag()?,
197            fit_interval: r.flag()?,
198        })
199    }
200
201    /// Encode this GPS ephemeris body (without the transport frame).
202    pub fn encode(&self) -> Vec<u8> {
203        let mut w = BitWriter::new();
204        w.push_u(1019, 12);
205        w.push_u(u64::from(self.satellite_id), 6);
206        w.push_u(u64::from(self.week_number), 10);
207        w.push_u(u64::from(self.sv_accuracy), 4);
208        w.push_u(u64::from(self.code_on_l2), 2);
209        w.push_i(i64::from(self.idot), 14);
210        w.push_u(u64::from(self.iode), 8);
211        w.push_u(u64::from(self.t_oc), 16);
212        w.push_i(i64::from(self.a_f2), 8);
213        w.push_i(i64::from(self.a_f1), 16);
214        w.push_i(i64::from(self.a_f0), 22);
215        w.push_u(u64::from(self.iodc), 10);
216        w.push_i(i64::from(self.c_rs), 16);
217        w.push_i(i64::from(self.delta_n), 16);
218        w.push_i(self.m0, 32);
219        w.push_i(i64::from(self.c_uc), 16);
220        w.push_u(self.eccentricity, 32);
221        w.push_i(i64::from(self.c_us), 16);
222        w.push_u(self.sqrt_a, 32);
223        w.push_u(u64::from(self.t_oe), 16);
224        w.push_i(i64::from(self.c_ic), 16);
225        w.push_i(self.omega0, 32);
226        w.push_i(i64::from(self.c_is), 16);
227        w.push_i(self.i0, 32);
228        w.push_i(i64::from(self.c_rc), 16);
229        w.push_i(self.omega, 32);
230        w.push_i(i64::from(self.omega_dot), 24);
231        w.push_i(i64::from(self.t_gd), 8);
232        w.push_u(u64::from(self.sv_health), 6);
233        w.push_flag(self.l2_p_data_flag);
234        w.push_flag(self.fit_interval);
235        w.into_bytes()
236    }
237
238    /// Convert this decoded RTCM ephemeris to the broadcast record consumed by
239    /// the solver. `full_week` is the caller-unrolled GPS week and must agree
240    /// with the 10-bit RTCM week residue.
241    pub fn to_broadcast_record(&self, full_week: u32) -> Result<BroadcastRecord> {
242        if full_week % 1024 != u32::from(self.week_number) {
243            return Err(Error::InvalidInput(format!(
244                "GPS full week {full_week} disagrees with 10-bit RTCM week {}",
245                self.week_number
246            )));
247        }
248        let satellite_id = self.satellite()?;
249        let toe_sow = f64::from(self.t_oe) * 16.0;
250        let toc_sow = f64::from(self.t_oc) * 16.0;
251        let toe = gnss_week_tow(TimeScale::Gpst, full_week, toe_sow, "GPS toe")?;
252        let toc = gnss_week_tow(TimeScale::Gpst, full_week, toc_sow, "GPS toc")?;
253        let fit_interval_s = gps_fit_interval_from_flag(
254            i64::from(u8::from(self.fit_interval)),
255            i64::from(self.iode),
256            i64::from(self.iodc),
257        )
258        .map_err(|e| Error::InvalidInput(e.to_string()))?;
259        Ok(BroadcastRecord {
260            satellite_id,
261            message: NavMessage::GpsLnav,
262            issue_of_data: BroadcastIssue {
263                issue: u32::from(self.iode),
264                message: NavMessage::GpsLnav,
265            },
266            week: full_week,
267            toe,
268            toc,
269            elements: KeplerianElements {
270                sqrt_a: scaled_u(self.sqrt_a, -19),
271                e: scaled_u(self.eccentricity, -33),
272                m0: scaled_semicircle(self.m0, -31),
273                delta_n: scaled_semicircle(self.delta_n, -43),
274                omega0: scaled_semicircle(self.omega0, -31),
275                i0: scaled_semicircle(self.i0, -31),
276                omega: scaled_semicircle(self.omega, -31),
277                omega_dot: scaled_semicircle(self.omega_dot, -43),
278                idot: scaled_semicircle(self.idot, -43),
279                cuc: scaled_i(self.c_uc, -29),
280                cus: scaled_i(self.c_us, -29),
281                crc: scaled_i(self.c_rc, -5),
282                crs: scaled_i(self.c_rs, -5),
283                cic: scaled_i(self.c_ic, -29),
284                cis: scaled_i(self.c_is, -29),
285                toe_sow,
286            },
287            clock: ClockPolynomial {
288                af0: scaled_i(self.a_f0, -31),
289                af1: scaled_i(self.a_f1, -43),
290                af2: scaled_i(self.a_f2, -55),
291                toc_sow,
292            },
293            group_delays: BroadcastGroupDelays::gps_lnav(scaled_i(self.t_gd, -31)),
294            cnav: None,
295            sv_health: f64::from(self.sv_health),
296            sv_accuracy_m: ura_or_wide(self.sv_accuracy),
297            fit_interval_s: Some(fit_interval_s),
298        })
299    }
300}
301
302/// A decoded Galileo F/NAV broadcast ephemeris (message 1045).
303#[derive(Clone, Copy, Debug, PartialEq, Eq)]
304pub struct GalileoFnavEphemeris {
305    pub satellite_id: u8,
306    pub week_number: u16,
307    pub iod_nav: u16,
308    pub sisa: u8,
309    pub idot: i32,
310    pub t_oc: u16,
311    pub a_f2: i16,
312    pub a_f1: i32,
313    pub a_f0: i64,
314    pub c_rs: i32,
315    pub delta_n: i32,
316    pub m0: i64,
317    pub c_uc: i32,
318    pub eccentricity: u64,
319    pub c_us: i32,
320    pub sqrt_a: u64,
321    pub t_oe: u16,
322    pub c_ic: i32,
323    pub omega0: i64,
324    pub c_is: i32,
325    pub i0: i64,
326    pub c_rc: i32,
327    pub omega: i64,
328    pub omega_dot: i32,
329    pub bgd_e5a_e1: i16,
330    pub e5a_signal_health: u8,
331    pub e5a_data_validity: bool,
332    pub reserved: u8,
333}
334
335impl GalileoFnavEphemeris {
336    pub fn satellite(&self) -> Result<GnssSatelliteId> {
337        GnssSatelliteId::new(GnssSystem::Galileo, self.satellite_id)
338            .map_err(|e| Error::Parse(format!("invalid Galileo SVID in 1045: {e}")))
339    }
340
341    pub fn decode(body: &[u8]) -> Result<Self> {
342        Self::decode_inner(body).map_err(Into::into)
343    }
344
345    pub(crate) fn decode_inner(body: &[u8]) -> DecodeResult<Self> {
346        let mut r = BitReader::new(body);
347        let message_number = r.u(12)? as u16;
348        if message_number != 1045 {
349            return Err(Error::Parse(format!(
350                "message {message_number} is not Galileo F/NAV ephemeris 1045"
351            ))
352            .into());
353        }
354        Ok(Self {
355            satellite_id: r.u(6)? as u8,
356            week_number: r.u(12)? as u16,
357            iod_nav: r.u(10)? as u16,
358            sisa: r.u(8)? as u8,
359            idot: r.i(14)? as i32,
360            t_oc: r.u(14)? as u16,
361            a_f2: r.i(6)? as i16,
362            a_f1: r.i(21)? as i32,
363            a_f0: r.i(31)?,
364            c_rs: r.i(16)? as i32,
365            delta_n: r.i(16)? as i32,
366            m0: r.i(32)?,
367            c_uc: r.i(16)? as i32,
368            eccentricity: r.u(32)?,
369            c_us: r.i(16)? as i32,
370            sqrt_a: r.u(32)?,
371            t_oe: r.u(14)? as u16,
372            c_ic: r.i(16)? as i32,
373            omega0: r.i(32)?,
374            c_is: r.i(16)? as i32,
375            i0: r.i(32)?,
376            c_rc: r.i(16)? as i32,
377            omega: r.i(32)?,
378            omega_dot: r.i(24)? as i32,
379            bgd_e5a_e1: r.i(10)? as i16,
380            e5a_signal_health: r.u(2)? as u8,
381            e5a_data_validity: r.flag()?,
382            reserved: r.u(7)? as u8,
383        })
384    }
385
386    pub fn encode(&self) -> Vec<u8> {
387        let mut w = BitWriter::new();
388        w.push_u(1045, 12);
389        w.push_u(u64::from(self.satellite_id), 6);
390        w.push_u(u64::from(self.week_number), 12);
391        w.push_u(u64::from(self.iod_nav), 10);
392        w.push_u(u64::from(self.sisa), 8);
393        w.push_i(i64::from(self.idot), 14);
394        w.push_u(u64::from(self.t_oc), 14);
395        w.push_i(i64::from(self.a_f2), 6);
396        w.push_i(i64::from(self.a_f1), 21);
397        w.push_i(self.a_f0, 31);
398        w.push_i(i64::from(self.c_rs), 16);
399        w.push_i(i64::from(self.delta_n), 16);
400        w.push_i(self.m0, 32);
401        w.push_i(i64::from(self.c_uc), 16);
402        w.push_u(self.eccentricity, 32);
403        w.push_i(i64::from(self.c_us), 16);
404        w.push_u(self.sqrt_a, 32);
405        w.push_u(u64::from(self.t_oe), 14);
406        w.push_i(i64::from(self.c_ic), 16);
407        w.push_i(self.omega0, 32);
408        w.push_i(i64::from(self.c_is), 16);
409        w.push_i(self.i0, 32);
410        w.push_i(i64::from(self.c_rc), 16);
411        w.push_i(self.omega, 32);
412        w.push_i(i64::from(self.omega_dot), 24);
413        w.push_i(i64::from(self.bgd_e5a_e1), 10);
414        w.push_u(u64::from(self.e5a_signal_health), 2);
415        w.push_flag(self.e5a_data_validity);
416        w.push_u(u64::from(self.reserved), 7);
417        w.into_bytes()
418    }
419
420    pub fn to_broadcast_record(&self) -> Result<BroadcastRecord> {
421        galileo_to_record(
422            self.satellite()?,
423            u32::from(self.week_number),
424            self.iod_nav,
425            self.sisa,
426            self.idot,
427            self.t_oc,
428            self.a_f2,
429            self.a_f1,
430            self.a_f0,
431            self.c_rs,
432            self.delta_n,
433            self.m0,
434            self.c_uc,
435            self.eccentricity,
436            self.c_us,
437            self.sqrt_a,
438            self.t_oe,
439            self.c_ic,
440            self.omega0,
441            self.c_is,
442            self.i0,
443            self.c_rc,
444            self.omega,
445            self.omega_dot,
446            self.bgd_e5a_e1,
447            0,
448            raw_health(self.e5a_signal_health == 0 && !self.e5a_data_validity),
449            NavMessage::GalileoFnav,
450        )
451    }
452}
453
454/// A decoded Galileo I/NAV broadcast ephemeris (message 1046).
455#[derive(Clone, Copy, Debug, PartialEq, Eq)]
456pub struct GalileoInavEphemeris {
457    pub satellite_id: u8,
458    pub week_number: u16,
459    pub iod_nav: u16,
460    pub sisa_index: u8,
461    pub idot: i32,
462    pub t_oc: u16,
463    pub a_f2: i16,
464    pub a_f1: i32,
465    pub a_f0: i64,
466    pub c_rs: i32,
467    pub delta_n: i32,
468    pub m0: i64,
469    pub c_uc: i32,
470    pub eccentricity: u64,
471    pub c_us: i32,
472    pub sqrt_a: u64,
473    pub t_oe: u16,
474    pub c_ic: i32,
475    pub omega0: i64,
476    pub c_is: i32,
477    pub i0: i64,
478    pub c_rc: i32,
479    pub omega: i64,
480    pub omega_dot: i32,
481    pub bgd_e5a_e1: i16,
482    pub bgd_e5b_e1: i16,
483    pub e5b_signal_health: u8,
484    pub e5b_data_validity: bool,
485    pub e1b_signal_health: u8,
486    pub e1b_data_validity: bool,
487    pub reserved: u8,
488}
489
490impl GalileoInavEphemeris {
491    pub fn satellite(&self) -> Result<GnssSatelliteId> {
492        GnssSatelliteId::new(GnssSystem::Galileo, self.satellite_id)
493            .map_err(|e| Error::Parse(format!("invalid Galileo SVID in 1046: {e}")))
494    }
495
496    pub fn decode(body: &[u8]) -> Result<Self> {
497        Self::decode_inner(body).map_err(Into::into)
498    }
499
500    pub(crate) fn decode_inner(body: &[u8]) -> DecodeResult<Self> {
501        let mut r = BitReader::new(body);
502        let message_number = r.u(12)? as u16;
503        if message_number != 1046 {
504            return Err(Error::Parse(format!(
505                "message {message_number} is not Galileo I/NAV ephemeris 1046"
506            ))
507            .into());
508        }
509        Ok(Self {
510            satellite_id: r.u(6)? as u8,
511            week_number: r.u(12)? as u16,
512            iod_nav: r.u(10)? as u16,
513            sisa_index: r.u(8)? as u8,
514            idot: r.i(14)? as i32,
515            t_oc: r.u(14)? as u16,
516            a_f2: r.i(6)? as i16,
517            a_f1: r.i(21)? as i32,
518            a_f0: r.i(31)?,
519            c_rs: r.i(16)? as i32,
520            delta_n: r.i(16)? as i32,
521            m0: r.i(32)?,
522            c_uc: r.i(16)? as i32,
523            eccentricity: r.u(32)?,
524            c_us: r.i(16)? as i32,
525            sqrt_a: r.u(32)?,
526            t_oe: r.u(14)? as u16,
527            c_ic: r.i(16)? as i32,
528            omega0: r.i(32)?,
529            c_is: r.i(16)? as i32,
530            i0: r.i(32)?,
531            c_rc: r.i(16)? as i32,
532            omega: r.i(32)?,
533            omega_dot: r.i(24)? as i32,
534            bgd_e5a_e1: r.i(10)? as i16,
535            bgd_e5b_e1: r.i(10)? as i16,
536            e5b_signal_health: r.u(2)? as u8,
537            e5b_data_validity: r.flag()?,
538            e1b_signal_health: r.u(2)? as u8,
539            e1b_data_validity: r.flag()?,
540            reserved: r.u(2)? as u8,
541        })
542    }
543
544    pub fn encode(&self) -> Vec<u8> {
545        let mut w = BitWriter::new();
546        w.push_u(1046, 12);
547        w.push_u(u64::from(self.satellite_id), 6);
548        w.push_u(u64::from(self.week_number), 12);
549        w.push_u(u64::from(self.iod_nav), 10);
550        w.push_u(u64::from(self.sisa_index), 8);
551        w.push_i(i64::from(self.idot), 14);
552        w.push_u(u64::from(self.t_oc), 14);
553        w.push_i(i64::from(self.a_f2), 6);
554        w.push_i(i64::from(self.a_f1), 21);
555        w.push_i(self.a_f0, 31);
556        w.push_i(i64::from(self.c_rs), 16);
557        w.push_i(i64::from(self.delta_n), 16);
558        w.push_i(self.m0, 32);
559        w.push_i(i64::from(self.c_uc), 16);
560        w.push_u(self.eccentricity, 32);
561        w.push_i(i64::from(self.c_us), 16);
562        w.push_u(self.sqrt_a, 32);
563        w.push_u(u64::from(self.t_oe), 14);
564        w.push_i(i64::from(self.c_ic), 16);
565        w.push_i(self.omega0, 32);
566        w.push_i(i64::from(self.c_is), 16);
567        w.push_i(self.i0, 32);
568        w.push_i(i64::from(self.c_rc), 16);
569        w.push_i(self.omega, 32);
570        w.push_i(i64::from(self.omega_dot), 24);
571        w.push_i(i64::from(self.bgd_e5a_e1), 10);
572        w.push_i(i64::from(self.bgd_e5b_e1), 10);
573        w.push_u(u64::from(self.e5b_signal_health), 2);
574        w.push_flag(self.e5b_data_validity);
575        w.push_u(u64::from(self.e1b_signal_health), 2);
576        w.push_flag(self.e1b_data_validity);
577        w.push_u(u64::from(self.reserved), 2);
578        w.into_bytes()
579    }
580
581    pub fn to_broadcast_record(&self) -> Result<BroadcastRecord> {
582        galileo_to_record(
583            self.satellite()?,
584            u32::from(self.week_number),
585            self.iod_nav,
586            self.sisa_index,
587            self.idot,
588            self.t_oc,
589            self.a_f2,
590            self.a_f1,
591            self.a_f0,
592            self.c_rs,
593            self.delta_n,
594            self.m0,
595            self.c_uc,
596            self.eccentricity,
597            self.c_us,
598            self.sqrt_a,
599            self.t_oe,
600            self.c_ic,
601            self.omega0,
602            self.c_is,
603            self.i0,
604            self.c_rc,
605            self.omega,
606            self.omega_dot,
607            self.bgd_e5a_e1,
608            self.bgd_e5b_e1,
609            raw_health(
610                self.e1b_signal_health == 0
611                    && !self.e1b_data_validity
612                    && self.e5b_signal_health == 0
613                    && !self.e5b_data_validity,
614            ),
615            NavMessage::GalileoInav,
616        )
617    }
618}
619
620#[allow(clippy::too_many_arguments)]
621fn galileo_to_record(
622    satellite_id: GnssSatelliteId,
623    week: u32,
624    iod_nav: u16,
625    sisa: u8,
626    idot: i32,
627    t_oc: u16,
628    a_f2: i16,
629    a_f1: i32,
630    a_f0: i64,
631    c_rs: i32,
632    delta_n: i32,
633    m0: i64,
634    c_uc: i32,
635    eccentricity: u64,
636    c_us: i32,
637    sqrt_a: u64,
638    t_oe: u16,
639    c_ic: i32,
640    omega0: i64,
641    c_is: i32,
642    i0: i64,
643    c_rc: i32,
644    omega: i64,
645    omega_dot: i32,
646    bgd_e5a_e1: i16,
647    bgd_e5b_e1: i16,
648    sv_health: f64,
649    message: NavMessage,
650) -> Result<BroadcastRecord> {
651    let toe_sow = f64::from(t_oe) * 60.0;
652    let toc_sow = f64::from(t_oc) * 60.0;
653    let gps_aligned_week = week
654        .checked_add(GALILEO_WEEK_OFFSET_TO_GPS)
655        .ok_or_else(|| Error::InvalidInput("RTCM Galileo week overflows GPST axis".to_string()))?;
656    let toe = gnss_week_tow(TimeScale::Gst, gps_aligned_week, toe_sow, "Galileo toe")?;
657    let toc = gnss_week_tow(TimeScale::Gst, gps_aligned_week, toc_sow, "Galileo toc")?;
658    Ok(BroadcastRecord {
659        satellite_id,
660        message,
661        issue_of_data: BroadcastIssue {
662            issue: u32::from(iod_nav),
663            message,
664        },
665        week: gps_aligned_week,
666        toe,
667        toc,
668        elements: KeplerianElements {
669            sqrt_a: scaled_u(sqrt_a, -19),
670            e: scaled_u(eccentricity, -33),
671            m0: scaled_semicircle(m0, -31),
672            delta_n: scaled_semicircle(delta_n, -43),
673            omega0: scaled_semicircle(omega0, -31),
674            i0: scaled_semicircle(i0, -31),
675            omega: scaled_semicircle(omega, -31),
676            omega_dot: scaled_semicircle(omega_dot, -43),
677            idot: scaled_semicircle(idot, -43),
678            cuc: scaled_i(c_uc, -29),
679            cus: scaled_i(c_us, -29),
680            crc: scaled_i(c_rc, -5),
681            crs: scaled_i(c_rs, -5),
682            cic: scaled_i(c_ic, -29),
683            cis: scaled_i(c_is, -29),
684            toe_sow,
685        },
686        clock: ClockPolynomial {
687            af0: scaled_i(a_f0, -34),
688            af1: scaled_i(a_f1, -46),
689            af2: scaled_i(a_f2, -59),
690            toc_sow,
691        },
692        group_delays: BroadcastGroupDelays::galileo(
693            scaled_i(bgd_e5a_e1, -32),
694            scaled_i(bgd_e5b_e1, -32),
695        ),
696        cnav: None,
697        sv_health,
698        sv_accuracy_m: galileo_sisa_m(sisa),
699        fit_interval_s: None,
700    })
701}
702
703/// A decoded BeiDou broadcast ephemeris (message 1042).
704#[derive(Clone, Copy, Debug, PartialEq, Eq)]
705pub struct BeidouEphemeris {
706    pub satellite_id: u8,
707    pub week_number: u16,
708    pub sv_urai: u8,
709    pub idot: i32,
710    pub aode: u8,
711    pub t_oc: u32,
712    pub a_f2: i16,
713    pub a_f1: i32,
714    pub a_f0: i32,
715    pub aodc: u8,
716    pub c_rs: i32,
717    pub delta_n: i32,
718    pub m0: i64,
719    pub c_uc: i32,
720    pub eccentricity: u64,
721    pub c_us: i32,
722    pub sqrt_a: u64,
723    pub t_oe: u32,
724    pub c_ic: i32,
725    pub omega0: i64,
726    pub c_is: i32,
727    pub i0: i64,
728    pub c_rc: i32,
729    pub omega: i64,
730    pub omega_dot: i32,
731    pub t_gd1: i16,
732    pub t_gd2: i16,
733    pub sv_health: bool,
734}
735
736impl BeidouEphemeris {
737    pub fn satellite(&self) -> Result<GnssSatelliteId> {
738        GnssSatelliteId::new(GnssSystem::BeiDou, self.satellite_id)
739            .map_err(|e| Error::Parse(format!("invalid BeiDou satellite ID in 1042: {e}")))
740    }
741
742    pub fn decode(body: &[u8]) -> Result<Self> {
743        Self::decode_inner(body).map_err(Into::into)
744    }
745
746    pub(crate) fn decode_inner(body: &[u8]) -> DecodeResult<Self> {
747        let mut r = BitReader::new(body);
748        let message_number = r.u(12)? as u16;
749        if message_number != 1042 {
750            return Err(Error::Parse(format!(
751                "message {message_number} is not BeiDou ephemeris 1042"
752            ))
753            .into());
754        }
755        Ok(Self {
756            satellite_id: r.u(6)? as u8,
757            week_number: r.u(13)? as u16,
758            sv_urai: r.u(4)? as u8,
759            idot: r.i(14)? as i32,
760            aode: r.u(5)? as u8,
761            t_oc: r.u(17)? as u32,
762            a_f2: r.i(11)? as i16,
763            a_f1: r.i(22)? as i32,
764            a_f0: r.i(24)? as i32,
765            aodc: r.u(5)? as u8,
766            c_rs: r.i(18)? as i32,
767            delta_n: r.i(16)? as i32,
768            m0: r.i(32)?,
769            c_uc: r.i(18)? as i32,
770            eccentricity: r.u(32)?,
771            c_us: r.i(18)? as i32,
772            sqrt_a: r.u(32)?,
773            t_oe: r.u(17)? as u32,
774            c_ic: r.i(18)? as i32,
775            omega0: r.i(32)?,
776            c_is: r.i(18)? as i32,
777            i0: r.i(32)?,
778            c_rc: r.i(18)? as i32,
779            omega: r.i(32)?,
780            omega_dot: r.i(24)? as i32,
781            t_gd1: r.i(10)? as i16,
782            t_gd2: r.i(10)? as i16,
783            sv_health: r.flag()?,
784        })
785    }
786
787    pub fn encode(&self) -> Vec<u8> {
788        let mut w = BitWriter::new();
789        w.push_u(1042, 12);
790        w.push_u(u64::from(self.satellite_id), 6);
791        w.push_u(u64::from(self.week_number), 13);
792        w.push_u(u64::from(self.sv_urai), 4);
793        w.push_i(i64::from(self.idot), 14);
794        w.push_u(u64::from(self.aode), 5);
795        w.push_u(u64::from(self.t_oc), 17);
796        w.push_i(i64::from(self.a_f2), 11);
797        w.push_i(i64::from(self.a_f1), 22);
798        w.push_i(i64::from(self.a_f0), 24);
799        w.push_u(u64::from(self.aodc), 5);
800        w.push_i(i64::from(self.c_rs), 18);
801        w.push_i(i64::from(self.delta_n), 16);
802        w.push_i(self.m0, 32);
803        w.push_i(i64::from(self.c_uc), 18);
804        w.push_u(self.eccentricity, 32);
805        w.push_i(i64::from(self.c_us), 18);
806        w.push_u(self.sqrt_a, 32);
807        w.push_u(u64::from(self.t_oe), 17);
808        w.push_i(i64::from(self.c_ic), 18);
809        w.push_i(self.omega0, 32);
810        w.push_i(i64::from(self.c_is), 18);
811        w.push_i(self.i0, 32);
812        w.push_i(i64::from(self.c_rc), 18);
813        w.push_i(self.omega, 32);
814        w.push_i(i64::from(self.omega_dot), 24);
815        w.push_i(i64::from(self.t_gd1), 10);
816        w.push_i(i64::from(self.t_gd2), 10);
817        w.push_flag(self.sv_health);
818        w.into_bytes()
819    }
820
821    pub fn to_broadcast_record(&self) -> Result<BroadcastRecord> {
822        let satellite_id = self.satellite()?;
823        let week = u32::from(self.week_number);
824        let toe_sow = f64::from(self.t_oe) * 8.0;
825        let toc_sow = f64::from(self.t_oc) * 8.0;
826        let toe = gnss_week_tow(TimeScale::Bdt, week, toe_sow, "BeiDou toe")?;
827        let toc = gnss_week_tow(TimeScale::Bdt, week, toc_sow, "BeiDou toc")?;
828        let message = if crate::rinex_nav::is_beidou_geo(satellite_id) {
829            NavMessage::BeidouD2
830        } else {
831            NavMessage::BeidouD1
832        };
833        Ok(BroadcastRecord {
834            satellite_id,
835            message,
836            issue_of_data: BroadcastIssue {
837                issue: u32::from(self.aode),
838                message,
839            },
840            week,
841            toe,
842            toc,
843            elements: KeplerianElements {
844                sqrt_a: scaled_u(self.sqrt_a, -19),
845                e: scaled_u(self.eccentricity, -33),
846                m0: scaled_semicircle(self.m0, -31),
847                delta_n: scaled_semicircle(self.delta_n, -43),
848                omega0: scaled_semicircle(self.omega0, -31),
849                i0: scaled_semicircle(self.i0, -31),
850                omega: scaled_semicircle(self.omega, -31),
851                omega_dot: scaled_semicircle(self.omega_dot, -43),
852                idot: scaled_semicircle(self.idot, -43),
853                cuc: scaled_i(self.c_uc, -31),
854                cus: scaled_i(self.c_us, -31),
855                crc: scaled_i(self.c_rc, -6),
856                crs: scaled_i(self.c_rs, -6),
857                cic: scaled_i(self.c_ic, -31),
858                cis: scaled_i(self.c_is, -31),
859                toe_sow,
860            },
861            clock: ClockPolynomial {
862                af0: scaled_i(self.a_f0, -33),
863                af1: scaled_i(self.a_f1, -50),
864                af2: scaled_i(self.a_f2, -66),
865                toc_sow,
866            },
867            group_delays: BroadcastGroupDelays::beidou(
868                f64::from(self.t_gd1) * 1.0e-10,
869                f64::from(self.t_gd2) * 1.0e-10,
870            ),
871            cnav: None,
872            sv_health: f64::from(u8::from(self.sv_health)),
873            sv_accuracy_m: ura_or_wide(self.sv_urai),
874            fit_interval_s: None,
875        })
876    }
877}
878
879/// A decoded QZSS broadcast ephemeris (message 1044).
880#[derive(Clone, Copy, Debug, PartialEq, Eq)]
881pub struct QzssEphemeris {
882    pub satellite_id: u8,
883    pub t_oc: u16,
884    pub a_f2: i16,
885    pub a_f1: i32,
886    pub a_f0: i32,
887    pub iode: u8,
888    pub c_rs: i32,
889    pub delta_n: i32,
890    pub m0: i64,
891    pub c_uc: i32,
892    pub eccentricity: u64,
893    pub c_us: i32,
894    pub sqrt_a: u64,
895    pub t_oe: u16,
896    pub c_ic: i32,
897    pub omega0: i64,
898    pub c_is: i32,
899    pub i0: i64,
900    pub c_rc: i32,
901    pub omega: i64,
902    pub omega_dot: i32,
903    pub idot: i32,
904    pub codes_on_l2: u8,
905    pub week_number: u16,
906    pub ura: u8,
907    pub sv_health: u8,
908    pub t_gd: i16,
909    pub iodc: u16,
910    pub fit_interval: bool,
911}
912
913impl QzssEphemeris {
914    pub fn satellite(&self) -> Result<GnssSatelliteId> {
915        GnssSatelliteId::new(GnssSystem::Qzss, self.satellite_id)
916            .map_err(|e| Error::Parse(format!("invalid QZSS satellite ID in 1044: {e}")))
917    }
918
919    pub fn decode(body: &[u8]) -> Result<Self> {
920        Self::decode_inner(body).map_err(Into::into)
921    }
922
923    pub(crate) fn decode_inner(body: &[u8]) -> DecodeResult<Self> {
924        let mut r = BitReader::new(body);
925        let message_number = r.u(12)? as u16;
926        if message_number != 1044 {
927            return Err(Error::Parse(format!(
928                "message {message_number} is not QZSS ephemeris 1044"
929            ))
930            .into());
931        }
932        Ok(Self {
933            satellite_id: r.u(4)? as u8,
934            t_oc: r.u(16)? as u16,
935            a_f2: r.i(8)? as i16,
936            a_f1: r.i(16)? as i32,
937            a_f0: r.i(22)? as i32,
938            iode: r.u(8)? as u8,
939            c_rs: r.i(16)? as i32,
940            delta_n: r.i(16)? as i32,
941            m0: r.i(32)?,
942            c_uc: r.i(16)? as i32,
943            eccentricity: r.u(32)?,
944            c_us: r.i(16)? as i32,
945            sqrt_a: r.u(32)?,
946            t_oe: r.u(16)? as u16,
947            c_ic: r.i(16)? as i32,
948            omega0: r.i(32)?,
949            c_is: r.i(16)? as i32,
950            i0: r.i(32)?,
951            c_rc: r.i(16)? as i32,
952            omega: r.i(32)?,
953            omega_dot: r.i(24)? as i32,
954            idot: r.i(14)? as i32,
955            codes_on_l2: r.u(2)? as u8,
956            week_number: r.u(10)? as u16,
957            ura: r.u(4)? as u8,
958            sv_health: r.u(6)? as u8,
959            t_gd: r.i(8)? as i16,
960            iodc: r.u(10)? as u16,
961            fit_interval: r.flag()?,
962        })
963    }
964
965    pub fn encode(&self) -> Vec<u8> {
966        let mut w = BitWriter::new();
967        w.push_u(1044, 12);
968        w.push_u(u64::from(self.satellite_id), 4);
969        w.push_u(u64::from(self.t_oc), 16);
970        w.push_i(i64::from(self.a_f2), 8);
971        w.push_i(i64::from(self.a_f1), 16);
972        w.push_i(i64::from(self.a_f0), 22);
973        w.push_u(u64::from(self.iode), 8);
974        w.push_i(i64::from(self.c_rs), 16);
975        w.push_i(i64::from(self.delta_n), 16);
976        w.push_i(self.m0, 32);
977        w.push_i(i64::from(self.c_uc), 16);
978        w.push_u(self.eccentricity, 32);
979        w.push_i(i64::from(self.c_us), 16);
980        w.push_u(self.sqrt_a, 32);
981        w.push_u(u64::from(self.t_oe), 16);
982        w.push_i(i64::from(self.c_ic), 16);
983        w.push_i(self.omega0, 32);
984        w.push_i(i64::from(self.c_is), 16);
985        w.push_i(self.i0, 32);
986        w.push_i(i64::from(self.c_rc), 16);
987        w.push_i(self.omega, 32);
988        w.push_i(i64::from(self.omega_dot), 24);
989        w.push_i(i64::from(self.idot), 14);
990        w.push_u(u64::from(self.codes_on_l2), 2);
991        w.push_u(u64::from(self.week_number), 10);
992        w.push_u(u64::from(self.ura), 4);
993        w.push_u(u64::from(self.sv_health), 6);
994        w.push_i(i64::from(self.t_gd), 8);
995        w.push_u(u64::from(self.iodc), 10);
996        w.push_flag(self.fit_interval);
997        w.into_bytes()
998    }
999
1000    pub fn to_broadcast_record(&self, full_week: u32) -> Result<BroadcastRecord> {
1001        if full_week % 1024 != u32::from(self.week_number) {
1002            return Err(Error::InvalidInput(format!(
1003                "QZSS full week {full_week} disagrees with 10-bit RTCM week {}",
1004                self.week_number
1005            )));
1006        }
1007        let satellite_id = self.satellite()?;
1008        let toe_sow = f64::from(self.t_oe) * 16.0;
1009        let toc_sow = f64::from(self.t_oc) * 16.0;
1010        let toe = gnss_week_tow(TimeScale::Gpst, full_week, toe_sow, "QZSS toe")?;
1011        let toc = gnss_week_tow(TimeScale::Gpst, full_week, toc_sow, "QZSS toc")?;
1012        Ok(BroadcastRecord {
1013            satellite_id,
1014            message: NavMessage::QzssLnav,
1015            issue_of_data: BroadcastIssue {
1016                issue: u32::from(self.iode),
1017                message: NavMessage::QzssLnav,
1018            },
1019            week: full_week,
1020            toe,
1021            toc,
1022            elements: KeplerianElements {
1023                sqrt_a: scaled_u(self.sqrt_a, -19),
1024                e: scaled_u(self.eccentricity, -33),
1025                m0: scaled_semicircle(self.m0, -31),
1026                delta_n: scaled_semicircle(self.delta_n, -43),
1027                omega0: scaled_semicircle(self.omega0, -31),
1028                i0: scaled_semicircle(self.i0, -31),
1029                omega: scaled_semicircle(self.omega, -31),
1030                omega_dot: scaled_semicircle(self.omega_dot, -43),
1031                idot: scaled_semicircle(self.idot, -43),
1032                cuc: scaled_i(self.c_uc, -29),
1033                cus: scaled_i(self.c_us, -29),
1034                crc: scaled_i(self.c_rc, -5),
1035                crs: scaled_i(self.c_rs, -5),
1036                cic: scaled_i(self.c_ic, -29),
1037                cis: scaled_i(self.c_is, -29),
1038                toe_sow,
1039            },
1040            clock: ClockPolynomial {
1041                af0: scaled_i(self.a_f0, -31),
1042                af1: scaled_i(self.a_f1, -43),
1043                af2: scaled_i(self.a_f2, -55),
1044                toc_sow,
1045            },
1046            group_delays: BroadcastGroupDelays::gps_lnav(scaled_i(self.t_gd, -31)),
1047            cnav: None,
1048            sv_health: f64::from(self.sv_health),
1049            sv_accuracy_m: ura_or_wide(self.ura),
1050            fit_interval_s: Some(if self.fit_interval {
1051                6.0 * SECONDS_PER_HOUR
1052            } else {
1053                2.0 * SECONDS_PER_HOUR
1054            }),
1055        })
1056    }
1057}
1058
1059/// A decoded GLONASS broadcast ephemeris (message 1020).
1060///
1061/// The orbit position / velocity / acceleration terms use sign-and-magnitude
1062/// integers (DF111..DF119). Every field below is the raw transmitted integer;
1063/// the noted scale factors recover km, km/s, km/s^2, and seconds.
1064#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1065pub struct GlonassEphemeris {
1066    /// GLONASS satellite slot number (DF038, 6 bits).
1067    pub satellite_id: u8,
1068    /// Frequency channel number (DF040, 5 bits; the wire value is k + 7).
1069    pub frequency_channel: u8,
1070    /// Almanac health C_n (DF104).
1071    pub almanac_health: bool,
1072    /// Almanac health availability (DF105).
1073    pub almanac_health_availability: bool,
1074    /// P1 flag (DF106, 2 bits).
1075    pub p1: u8,
1076    /// Frame time t_k (DF107, 12 bits).
1077    pub t_k: u16,
1078    /// MSB of the B_n health word (DF108).
1079    pub b_n_msb: bool,
1080    /// P2 flag (DF109).
1081    pub p2: bool,
1082    /// Ephemeris reference time t_b (DF110, 7 bits).
1083    pub t_b: u8,
1084    /// X-velocity (DF111, sign-magnitude 24-bit, scale 2^-20 km/s).
1085    pub xn_dot: i32,
1086    /// X-position (DF112, sign-magnitude 27-bit, scale 2^-11 km).
1087    pub xn: i32,
1088    /// X-acceleration (DF113, sign-magnitude 5-bit, scale 2^-30 km/s^2).
1089    pub xn_dot_dot: i8,
1090    /// Y-velocity (DF114, sign-magnitude 24-bit, scale 2^-20 km/s).
1091    pub yn_dot: i32,
1092    /// Y-position (DF115, sign-magnitude 27-bit, scale 2^-11 km).
1093    pub yn: i32,
1094    /// Y-acceleration (DF116, sign-magnitude 5-bit, scale 2^-30 km/s^2).
1095    pub yn_dot_dot: i8,
1096    /// Z-velocity (DF117, sign-magnitude 24-bit, scale 2^-20 km/s).
1097    pub zn_dot: i32,
1098    /// Z-position (DF118, sign-magnitude 27-bit, scale 2^-11 km).
1099    pub zn: i32,
1100    /// Z-acceleration (DF119, sign-magnitude 5-bit, scale 2^-30 km/s^2).
1101    pub zn_dot_dot: i8,
1102    /// P3 flag (DF120).
1103    pub p3: bool,
1104    /// Relative carrier-frequency offset gamma_n (DF121, sign-magnitude 11-bit,
1105    /// scale 2^-40).
1106    pub gamma_n: i16,
1107    /// GLONASS-M P flag (DF122, 2 bits).
1108    pub m_p: u8,
1109    /// Third-string l_n health flag (DF123).
1110    pub m_l_n_third: bool,
1111    /// Clock bias tau_n (DF124, sign-magnitude 22-bit, scale 2^-30 s).
1112    pub tau_n: i32,
1113    /// Inter-frequency bias delta_tau_n (DF125, sign-magnitude 5-bit, scale
1114    /// 2^-30 s).
1115    pub delta_tau_n: i8,
1116    /// Age of operation E_n (DF126, 5 bits, days).
1117    pub e_n: u8,
1118    /// GLONASS-M P4 flag (DF127).
1119    pub m_p4: bool,
1120    /// GLONASS-M F_t accuracy index (DF128, 4 bits).
1121    pub m_f_t: u8,
1122    /// GLONASS-M N_t calendar day number (DF129, 11 bits).
1123    pub m_n_t: u16,
1124    /// GLONASS-M M satellite type (DF130, 2 bits).
1125    pub m_m: u8,
1126    /// Additional data availability (DF131).
1127    pub additional_data_available: bool,
1128    /// N_A almanac reference day (DF132, 11 bits).
1129    pub n_a: u16,
1130    /// System time scale offset tau_c (DF133, sign-magnitude 32-bit, scale
1131    /// 2^-31 s).
1132    pub tau_c: i64,
1133    /// GLONASS-M N_4 four-year interval number (DF134, 5 bits).
1134    pub m_n4: u8,
1135    /// GLONASS-M tau_GPS offset to GPS time (DF135, sign-magnitude 22-bit, scale
1136    /// 2^-30 s).
1137    pub m_tau_gps: i32,
1138    /// Fifth-string l_n health flag (DF136).
1139    pub m_l_n_fifth: bool,
1140    /// Reserved field DF001 (7 bits), preserved for exact round-trip.
1141    pub reserved: u8,
1142}
1143
1144impl GlonassEphemeris {
1145    /// The satellite identifier for this ephemeris.
1146    pub fn satellite(&self) -> Result<GnssSatelliteId> {
1147        GnssSatelliteId::new(GnssSystem::Glonass, self.satellite_id)
1148            .map_err(|e| Error::Parse(format!("invalid GLONASS slot in 1020: {e}")))
1149    }
1150
1151    /// Decode a message 1020 body (without the transport frame).
1152    pub fn decode(body: &[u8]) -> Result<Self> {
1153        Self::decode_inner(body).map_err(Into::into)
1154    }
1155
1156    pub(crate) fn decode_inner(body: &[u8]) -> DecodeResult<Self> {
1157        let mut r = BitReader::new(body);
1158        let message_number = r.u(12)? as u16;
1159        if message_number != 1020 {
1160            return Err(Error::Parse(format!(
1161                "message {message_number} is not GLONASS ephemeris 1020"
1162            ))
1163            .into());
1164        }
1165        Ok(Self {
1166            satellite_id: r.u(6)? as u8,
1167            frequency_channel: r.u(5)? as u8,
1168            almanac_health: r.flag()?,
1169            almanac_health_availability: r.flag()?,
1170            p1: r.u(2)? as u8,
1171            t_k: r.u(12)? as u16,
1172            b_n_msb: r.flag()?,
1173            p2: r.flag()?,
1174            t_b: r.u(7)? as u8,
1175            xn_dot: r.ism(24)? as i32,
1176            xn: r.ism(27)? as i32,
1177            xn_dot_dot: r.ism(5)? as i8,
1178            yn_dot: r.ism(24)? as i32,
1179            yn: r.ism(27)? as i32,
1180            yn_dot_dot: r.ism(5)? as i8,
1181            zn_dot: r.ism(24)? as i32,
1182            zn: r.ism(27)? as i32,
1183            zn_dot_dot: r.ism(5)? as i8,
1184            p3: r.flag()?,
1185            gamma_n: r.ism(11)? as i16,
1186            m_p: r.u(2)? as u8,
1187            m_l_n_third: r.flag()?,
1188            tau_n: r.ism(22)? as i32,
1189            delta_tau_n: r.ism(5)? as i8,
1190            e_n: r.u(5)? as u8,
1191            m_p4: r.flag()?,
1192            m_f_t: r.u(4)? as u8,
1193            m_n_t: r.u(11)? as u16,
1194            m_m: r.u(2)? as u8,
1195            additional_data_available: r.flag()?,
1196            n_a: r.u(11)? as u16,
1197            tau_c: r.ism(32)?,
1198            m_n4: r.u(5)? as u8,
1199            m_tau_gps: r.ism(22)? as i32,
1200            m_l_n_fifth: r.flag()?,
1201            reserved: r.u(7)? as u8,
1202        })
1203    }
1204
1205    /// Encode this GLONASS ephemeris body (without the transport frame).
1206    pub fn encode(&self) -> Vec<u8> {
1207        let mut w = BitWriter::new();
1208        w.push_u(1020, 12);
1209        w.push_u(u64::from(self.satellite_id), 6);
1210        w.push_u(u64::from(self.frequency_channel), 5);
1211        w.push_flag(self.almanac_health);
1212        w.push_flag(self.almanac_health_availability);
1213        w.push_u(u64::from(self.p1), 2);
1214        w.push_u(u64::from(self.t_k), 12);
1215        w.push_flag(self.b_n_msb);
1216        w.push_flag(self.p2);
1217        w.push_u(u64::from(self.t_b), 7);
1218        w.push_ism(i64::from(self.xn_dot), 24);
1219        w.push_ism(i64::from(self.xn), 27);
1220        w.push_ism(i64::from(self.xn_dot_dot), 5);
1221        w.push_ism(i64::from(self.yn_dot), 24);
1222        w.push_ism(i64::from(self.yn), 27);
1223        w.push_ism(i64::from(self.yn_dot_dot), 5);
1224        w.push_ism(i64::from(self.zn_dot), 24);
1225        w.push_ism(i64::from(self.zn), 27);
1226        w.push_ism(i64::from(self.zn_dot_dot), 5);
1227        w.push_flag(self.p3);
1228        w.push_ism(i64::from(self.gamma_n), 11);
1229        w.push_u(u64::from(self.m_p), 2);
1230        w.push_flag(self.m_l_n_third);
1231        w.push_ism(i64::from(self.tau_n), 22);
1232        w.push_ism(i64::from(self.delta_tau_n), 5);
1233        w.push_u(u64::from(self.e_n), 5);
1234        w.push_flag(self.m_p4);
1235        w.push_u(u64::from(self.m_f_t), 4);
1236        w.push_u(u64::from(self.m_n_t), 11);
1237        w.push_u(u64::from(self.m_m), 2);
1238        w.push_flag(self.additional_data_available);
1239        w.push_u(u64::from(self.n_a), 11);
1240        w.push_ism(self.tau_c, 32);
1241        w.push_u(u64::from(self.m_n4), 5);
1242        w.push_ism(i64::from(self.m_tau_gps), 22);
1243        w.push_flag(self.m_l_n_fifth);
1244        w.push_u(u64::from(self.reserved), 7);
1245        w.into_bytes()
1246    }
1247}