use crate::error::AprsError;
use crate::types::{Extension, Position, Timestamp};
use crate::weather::AprsWeatherData;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AprsPosition {
pub timestamp: Option<Timestamp>,
pub messaging_supported: bool,
pub position: Position,
pub extension: Option<Extension>,
pub weather: Option<AprsWeatherData>,
pub frequency_mhz: Option<f32>,
pub comment: Vec<u8>,
}
impl AprsPosition {
pub(crate) fn parse(info: &[u8]) -> Result<Self, AprsError> {
let dti = *info.first().ok_or(AprsError::EmptyPacket)?;
let messaging_supported = dti == b'=' || dti == b'@';
let has_timestamp = dti == b'@' || dti == b'/';
let (b, timestamp) = if has_timestamp {
let ts_bytes = info.get(1..8)
.ok_or(AprsError::TruncatedPacket { expected: 8, got: info.len() })?;
(
info.get(8..).unwrap_or_default(),
Some(Timestamp::parse(ts_bytes)?),
)
} else {
(info.get(1..).unwrap_or_default(), None)
};
let (remaining, position) = Position::parse(b)?;
let comment = remaining.unwrap_or_default().to_vec();
let extension = Extension::parse(&comment);
let weather = if position.symbol.table == '/' && position.symbol.code == '_' {
crate::weather::AprsWeatherData::parse(&comment).ok()
} else {
None
};
let frequency_mhz = crate::util::extract_frequency_mhz(&comment);
Ok(Self {
timestamp,
messaging_supported,
position,
extension,
weather,
frequency_mhz,
comment,
})
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::new();
let dti = match (self.timestamp.is_some(), self.messaging_supported) {
(true, true) => b'@',
(true, false) => b'/',
(false, true) => b'=',
(false, false) => b'!',
};
out.push(dti);
if let Some(ref ts) = self.timestamp {
ts.encode(&mut out);
}
if self.position.compressed_cs.is_some() {
self.position.encode_compressed(&mut out);
} else {
self.position.encode_uncompressed(&mut out);
}
out.extend_from_slice(&self.comment);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn no_timestamp_no_messaging() {
let pos = AprsPosition::parse(b"!4903.50N/07201.75W-").unwrap();
assert!(pos.timestamp.is_none());
assert!(!pos.messaging_supported);
assert_relative_eq!(pos.position.latitude.value(), 49.05833333333333, epsilon = 1e-9);
assert_relative_eq!(pos.position.longitude.value(), -72.02916666666667, epsilon = 1e-9);
assert_eq!(pos.comment, b"");
}
#[test]
fn no_timestamp_with_messaging() {
let pos = AprsPosition::parse(b"=4903.50N/07201.75W-").unwrap();
assert!(pos.timestamp.is_none());
assert!(pos.messaging_supported);
}
#[test]
fn with_timestamp_no_messaging() {
let pos = AprsPosition::parse(b"/074849h4821.61N\\01224.49E^322/103/A=003054").unwrap();
assert_eq!(
pos.timestamp.unwrap(),
Timestamp::Hhmmss(7, 48, 49)
);
assert!(!pos.messaging_supported);
assert_relative_eq!(pos.position.latitude.value(), 48.36016666666667, epsilon = 1e-9);
assert_relative_eq!(pos.position.longitude.value(), 12.408166666666666, epsilon = 1e-9);
assert_eq!(pos.position.symbol.table, '\\');
assert_eq!(pos.position.symbol.code, '^');
assert_eq!(pos.comment, b"322/103/A=003054");
}
#[test]
fn with_timestamp_and_messaging() {
let pos = AprsPosition::parse(b"@074849h4821.61N\\01224.49E^322/103/A=003054").unwrap();
assert!(pos.timestamp.is_some());
assert!(pos.messaging_supported);
}
#[test]
fn with_comment_and_altitude() {
let pos = AprsPosition::parse(b"!4903.50N/07201.75W-Hello/A=001000").unwrap();
assert_eq!(pos.comment, b"Hello/A=001000");
assert!(pos.position.altitude.is_some());
}
#[test]
fn extension_course_speed_parsed() {
let pos = AprsPosition::parse(b"/074849h4821.61N\\01224.49E^322/103/A=003054").unwrap();
assert!(pos.extension.is_some());
assert!(matches!(
pos.extension.unwrap(),
Extension::DirectionSpeed { direction_degrees: 322, speed_knots: 103 }
));
}
#[test]
fn compressed_no_timestamp() {
let pos = AprsPosition::parse(b"!/ABCD#$%^- sT").unwrap();
assert!(pos.timestamp.is_none());
assert_relative_eq!(pos.position.latitude.value(), 25.97004667573229, epsilon = 0.001);
assert_relative_eq!(pos.position.longitude.value(), -171.95429033460567, epsilon = 0.001);
}
#[test]
fn encode_round_trip_uncompressed() {
let raw = b"!4903.50N/07201.75W-";
let pos = AprsPosition::parse(raw).unwrap();
let encoded = pos.encode();
assert_eq!(&encoded, raw);
}
#[test]
fn encode_round_trip_with_timestamp() {
let raw = b"/074849h4821.61N\\01224.49E^322/103/A=003054";
let pos = AprsPosition::parse(raw).unwrap();
let encoded = pos.encode();
assert_eq!(encoded, raw);
}
#[test]
fn encode_round_trip_compressed() {
let raw = b"!/ABCD#$%^- sT";
let pos = AprsPosition::parse(raw).unwrap();
let encoded = pos.encode();
assert_eq!(&encoded, raw);
}
#[test]
fn timestamp_validates_strictly() {
let err = AprsPosition::parse(b"/092460z4903.50N/07201.75W-");
assert!(err.is_err());
}
}