1use core::fmt;
4
5use kinavis_kernel::geodesy::Height;
6use kinavis_kernel::gnss::{Dop, FixType, GnssFix};
7use kinavis_kernel::position::Position;
8use kinavis_kernel::time::{Instant, Utc};
9use kinavis_kernel::units::Distance;
10
11use crate::error::Nmea2000Error;
12use crate::fields::{distance, position, resolution, value_of, Fields};
13use crate::frame::Payload;
14
15#[non_exhaustive]
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub enum GnssSystem {
22 Gps,
24 Glonass,
26 GpsAndGlonass,
28 GpsWithSbas,
30 GpsWithSbasAndGlonass,
32 Chayka,
34 Integrated,
36 Surveyed,
38 Galileo,
40 Reserved(u8),
42}
43
44impl GnssSystem {
45 #[must_use]
47 pub const fn from_code(code: u8) -> Self {
48 match code {
49 0 => Self::Gps,
50 1 => Self::Glonass,
51 2 => Self::GpsAndGlonass,
52 3 => Self::GpsWithSbas,
53 4 => Self::GpsWithSbasAndGlonass,
54 5 => Self::Chayka,
55 6 => Self::Integrated,
56 7 => Self::Surveyed,
57 8 => Self::Galileo,
58 _ => Self::Reserved(code),
59 }
60 }
61}
62
63impl fmt::Display for GnssSystem {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::Gps => f.write_str("GPS"),
67 Self::Glonass => f.write_str("GLONASS"),
68 Self::GpsAndGlonass => f.write_str("GPS and GLONASS"),
69 Self::GpsWithSbas => f.write_str("GPS with SBAS"),
70 Self::GpsWithSbasAndGlonass => f.write_str("GPS with SBAS, and GLONASS"),
71 Self::Chayka => f.write_str("Chayka"),
72 Self::Integrated => f.write_str("integrated navigation"),
73 Self::Surveyed => f.write_str("surveyed"),
74 Self::Galileo => f.write_str("Galileo"),
75 Self::Reserved(code) => write!(f, "reserved system {code}"),
76 }
77 }
78}
79
80#[non_exhaustive]
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
86pub enum Integrity {
87 NotChecked,
89 Safe,
91 Caution,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102pub struct GnssPosition {
103 pub sid: Option<u8>,
105 pub taken_at: Option<Instant<Utc>>,
107 pub position: Option<Position>,
109 pub altitude: Option<Height>,
111 pub system: GnssSystem,
113 pub method: Option<FixType>,
115 pub integrity: Option<Integrity>,
117 pub satellites: Option<u8>,
119 pub hdop: Option<Dop>,
121 pub pdop: Option<Dop>,
123 pub geoidal_separation: Option<Distance>,
126 pub reference_stations: Option<u8>,
128}
129
130impl GnssPosition {
131 const NEEDED: usize = 43;
133
134 pub(crate) fn decode(payload: &Payload) -> Result<Self, Nmea2000Error> {
135 let field = Fields::of(payload, Self::NEEDED)?;
136 #[allow(clippy::cast_possible_truncation)]
138 let system = GnssSystem::from_code(field.raw_bits(31 * 8, 4) as u8);
139 let method = match field.raw_bits(31 * 8 + 4, 4) {
140 0 => Some(FixType::None),
141 1 => Some(FixType::Autonomous),
142 2 => Some(FixType::Differential),
143 3 => Some(FixType::Precise),
144 4 => Some(FixType::RtkFixed),
145 5 => Some(FixType::RtkFloat),
146 6 => Some(FixType::Estimated),
147 7 => Some(FixType::Manual),
148 8 => Some(FixType::Simulated),
149 _ => None,
150 };
151 let integrity = match field.raw_bits(32 * 8, 2) {
152 0 => Some(Integrity::NotChecked),
153 1 => Some(Integrity::Safe),
154 2 => Some(Integrity::Caution),
155 _ => None,
156 };
157 Ok(Self {
158 sid: field.u8(0),
159 taken_at: taken_at(field.unsigned(1, 2), field.unsigned(3, 4)),
160 position: position(
161 field.signed(7, 8),
162 field.signed(15, 8),
163 resolution::PRECISE_POSITION_DEG,
164 )?,
165 altitude: distance(field.signed(23, 8), resolution::ALTITUDE_M, "altitude")?
166 .map(Height::above_ellipsoid),
167 system,
168 method,
169 integrity,
170 satellites: field.u8(33),
171 hdop: dop(field.signed(34, 2))?,
172 pdop: dop(field.signed(36, 2))?,
173 geoidal_separation: distance(
174 field.signed(38, 4),
175 resolution::CENTIMETRE_M,
176 "geoidal separation",
177 )?,
178 reference_stations: field.u8(42),
179 })
180 }
181
182 #[must_use]
184 pub fn fix(&self) -> Option<GnssFix> {
185 let mut builder = GnssFix::builder(self.taken_at?, self.position?);
186 if let Some(method) = self.method {
187 builder = builder.fix_type(method);
188 }
189 if let Some(satellites) = self.satellites {
190 builder = builder.satellites(satellites);
191 }
192 if let Some(hdop) = self.hdop {
193 builder = builder.hdop(hdop);
194 }
195 if let Some(pdop) = self.pdop {
196 builder = builder.pdop(pdop);
197 }
198 Some(builder.build())
199 }
200}
201
202fn taken_at(days: Option<u64>, time: Option<u64>) -> Option<Instant<Utc>> {
204 let days = i64::try_from(days?).ok()?;
205 let time = i64::try_from(time?).ok()?;
206 let nanos = days
208 .checked_mul(86_400 * 1_000_000_000)?
209 .checked_add(time.checked_mul(100_000)?)?;
210 Some(Instant::from_unix_nanos(nanos))
211}
212
213fn dop(field: Option<i64>) -> Result<Option<Dop>, Nmea2000Error> {
215 let Some(field) = field else {
216 return Ok(None);
217 };
218 #[allow(clippy::cast_precision_loss)]
220 Dop::new(field as f64 * resolution::DOP)
221 .map(Some)
222 .map_err(value_of("dilution of precision"))
223}