use crate::error::AprsError;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AprsTelemetry {
pub sequence: Vec<u8>,
pub analog: [Option<f32>; 5],
pub digital: u8,
pub comment: Vec<u8>,
}
impl AprsTelemetry {
pub(crate) fn parse(info: &[u8]) -> Result<Self, AprsError> {
if info.len() < 2 || info[1] != b'#' {
return Err(AprsError::TruncatedPacket { expected: 2, got: info.len() });
}
let body = &info[2..];
let parts: Vec<&[u8]> = body.split(|&c| c == b',').collect();
let sequence = parts.first().unwrap_or(&b"".as_slice()).to_vec();
let mut analog = [None; 5];
for (i, slot) in analog.iter_mut().enumerate() {
if let Some(part) = parts.get(i + 1) {
*slot = std::str::from_utf8(part).ok()
.and_then(|s| s.trim().parse::<f32>().ok());
}
}
let digital = parts.get(6).and_then(|part| {
if part.len() >= 8 && part[..8].iter().all(|&c| c == b'0' || c == b'1') {
let mut val = 0u8;
for &bit in &part[..8] {
val = (val << 1) | (bit - b'0');
}
Some(val)
} else {
None
}
}).unwrap_or(0);
let comment = if parts.len() > 7 {
let mut c = Vec::new();
for (i, part) in parts[7..].iter().enumerate() {
if i > 0 { c.push(b','); }
c.extend_from_slice(part);
}
c
} else {
vec![]
};
Ok(Self { sequence, analog, digital, comment })
}
pub fn encode(&self) -> Vec<u8> {
let mut out = b"T#".to_vec();
out.extend_from_slice(&self.sequence);
for val in &self.analog {
out.push(b',');
if let Some(v) = val {
if *v == v.trunc() && v.is_finite() {
out.extend_from_slice(format!("{}", *v as i64).as_bytes());
} else {
out.extend_from_slice(format!("{}", v).as_bytes());
}
}
}
out.push(b',');
for i in (0..8).rev() {
out.push(b'0' + ((self.digital >> i) & 1));
}
if !self.comment.is_empty() {
out.push(b',');
out.extend_from_slice(&self.comment);
}
out
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TelemetryEquation {
pub a: f32,
pub b: f32,
pub c: f32,
}
impl TelemetryEquation {
pub fn apply(&self, raw: f32) -> f32 {
self.a + self.b * raw + self.c * raw * raw
}
}
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TelemetryMetadata {
pub param_names: Vec<Option<Vec<u8>>>,
pub unit_labels: Vec<Option<Vec<u8>>>,
pub equations: Vec<TelemetryEquation>,
pub bit_sense: u8,
pub project_name: Vec<u8>,
}
impl TelemetryMetadata {
pub fn parse_parm(text: &[u8]) -> Vec<Option<Vec<u8>>> {
parse_csv_fields(text, 13)
}
pub fn parse_unit(text: &[u8]) -> Vec<Option<Vec<u8>>> {
parse_csv_fields(text, 13)
}
pub fn parse_eqns(text: &[u8]) -> Vec<TelemetryEquation> {
let parts: Vec<&[u8]> = text.split(|&c| c == b',').collect();
let mut result = Vec::with_capacity(5);
for i in 0..5 {
let a = parts.get(i * 3).and_then(|p| parse_f32(p)).unwrap_or(0.0);
let b = parts.get(i * 3 + 1).and_then(|p| parse_f32(p)).unwrap_or(1.0);
let c = parts.get(i * 3 + 2).and_then(|p| parse_f32(p)).unwrap_or(0.0);
result.push(TelemetryEquation { a, b, c });
}
result
}
pub fn parse_bits(text: &[u8]) -> (u8, Vec<u8>) {
let comma = text.iter().position(|&b| b == b',');
let sense_bytes = match comma {
Some(pos) => &text[..pos],
None => text,
};
let project = match comma {
Some(pos) => text.get(pos + 1..).unwrap_or_default().to_vec(),
None => vec![],
};
let mut sense = 0u8;
for (i, &b) in sense_bytes.iter().enumerate().take(8) {
if b == b'1' {
sense |= 0x80 >> i;
}
}
(sense, project)
}
}
fn parse_csv_fields(text: &[u8], max: usize) -> Vec<Option<Vec<u8>>> {
text.split(|&c| c == b',')
.take(max)
.map(|part| {
let trimmed: Vec<u8> = part.iter().copied()
.skip_while(|&b| b == b' ')
.collect();
if trimmed.is_empty() { None } else { Some(trimmed) }
})
.collect()
}
fn parse_f32(b: &[u8]) -> Option<f32> {
std::str::from_utf8(b).ok()?.trim().parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_basic_telemetry() {
let t = AprsTelemetry::parse(b"T#001,100,200,300,400,500,10101010").unwrap();
assert_eq!(t.sequence, b"001");
assert_eq!(t.analog[0], Some(100.0));
assert_eq!(t.analog[4], Some(500.0));
assert_eq!(t.digital, 0b10101010);
assert!(t.comment.is_empty());
}
#[test]
fn parse_telemetry_with_comment() {
let t = AprsTelemetry::parse(b"T#001,100,200,300,400,500,11110000,Hello World").unwrap();
assert_eq!(t.digital, 0b11110000);
assert_eq!(t.comment, b"Hello World");
}
#[test]
fn encode_round_trip() {
let raw = b"T#001,100,200,300,400,500,10101010,Test";
let t = AprsTelemetry::parse(raw).unwrap();
assert_eq!(t.encode().as_slice(), raw.as_slice());
}
#[test]
fn parse_parm_names() {
let names = TelemetryMetadata::parse_parm(b"Bat1,Bat2,Temp,Hum,Pres,LED1,LED2");
assert_eq!(names[0].as_deref(), Some(b"Bat1".as_slice()));
assert_eq!(names[4].as_deref(), Some(b"Pres".as_slice()));
assert_eq!(names[5].as_deref(), Some(b"LED1".as_slice()));
}
#[test]
fn parse_eqns() {
let eqns = TelemetryMetadata::parse_eqns(b"0,0.01,0,0,0.01,0,0,1,0,0,1,0,0,1,0");
assert_eq!(eqns.len(), 5);
assert!((eqns[0].b - 0.01).abs() < 0.001);
assert!((eqns[0].c).abs() < 0.001);
}
#[test]
fn equation_apply() {
let eq = TelemetryEquation { a: 0.0, b: 0.01, c: 0.0 };
assert!((eq.apply(100.0) - 1.0).abs() < 0.001);
}
#[test]
fn parse_bits() {
let (sense, project) = TelemetryMetadata::parse_bits(b"11111111,My Station");
assert_eq!(sense, 0xFF);
assert_eq!(project, b"My Station");
}
#[test]
fn parse_bits_mixed() {
let (sense, _) = TelemetryMetadata::parse_bits(b"10110100,Test");
assert_eq!(sense, 0b10110100);
}
#[test]
fn missing_digital_bits_defaults_to_zero() {
let t = AprsTelemetry::parse(b"T#001,100,200,300,400,500").unwrap();
assert_eq!(t.digital, 0);
}
}