monarch2 0.1.0-beta.1

A driver crate for the Sequans Monarch 2 Platform chips.
Documentation
use atat::atat_derive::AtatResp;
use jiff::civil;
use serde::{Deserialize, Deserializer, de};

use crate::gnss::types::QuotedF32;

/// The maximum number of tracked GNSS satellites.
static GNSS_MAX_SATS: usize = 32;

/// This notification is received when a GNSS fix is available. The notification information depends on <urc_settings> and <metrics> configuration set by the [`SetGnssConfig` (AT+LPGNSSCFG)](super::SetGnssConfig) command.
#[derive(Debug, Clone, PartialEq, AtatResp)]
pub struct GnssFixReady {
    /// Fix identifier. The memory can store ten fixes. If no free slot remains, the oldest fix is overwritten.
    #[at_arg(position = 0)]
    pub fix_id: u8,

    /// UTC time, in ISO 8601 format, of the GNSS fix. When <loc_mode> is set to "on-device location" mode by the [`SetGnssConfig` (AT+LPGNSSCFG)](super::SetGnssConfig) command, the time stamp is computed using GNSS.
    #[at_arg(position = 1)]
    pub timestamp: civil::DateTime,

    /// Duration (in milliseconds) of the fix. When <loc_mode> is set to "on-device location' mode by the [`SetGnssConfig` (AT+LPGNSSCFG)](super::SetGnssConfig) command, the duration runs from the start of the capture to the completion of the computation.
    #[at_arg(position = 2)]
    pub ttf: u32,

    /// Estimated error of the fix in metres. When <loc_mode> is set to "on-device location" mode by the [`SetGnssConfig` (AT+LPGNSSCFG)](super::SetGnssConfig) command, the confidence is estimated at 1 a (68 %).
    #[at_arg(position = 3)]
    pub confidence: QuotedF32,

    /// Latitude in degrees from -90 to 90. Only available when <loc_mode> is set to "on-device location" mode by the [`SetGnssConfig` (AT+LPGNSSCFG)](super::SetGnssConfig) command.
    #[at_arg(position = 4)]
    pub lat: QuotedF32,

    /// Longitude in degrees from -180 to 180. Only available when <loc_mode> is set to "on-device location" mode by the [`SetGnssConfig` (AT+LPGNSSCFG)](super::SetGnssConfig) command.
    #[at_arg(position = 5)]
    pub long: QuotedF32,

    /// Elevation in metres. Only available when <loc_mode> is set to "on-device location' mode by the [`SetGnssConfig` (AT+LPGNSSCFG)](super::SetGnssConfig) command. Since this figure is computed using the GRS 80 ellipsoid as reference, it is likely to depart drastically from the true (geodesic) value in some areas.
    #[at_arg(position = 6)]
    pub elev: QuotedF32,

    /// Northing speed in m/s. Only available when <loc_mode> is set to "on-device location" mode by the [`SetGnssConfig` (AT+LPGNSSCFG)](super::SetGnssConfig) command.
    #[at_arg(position = 7)]
    pub north_speed: QuotedF32,

    /// Easting speed in m/s. Only available when <loc_mode> is set to "on-device location" mode by the [`SetGnssConfig` (AT+LPGNSSCFG)](super::SetGnssConfig) command.
    #[at_arg(position = 8)]
    pub east_speed: QuotedF32,

    /// Down speed in m/s. Only available when <loc_mode> is set to "on-device location" mode by the [`SetGnssConfig` (AT+LPGNSSCFG)](super::SetGnssConfig) command.
    #[at_arg(position = 9)]
    pub down_speed: QuotedF32,

    // Base64 encoding of the GNSS raw data to be used with AT+LPGNSSSENDRAW. Maximum 256 chars.
    // This field is ignored.
    #[at_arg(position = 10)]
    pub raw_data: heapless::String<1024>,

    #[at_arg(position = 11)]
    pub sats: Option<SateliteInfos>,
}

#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct SateliteInfo {
    // Sattelite number.
    pub sat_no: heapless::String<2>,
    // CN0 figure for the satellite, in dB / Hz.
    // The minimum required signal strength is 30dB/Hz.
    pub signal_strength: u32,
}

/// List of satellite information.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct SateliteInfos(pub heapless::Vec<SateliteInfo, GNSS_MAX_SATS>);

impl<'de> Deserialize<'de> for SateliteInfos {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        // This is a very unfortunate 🍝 code. atat splits on `,` by default
        // but in our case we have data such as `("XX",100),("YY",200)`
        // (notice the comma `,` inside the parentheses).
        // What we do here is that we take all of these pairs at the end of the response
        // as one long string and then manually parse it into the sattelit info.
        let s: heapless::String<256> = heapless::String::deserialize(deserializer)?;
        let mut infos = heapless::Vec::new();

        for part in s.split_terminator("),(") {
            let cleaned = part.trim_start_matches('(').trim_end_matches(')');

            let mut fields = cleaned.splitn(2, ',');
            let num_raw = fields
                .next()
                .ok_or_else(|| de::Error::custom("Missing num"))?;
            let hx_raw = fields
                .next()
                .ok_or_else(|| de::Error::custom("Missing hx"))?;

            let num_trimmed = num_raw.trim_matches('"');
            let mut num = heapless::String::<2>::new();
            num.push_str(num_trimmed)
                .map_err(|_| de::Error::custom("num too long"))?;

            let hx: u32 = hx_raw
                .parse()
                .map_err(|_| de::Error::custom("Invalid number"))?;

            infos
                .push(SateliteInfo {
                    sat_no: num,
                    signal_strength: hx,
                })
                .map_err(|_| de::Error::custom("Too many satellites"))?;
        }

        Ok(SateliteInfos(infos))
    }
}

#[cfg(feature = "defmt")]
impl defmt::Format for GnssFixReady {
    fn format(&self, f: defmt::Formatter) {
        defmt::write!(f, "GnssFixReady",);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_gnss_fix_ready_parsing() {
        let input = b"0,\"2025-06-24T15:55:20.000000\",66563,\"20000000.000000\",\"0.000000\",\"0.000000\",\"0.000000\",\"0.000000\",\"0.000000\",\"0.000000\",\"+oyFVQ4AAADeYQAAAAAAAIADTG5IQAAAALCAxgJAAAAAAAAALkDoAwAAAwQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQEnNBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaMpaaAAAAAA=\",(\"XX\",21)\r\n";

        let got = atat::serde_at::from_slice::<GnssFixReady>(input).ok();
        let expected = Some(GnssFixReady {
            fix_id: 0,
            timestamp: civil::DateTime::from_parts(
                civil::date(2025, 6, 24),
                civil::time(15, 55, 20, 00)
            ),
            ttf: 66563,
            confidence: QuotedF32(20000000.000000),
            lat: QuotedF32(0.),
            long: QuotedF32(0.),
            elev: QuotedF32(0.),
            north_speed: QuotedF32(0.),
            east_speed: QuotedF32(0.),
            down_speed: QuotedF32(0.),
            raw_data: heapless::String::try_from(
                "+oyFVQ4AAADeYQAAAAAAAIADTG5IQAAAALCAxgJAAAAAAAAALkDoAwAAAwQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQEnNBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaMpaaAAAAAA="
            ).unwrap(),
            sats: Some(SateliteInfos(heapless::Vec::from_slice(&[
                SateliteInfo{
                    sat_no: heapless::String::try_from("XX").unwrap(),
                    signal_strength: 21,
                 }
            ]).unwrap())),
        });
        assert_eq!(got, expected);
    }
}