dstar_gateway_core/dprs/
encoder.rs1use std::fmt::Write as _;
4
5use super::crc::compute_crc;
6use super::error::DprsError;
7use super::parser::DprsReport;
8
9pub fn encode_dprs(report: &DprsReport, out: &mut String) -> Result<(), DprsError> {
31 let mut body = String::new();
36
37 let cs_bytes = report.callsign.as_bytes();
41 for &b in cs_bytes {
42 body.push(char::from(b));
43 }
44
45 body.push_str("*>APDPRS,DSTAR*:!");
46
47 let lat = report.latitude.degrees();
49 let lat_hemi = if lat < 0.0 { 'S' } else { 'N' };
50 let lat_abs = lat.abs();
51 let lat_int = lat_abs.trunc();
52 let lat_min = (lat_abs - lat_int) * 60.0;
53 write!(body, "{lat_int:02.0}{lat_min:05.2}{lat_hemi}")
57 .map_err(|_| DprsError::MalformedCoordinates)?;
58
59 body.push('/');
60
61 let lon = report.longitude.degrees();
63 let lon_hemi = if lon < 0.0 { 'W' } else { 'E' };
64 let lon_abs = lon.abs();
65 let lon_int = lon_abs.trunc();
66 let lon_min = (lon_abs - lon_int) * 60.0;
67 write!(body, "{lon_int:03.0}{lon_min:05.2}{lon_hemi}")
68 .map_err(|_| DprsError::MalformedCoordinates)?;
69
70 body.push('#');
72 body.push(report.symbol);
73
74 if let Some(comment) = &report.comment {
75 body.push_str(comment);
76 }
77
78 let crc = compute_crc(body.as_bytes());
80 out.clear();
81 write!(out, "$$CRC{crc:04X},").map_err(|_| DprsError::MalformedCoordinates)?;
82 out.push_str(&body);
83
84 Ok(())
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::dprs::coordinates::{Latitude, Longitude};
91 use crate::types::Callsign;
92
93 type TestResult = Result<(), Box<dyn std::error::Error>>;
94
95 #[test]
96 fn encode_round_trips_through_parser() -> TestResult {
97 let original = DprsReport {
98 callsign: Callsign::from_wire_bytes(*b"W1AW "),
99 latitude: Latitude::try_new(35.5)?,
100 longitude: Longitude::try_new(-82.55)?,
101 symbol: '/',
102 comment: Some("Asheville test".to_string()),
103 };
104 let mut encoded = String::new();
105 encode_dprs(&original, &mut encoded)?;
106 assert!(encoded.starts_with("$$CRC"));
109 assert_eq!(
110 encoded.as_bytes().get(9),
111 Some(&b','),
112 "comma after 4 hex digits"
113 );
114 for b in encoded.as_bytes().get(5..9).unwrap_or(&[]) {
116 assert!(b.is_ascii_hexdigit(), "CRC byte {b:02X} is not hex");
117 }
118 assert!(encoded.contains("W1AW"));
119 assert!(encoded.contains("3530.00N"));
120 assert!(encoded.contains("08233.00W"));
121 assert!(encoded.contains("Asheville test"));
122 Ok(())
123 }
124
125 #[test]
126 fn encode_crc_matches_compute_crc_over_body() -> TestResult {
127 use super::super::crc::compute_crc;
128 let original = DprsReport {
129 callsign: Callsign::from_wire_bytes(*b"W1AW "),
130 latitude: Latitude::try_new(35.5)?,
131 longitude: Longitude::try_new(-82.55)?,
132 symbol: '/',
133 comment: Some("Asheville test".to_string()),
134 };
135 let mut encoded = String::new();
136 encode_dprs(&original, &mut encoded)?;
137 let body = encoded.get(10..).ok_or("body after prefix")?;
139 let expected_crc = compute_crc(body.as_bytes());
140 let crc_str = encoded.get(5..9).ok_or("crc field")?;
141 let actual_crc = u16::from_str_radix(crc_str, 16)?;
142 assert_eq!(actual_crc, expected_crc);
143 Ok(())
144 }
145
146 #[test]
147 fn encode_then_parse_roundtrip() -> TestResult {
148 let original = DprsReport {
149 callsign: Callsign::from_wire_bytes(*b"W1AW "),
150 latitude: Latitude::try_new(35.5)?,
151 longitude: Longitude::try_new(-82.55)?,
152 symbol: '/',
153 comment: None,
154 };
155 let mut encoded = String::new();
156 encode_dprs(&original, &mut encoded)?;
157 let parsed = super::super::parser::parse_dprs(&encoded)?;
158 assert_eq!(parsed.callsign, original.callsign);
159 assert!((parsed.latitude.degrees() - original.latitude.degrees()).abs() < 0.001);
160 assert!((parsed.longitude.degrees() - original.longitude.degrees()).abs() < 0.001);
161 Ok(())
162 }
163}