aprs_parser/
status.rs

1//! A Status Report announces the station's current mission or any other single
2//! line status to everyone. The report starts with the '>' APRS Data Type Identifier.
3//! The report may optionally contain a timestamp.
4//!
5//! Examples:
6//! - ">12.6V 0.2A 22degC"              (report without timestamp)
7//! - ">120503hFatal error"             (report with timestamp in HMS format)
8//! - ">281205zSystem will shutdown"    (report with timestamp in DHM format)
9
10use std::convert::TryFrom;
11use std::io::Write;
12
13use Callsign;
14use DecodeError;
15use DhmTimestamp;
16use EncodeError;
17use Timestamp;
18
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct AprsStatus {
21    pub to: Callsign,
22
23    timestamp: Option<Timestamp>,
24    comment: Vec<u8>,
25}
26
27impl AprsStatus {
28    pub fn new(to: Callsign, timestamp: Option<DhmTimestamp>, comment: Vec<u8>) -> Self {
29        let timestamp = timestamp.map(|t| t.into());
30        Self {
31            to,
32            timestamp,
33            comment,
34        }
35    }
36
37    /// According to APRS spec, an AprsStatus should only allow the DDHHMM timestamp. (See page 80 of APRS101.PDF)
38    /// In practice, many encoders don't adhere to this.
39    /// Use this function to create an AprsStatus with any timestamp type
40    pub fn new_noncompliant(to: Callsign, timestamp: Option<Timestamp>, comment: Vec<u8>) -> Self {
41        Self {
42            to,
43            timestamp,
44            comment,
45        }
46    }
47
48    pub fn is_timestamp_compliant(&self) -> bool {
49        self.timestamp
50            .as_ref()
51            .map(|t| matches!(t, Timestamp::DDHHMM(_, _, _)))
52            .unwrap_or(true)
53    }
54
55    pub fn timestamp(&self) -> Option<&Timestamp> {
56        self.timestamp.as_ref()
57    }
58
59    pub fn comment(&self) -> &[u8] {
60        &self.comment
61    }
62
63    pub fn decode(b: &[u8], to: Callsign) -> Result<Self, DecodeError> {
64        // Interpret the first 7 bytes as a timestamp, if valid.
65        // Otherwise the whole field is the comment.
66        let timestamp = b.get(..7).and_then(|b| Timestamp::try_from(b).ok());
67        let comment = if timestamp.is_some() { &b[7..] } else { b };
68
69        Ok(AprsStatus {
70            to,
71            timestamp,
72            comment: comment.to_owned(),
73        })
74    }
75
76    pub fn encode<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
77        write!(buf, ">")?;
78
79        if let Some(ts) = &self.timestamp {
80            ts.encode(buf)?;
81        }
82
83        buf.write_all(&self.comment)?;
84
85        Ok(())
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn default_callsign() -> Callsign {
94        Callsign::new_no_ssid("VE9")
95    }
96
97    #[test]
98    fn parse_without_timestamp_or_comment() {
99        let result = AprsStatus::decode(&b""[..], default_callsign()).unwrap();
100
101        assert_eq!(result.to, default_callsign());
102        assert_eq!(result.timestamp, None);
103        assert_eq!(result.comment, []);
104    }
105
106    #[test]
107    fn parse_with_timestamp_without_comment() {
108        let result = AprsStatus::decode(r"312359z".as_bytes(), default_callsign()).unwrap();
109
110        assert_eq!(result.to, default_callsign());
111        assert_eq!(result.timestamp, Some(Timestamp::DDHHMM(31, 23, 59)));
112        assert_eq!(result.comment, b"");
113    }
114
115    #[test]
116    fn parse_without_timestamp_with_comment() {
117        let result = AprsStatus::decode(&b"Hi there!"[..], default_callsign()).unwrap();
118
119        assert_eq!(result.to, default_callsign());
120        assert_eq!(result.timestamp, None);
121        assert_eq!(result.comment, b"Hi there!");
122    }
123
124    #[test]
125    fn parse_with_timestamp_and_comment() {
126        let result =
127            AprsStatus::decode(r"235959hHi there!".as_bytes(), default_callsign()).unwrap();
128
129        assert_eq!(result.to, default_callsign());
130        assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(23, 59, 59)));
131        assert_eq!(result.comment, b"Hi there!");
132    }
133
134    #[test]
135    fn compliant_time_is_compliant() {
136        let result = AprsStatus::decode(r"312359z".as_bytes(), default_callsign()).unwrap();
137
138        assert_eq!(result.to, default_callsign());
139        assert_eq!(result.timestamp, Some(Timestamp::DDHHMM(31, 23, 59)));
140        assert!(result.is_timestamp_compliant());
141    }
142
143    #[test]
144    fn uncompliant_time_is_not_compliant() {
145        let result =
146            AprsStatus::decode(r"235959hHi there!".as_bytes(), default_callsign()).unwrap();
147
148        assert_eq!(result.to, default_callsign());
149        assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(23, 59, 59)));
150        assert!(!result.is_timestamp_compliant());
151    }
152
153    #[test]
154    fn missing_time_is_compliant() {
155        let result = AprsStatus::decode(&b"Hi there!"[..], default_callsign()).unwrap();
156
157        assert_eq!(result.to, default_callsign());
158        assert_eq!(result.timestamp, None);
159        assert!(result.is_timestamp_compliant());
160    }
161}