1use crate::error::AprsError;
13
14#[derive(Debug, Clone, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct AprsTelemetry {
26 pub sequence: Vec<u8>,
28 pub analog: [Option<f32>; 5],
30 pub digital: u8,
32 pub comment: Vec<u8>,
33}
34
35impl AprsTelemetry {
36 pub(crate) fn parse(info: &[u8]) -> Result<Self, AprsError> {
38 if info.len() < 2 || info[1] != b'#' {
40 return Err(AprsError::TruncatedPacket {
41 expected: 2,
42 got: info.len(),
43 });
44 }
45 let body = &info[2..]; let parts: Vec<&[u8]> = body.split(|&c| c == b',').collect();
49
50 let sequence = parts.first().unwrap_or(&b"".as_slice()).to_vec();
51
52 let mut analog = [None; 5];
53 for (i, slot) in analog.iter_mut().enumerate() {
54 if let Some(part) = parts.get(i + 1) {
55 *slot = std::str::from_utf8(part)
56 .ok()
57 .and_then(|s| s.trim().parse::<f32>().ok());
58 }
59 }
60
61 let digital = parts
62 .get(6)
63 .and_then(|part| {
64 if part.len() >= 8 && part[..8].iter().all(|&c| c == b'0' || c == b'1') {
65 let mut val = 0u8;
66 for &bit in &part[..8] {
67 val = (val << 1) | (bit - b'0');
68 }
69 Some(val)
70 } else {
71 None
72 }
73 })
74 .unwrap_or(0);
75
76 let comment = if parts.len() > 7 {
77 let mut c = Vec::new();
78 for (i, part) in parts[7..].iter().enumerate() {
79 if i > 0 {
80 c.push(b',');
81 }
82 c.extend_from_slice(part);
83 }
84 c
85 } else {
86 vec![]
87 };
88
89 Ok(Self {
90 sequence,
91 analog,
92 digital,
93 comment,
94 })
95 }
96
97 pub fn encode(&self) -> Vec<u8> {
98 let mut out = b"T#".to_vec();
99 out.extend_from_slice(&self.sequence);
100 for val in &self.analog {
101 out.push(b',');
102 if let Some(v) = val {
103 if *v == v.trunc() && v.is_finite() {
105 out.extend_from_slice(format!("{}", *v as i64).as_bytes());
106 } else {
107 out.extend_from_slice(format!("{}", v).as_bytes());
108 }
109 }
110 }
111 out.push(b',');
112 for i in (0..8).rev() {
113 out.push(b'0' + ((self.digital >> i) & 1));
114 }
115 if !self.comment.is_empty() {
116 out.push(b',');
117 out.extend_from_slice(&self.comment);
118 }
119 out
120 }
121}
122
123#[derive(Debug, Clone, PartialEq)]
127#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
128pub struct TelemetryEquation {
129 pub a: f32,
130 pub b: f32,
131 pub c: f32,
132}
133
134impl TelemetryEquation {
135 pub fn apply(&self, raw: f32) -> f32 {
137 self.a + self.b * raw + self.c * raw * raw
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Default)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
148pub struct TelemetryMetadata {
149 pub param_names: Vec<Option<Vec<u8>>>,
151 pub unit_labels: Vec<Option<Vec<u8>>>,
153 pub equations: Vec<TelemetryEquation>,
155 pub bit_sense: u8,
157 pub project_name: Vec<u8>,
159}
160
161impl TelemetryMetadata {
162 pub fn parse_parm(text: &[u8]) -> Vec<Option<Vec<u8>>> {
166 parse_csv_fields(text, 13)
167 }
168
169 pub fn parse_unit(text: &[u8]) -> Vec<Option<Vec<u8>>> {
171 parse_csv_fields(text, 13)
172 }
173
174 pub fn parse_eqns(text: &[u8]) -> Vec<TelemetryEquation> {
178 let parts: Vec<&[u8]> = text.split(|&c| c == b',').collect();
179 let mut result = Vec::with_capacity(5);
180 for i in 0..5 {
181 let a = parts.get(i * 3).and_then(|p| parse_f32(p)).unwrap_or(0.0);
182 let b = parts
183 .get(i * 3 + 1)
184 .and_then(|p| parse_f32(p))
185 .unwrap_or(1.0);
186 let c = parts
187 .get(i * 3 + 2)
188 .and_then(|p| parse_f32(p))
189 .unwrap_or(0.0);
190 result.push(TelemetryEquation { a, b, c });
191 }
192 result
193 }
194
195 pub fn parse_bits(text: &[u8]) -> (u8, Vec<u8>) {
199 let comma = text.iter().position(|&b| b == b',');
200 let sense_bytes = match comma {
201 Some(pos) => &text[..pos],
202 None => text,
203 };
204 let project = match comma {
205 Some(pos) => text.get(pos + 1..).unwrap_or_default().to_vec(),
206 None => vec![],
207 };
208 let mut sense = 0u8;
209 for (i, &b) in sense_bytes.iter().enumerate().take(8) {
210 if b == b'1' {
211 sense |= 0x80 >> i;
212 }
213 }
214 (sense, project)
215 }
216}
217
218fn parse_csv_fields(text: &[u8], max: usize) -> Vec<Option<Vec<u8>>> {
221 text.split(|&c| c == b',')
222 .take(max)
223 .map(|part| {
224 let trimmed: Vec<u8> = part.iter().copied().skip_while(|&b| b == b' ').collect();
225 if trimmed.is_empty() {
226 None
227 } else {
228 Some(trimmed)
229 }
230 })
231 .collect()
232}
233
234fn parse_f32(b: &[u8]) -> Option<f32> {
235 std::str::from_utf8(b).ok()?.trim().parse().ok()
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn parse_basic_telemetry() {
244 let t = AprsTelemetry::parse(b"T#001,100,200,300,400,500,10101010").unwrap();
245 assert_eq!(t.sequence, b"001");
246 assert_eq!(t.analog[0], Some(100.0));
247 assert_eq!(t.analog[4], Some(500.0));
248 assert_eq!(t.digital, 0b10101010);
249 assert!(t.comment.is_empty());
250 }
251
252 #[test]
253 fn parse_telemetry_with_comment() {
254 let t = AprsTelemetry::parse(b"T#001,100,200,300,400,500,11110000,Hello World").unwrap();
255 assert_eq!(t.digital, 0b11110000);
256 assert_eq!(t.comment, b"Hello World");
257 }
258
259 #[test]
260 fn encode_round_trip() {
261 let raw = b"T#001,100,200,300,400,500,10101010,Test";
262 let t = AprsTelemetry::parse(raw).unwrap();
263 assert_eq!(t.encode().as_slice(), raw.as_slice());
264 }
265
266 #[test]
267 fn parse_parm_names() {
268 let names = TelemetryMetadata::parse_parm(b"Bat1,Bat2,Temp,Hum,Pres,LED1,LED2");
269 assert_eq!(names[0].as_deref(), Some(b"Bat1".as_slice()));
270 assert_eq!(names[4].as_deref(), Some(b"Pres".as_slice()));
271 assert_eq!(names[5].as_deref(), Some(b"LED1".as_slice()));
272 }
273
274 #[test]
275 fn parse_eqns() {
276 let eqns = TelemetryMetadata::parse_eqns(b"0,0.01,0,0,0.01,0,0,1,0,0,1,0,0,1,0");
277 assert_eq!(eqns.len(), 5);
278 assert!((eqns[0].b - 0.01).abs() < 0.001);
279 assert!((eqns[0].c).abs() < 0.001);
280 }
281
282 #[test]
283 fn equation_apply() {
284 let eq = TelemetryEquation {
285 a: 0.0,
286 b: 0.01,
287 c: 0.0,
288 };
289 assert!((eq.apply(100.0) - 1.0).abs() < 0.001);
290 }
291
292 #[test]
293 fn parse_bits() {
294 let (sense, project) = TelemetryMetadata::parse_bits(b"11111111,My Station");
295 assert_eq!(sense, 0xFF);
296 assert_eq!(project, b"My Station");
297 }
298
299 #[test]
300 fn parse_bits_mixed() {
301 let (sense, _) = TelemetryMetadata::parse_bits(b"10110100,Test");
302 assert_eq!(sense, 0b10110100);
303 }
304
305 #[test]
306 fn missing_digital_bits_defaults_to_zero() {
307 let t = AprsTelemetry::parse(b"T#001,100,200,300,400,500").unwrap();
309 assert_eq!(t.digital, 0);
310 }
311}