use crate::error::AprsError;
use crate::types::{Extension, Position, Timestamp};
use crate::util::trim_spaces_end;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AprsObject {
pub name: Vec<u8>,
pub live: bool,
pub timestamp: Timestamp,
pub position: Position,
pub extension: Option<Extension>,
pub frequency_mhz: Option<f32>,
pub comment: Vec<u8>,
}
impl AprsObject {
pub(crate) fn parse(info: &[u8]) -> Result<Self, AprsError> {
if info.len() < 18 {
return Err(AprsError::InvalidObject {
detail: "packet too short",
});
}
let mut name = info[1..10].to_vec();
trim_spaces_end(&mut name);
let live = match info[10] {
b'*' => true,
b'_' | b' ' => false, _ => {
return Err(AprsError::InvalidObject {
detail: "invalid liveness byte",
});
}
};
let timestamp = Timestamp::parse(&info[11..18])?;
let position_bytes = info.get(18..).ok_or(AprsError::InvalidObject {
detail: "truncated after timestamp",
})?;
let (remaining, position) = Position::parse(position_bytes)?;
let comment_raw = remaining.unwrap_or_default();
let (extension, comment) = if position.compressed_cs.is_none() {
if let Some(ext) = Extension::parse(comment_raw) {
(Some(ext), comment_raw.get(7..).unwrap_or_default().to_vec())
} else {
(None, comment_raw.to_vec())
}
} else {
(None, comment_raw.to_vec())
};
let frequency_mhz = crate::util::extract_frequency_mhz(&comment);
Ok(Self {
name,
live,
timestamp,
position,
extension,
frequency_mhz,
comment,
})
}
pub fn encode(&self) -> Vec<u8> {
let mut out = vec![b';'];
out.extend_from_slice(&self.name);
out.extend(std::iter::repeat_n(
b' ',
9usize.saturating_sub(self.name.len()),
));
out.push(if self.live { b'*' } else { b'_' });
self.timestamp.encode(&mut out);
if self.extension.is_some() || self.position.compressed_cs.is_none() {
self.position.encode_uncompressed(&mut out);
if let Some(ref ext) = self.extension {
ext.encode(&mut out);
}
} else {
self.position.encode_compressed(&mut out);
}
out.extend_from_slice(&self.comment);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const LIVE_OBJ: &[u8] =
b";HFEST-18H*170403z3443.55N\\08635.47Wh146.940MHz T100 Huntsville Hamfest";
#[test]
fn parse_live_object() {
let o = AprsObject::parse(LIVE_OBJ).unwrap();
assert_eq!(o.name, b"HFEST-18H");
assert!(o.live);
assert_eq!(o.timestamp, Timestamp::Ddhhmm(17, 4, 3));
assert_relative_eq!(
o.position.latitude.value(),
34.725833333333334,
epsilon = 1e-9
);
assert_relative_eq!(
o.position.longitude.value(),
-86.59116666666667,
epsilon = 1e-9
);
assert_eq!(o.position.symbol.table, '\\');
assert_eq!(o.position.symbol.code, 'h');
assert_eq!(o.comment, b"146.940MHz T100 Huntsville Hamfest");
}
#[test]
fn parse_dead_object_space_liveness() {
let o = AprsObject::parse(b";HFEST 170403z3443.55N\\08635.47Wh").unwrap();
assert_eq!(o.name, b"HFEST");
assert!(!o.live);
}
#[test]
fn parse_dead_object_underscore_liveness() {
let o = AprsObject::parse(b";HFEST _170403z3443.55N\\08635.47Wh").unwrap();
assert_eq!(o.name, b"HFEST");
assert!(!o.live);
}
#[test]
fn parse_with_extension() {
let o = AprsObject::parse(b";HFEST _170403z3443.55N\\08635.47WhPHG5132Comment").unwrap();
assert!(o.extension.is_some());
}
#[test]
fn parse_compressed_object() {
let o = AprsObject::parse(b";CAR _092345z/5L!!<*e7>7P[Moving to the north").unwrap();
assert_eq!(o.name, b"CAR");
assert!(!o.live);
assert_relative_eq!(o.position.latitude.value(), 49.5, epsilon = 0.01);
assert_eq!(o.comment, b"Moving to the north");
}
#[test]
fn encode_round_trip_live() {
let o = AprsObject::parse(LIVE_OBJ).unwrap();
assert_eq!(o.encode(), LIVE_OBJ);
}
#[test]
fn timestamp_validates_strictly() {
assert!(AprsObject::parse(b";HFEST-18H*002345z3443.55N\\08635.47Wh").is_err());
}
}