use crate::{
encoder::{NmeaEncode, NmeaEncoderError},
macros::{write_byte, write_str},
message::{Message, NmeaMessageError},
talker::Talker,
};
#[derive(Debug, thiserror::Error)]
pub enum NmeaSentenceError {
#[error("Sentence is missing dollar sign")]
MissingDollar,
#[error("Sentence is missing comma")]
MissingComma,
#[error("Sentence is missing checksum")]
MissingChecksum,
#[error("Sentence checksum is invalid")]
InvalidChecksum,
#[error("Sentence checksum mismatch - expected: {expected}, got: {computed}")]
ChecksumMismatch { expected: u8, computed: u8 },
#[error("Sentence is not UTF8")]
NotUtf8(#[from] core::str::Utf8Error),
#[error("Sentence is not ASCII")]
NotAscii,
#[error("Sentence tag is not 5 characters long")]
TagTooShort,
#[error("Sentence is missing field")]
MissingField,
#[error("Message Error: {0}")]
Message(#[from] NmeaMessageError),
}
#[derive(Debug)]
pub struct Sentence<'a> {
pub talker: Talker,
pub message: Message<'a>,
}
impl<'a> Sentence<'a> {
pub(crate) fn parse(slice: &'a [u8]) -> Result<Self, NmeaSentenceError> {
let slice = slice
.strip_prefix(b"$")
.ok_or(NmeaSentenceError::MissingDollar)?;
if !slice.is_ascii() {
return Err(NmeaSentenceError::NotAscii);
}
let (payload, checksum_part) = memchr::memrchr(b'*', slice)
.map(|i| (&slice[..i], &slice[i + 1..]))
.ok_or(NmeaSentenceError::MissingChecksum)?;
let checksum_str = core::str::from_utf8(
checksum_part
.get(..2)
.ok_or(NmeaSentenceError::InvalidChecksum)?,
)?;
let expected =
u8::from_str_radix(checksum_str, 16).map_err(|_| NmeaSentenceError::InvalidChecksum)?;
let computed = payload.iter().fold(0u8, |acc, &b| acc ^ b);
if computed != expected {
return Err(NmeaSentenceError::ChecksumMismatch { expected, computed });
}
let comma = memchr::memchr(b',', payload).ok_or(NmeaSentenceError::MissingComma)?;
let payload_str = core::str::from_utf8(payload)?;
let tag = &payload_str[..comma];
if tag.starts_with('P') {
return Ok(Self {
talker: Talker::Proprietary,
message: Message::Unknown(payload_str),
});
}
if tag.len() < 3 {
return Err(NmeaSentenceError::TagTooShort);
}
let talker = Talker::parse(&tag[..2]);
let code = &tag[2..];
let fields = &payload_str[comma + 1..];
let message = Message::parse(code, fields)?;
Ok(Self { talker, message })
}
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, NmeaEncoderError> {
const OVERHEAD: usize = 1 + 2 + 3 + 1 + 1 + 2 + 2;
if buf.len() < OVERHEAD + self.message.encoded_len() {
return Err(NmeaEncoderError::SentenceTooLong);
}
let mut pos = 0;
write_byte!(buf, pos, b'$');
write_str!(buf, pos, "GP");
write_str!(buf, pos, self.message.code());
write_byte!(buf, pos, b',');
pos += self.message.encode(&mut buf[pos..]);
let fields_end = pos;
let checksum = buf[1..fields_end].iter().fold(0u8, |acc, &b| acc ^ b);
write_byte!(buf, pos, b'*');
buf[pos] = b"0123456789ABCDEF"[(checksum >> 4) as usize];
pos += 1;
buf[pos] = b"0123456789ABCDEF"[(checksum & 0xF) as usize];
pos += 1;
write_byte!(buf, pos, b'\r');
write_byte!(buf, pos, b'\n');
Ok(pos)
}
}