Skip to main content

sms_types/
gnss.rs

1//! GNSS position report types.
2
3use serde::{Deserialize, Serialize};
4
5/// GNSS (Global Navigation Satellite System) fix status.
6#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
7#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
8pub enum FixStatus {
9    /// GNSS fix status is unknown.
10    Unknown,
11
12    /// No GNSS fix.
13    NotFix,
14
15    /// 2D GNSS fix (latitude and longitude only).
16    Fix2D,
17
18    /// 3D GNSS fix (latitude, longitude, and altitude).
19    Fix3D,
20}
21impl TryFrom<&str> for FixStatus {
22    type Error = String;
23
24    fn try_from(value: &str) -> Result<Self, Self::Error> {
25        match value.trim() {
26            "Location Unknown" | "Unknown" => Ok(FixStatus::Unknown),
27            "Location Not Fix" | "Not Fix" => Ok(FixStatus::NotFix),
28            "Location 2D Fix" | "2D Fix" => Ok(FixStatus::Fix2D),
29            "Location 3D Fix" | "3D Fix" => Ok(FixStatus::Fix3D),
30            _ => Err(format!("Invalid GNSS fix status: '{value}'")),
31        }
32    }
33}
34impl From<u8> for FixStatus {
35    fn from(value: u8) -> Self {
36        match value {
37            0 => FixStatus::NotFix,
38            1 => FixStatus::Fix2D,
39            2 => FixStatus::Fix3D,
40            _ => FixStatus::Unknown,
41        }
42    }
43}
44
45/// Represents a GNSS position report with optional fields for satellite info.
46#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
47#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
48pub struct PositionReport {
49    /// Indicates whether the GNSS receiver is currently running.
50    pub run_status: bool,
51
52    /// Whether a valid fix has been obtained.
53    pub fix_status: bool,
54
55    /// UTC time of the position report in ISO 8601 format.
56    pub utc_time: String,
57
58    /// Latitude in decimal degrees.
59    pub latitude: Option<f64>,
60
61    /// Longitude in decimal degrees.
62    pub longitude: Option<f64>,
63
64    /// Mean sea level altitude in meters.
65    pub msl_altitude: Option<f64>,
66
67    /// Ground speed in meters per second.
68    pub ground_speed: Option<f32>,
69
70    /// Ground course in degrees.
71    pub ground_course: Option<f32>,
72
73    /// Fix mode indicating 2D/3D fix or unknown.
74    pub fix_mode: FixStatus,
75
76    /// Horizontal Dilution of Precision.
77    pub hdop: Option<f32>,
78
79    /// Position Dilution of Precision.
80    pub pdop: Option<f32>,
81
82    /// Vertical Dilution of Precision.
83    pub vdop: Option<f32>,
84
85    /// Number of GPS satellites in view.
86    pub gps_in_view: Option<u8>,
87
88    /// Number of GNSS satellites used in the fix.
89    pub gnss_used: Option<u8>,
90
91    /// Number of GLONASS satellites in view.
92    pub glonass_in_view: Option<u8>,
93}
94impl TryFrom<Vec<&str>> for PositionReport {
95    type Error = String;
96
97    fn try_from(fields: Vec<&str>) -> Result<Self, Self::Error> {
98        if fields.len() < 15 {
99            return Err(format!(
100                "Insufficient GNSS data fields got {}",
101                fields.len()
102            ));
103        }
104
105        // Based on: https://simcom.ee/documents/SIM868/SIM868_GNSS_Application%20Note_V1.00.pdf (2.3)
106        Ok(Self {
107            run_status: fields[0] == "1",
108            fix_status: fields[1] == "1",
109            utc_time: fields[2].to_string(),
110            latitude: fields[3].parse().ok(),
111            longitude: fields[4].parse().ok(),
112            msl_altitude: fields[5].parse().ok(),
113            ground_speed: fields[6].parse().ok(),
114            ground_course: fields[7].parse().ok(),
115            fix_mode: FixStatus::from(fields[8].parse::<u8>().unwrap_or(0)),
116            // Reserved1
117            hdop: fields[10].parse().ok(),
118            pdop: fields[11].parse().ok(),
119            vdop: fields[12].parse().ok(),
120            // Reserved2
121            gps_in_view: fields[14].parse().ok(),
122            gnss_used: fields[15].parse().ok(),
123            glonass_in_view: fields[16].parse().ok(),
124        })
125    }
126}
127impl std::fmt::Display for PositionReport {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        fn convert_opt<T: std::fmt::Display>(opt: Option<&T>) -> String {
130            match opt {
131                Some(value) => value.to_string(),
132                None => "None".to_string(),
133            }
134        }
135
136        write!(
137            f,
138            "Lat: {}, Lon: {}, Alt: {}, Speed: {}, Course: {}",
139            convert_opt(self.latitude.as_ref()),
140            convert_opt(self.longitude.as_ref()),
141            convert_opt(self.msl_altitude.as_ref()),
142            convert_opt(self.ground_speed.as_ref()),
143            convert_opt(self.ground_course.as_ref())
144        )
145    }
146}