1use crate::callsign::Callsign;
2use crate::error::AprsError;
3use crate::types::lonlat::{Latitude, Longitude, Precision};
4
5#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub enum MicEMessage {
14 M0, M1, M2, M3, M4, M5, M6,
15 C0, C1, C2, C3, C4, C5, C6,
16 Emergency,
17 Unknown,
18}
19
20impl MicEMessage {
21 fn from_bits(a: MsgBit, b: MsgBit, c: MsgBit) -> Self {
22 use MsgBit::{Custom, Standard, Zero};
23 use MicEMessage::*;
24 match (a, b, c) {
25 (Standard, Standard, Standard) => M0,
26 (Custom, Custom, Custom) => C0,
27 (Standard, Standard, Zero) => M1,
28 (Custom, Custom, Zero) => C1,
29 (Standard, Zero, Standard) => M2,
30 (Custom, Zero, Custom) => C2,
31 (Standard, Zero, Zero) => M3,
32 (Custom, Zero, Zero) => C3,
33 (Zero, Standard, Standard) => M4,
34 (Zero, Custom, Custom) => C4,
35 (Zero, Standard, Zero) => M5,
36 (Zero, Custom, Zero) => C5,
37 (Zero, Zero, Standard) => M6,
38 (Zero, Zero, Custom) => C6,
39 (Zero, Zero, Zero) => Emergency,
40 _ => Unknown,
41 }
42 }
43
44 fn to_bits(self) -> (MsgBit, MsgBit, MsgBit) {
45 use MsgBit::{Custom, Standard, Zero};
46 use MicEMessage::*;
47 match self {
48 M0 => (Standard, Standard, Standard),
49 C0 => (Custom, Custom, Custom),
50 M1 => (Standard, Standard, Zero),
51 C1 => (Custom, Custom, Zero),
52 M2 => (Standard, Zero, Standard),
53 C2 => (Custom, Zero, Custom),
54 M3 => (Standard, Zero, Zero),
55 C3 => (Custom, Zero, Zero),
56 M4 => (Zero, Standard, Standard),
57 C4 => (Zero, Custom, Custom),
58 M5 => (Zero, Standard, Zero),
59 C5 => (Zero, Custom, Zero),
60 M6 => (Zero, Zero, Standard),
61 C6 => (Zero, Zero, Custom),
62 Emergency => (Zero, Zero, Zero),
63 Unknown => (Standard, Custom, Standard), }
65 }
66}
67
68#[derive(Copy, Clone)]
69enum MsgBit { Zero, Custom, Standard }
70
71impl MsgBit {
72 fn from_byte(c: u8) -> Option<Self> {
73 match c {
74 b'0'..=b'9' | b'L' => Some(MsgBit::Zero),
75 b'A'..=b'K' => Some(MsgBit::Custom),
76 b'P'..=b'Z' => Some(MsgBit::Standard),
77 _ => None,
78 }
79 }
80}
81
82#[derive(Debug, Copy, Clone, PartialEq, Eq)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87#[cfg_attr(feature = "serde", serde(transparent))]
88pub struct MicESpeed(pub u32);
89
90impl MicESpeed {
91 pub fn knots(self) -> u32 { self.0 }
92}
93
94#[derive(Debug, Copy, Clone, PartialEq, Eq)]
96#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
97#[cfg_attr(feature = "serde", serde(transparent))]
98pub struct MicECourse(pub u32);
99
100impl MicECourse {
101 pub const UNKNOWN: Self = Self(0);
102 pub fn degrees(self) -> u32 { self.0 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
110#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
111pub struct MicEDevice {
112 pub manufacturer: String,
113 pub model: String,
114}
115
116static MICE_PREFIX_TABLE: &[(&[u8], &str, &str)] = &[
122 (b">=", "Kenwood", "TH-D72"),
124 (b">^", "Kenwood", "TH-D74"),
125 (b">", "Kenwood", "TH-D7A"),
126 (b"]=", "Kenwood", "TM-D710"),
127 (b"]", "Kenwood", "TM-D700"),
128 (b"_ ", "Yaesu", "VX-8"),
130 (b"_\"","Yaesu", "FTM-350"),
131 (b"_#", "Yaesu", "VX-8G"),
132 (b"_$", "Yaesu", "FT1D"),
133 (b"_%", "Yaesu", "FTM-400DR"),
134 (b"_)", "Yaesu", "FTM-100D"),
135 (b"_3", "Yaesu", "FT5D"),
136 (b"_8", "Yaesu", "FT3D"),
137 ];
140
141fn lookup_device(prefix: &[u8]) -> Option<MicEDevice> {
142 for &(pat, mfr, model) in MICE_PREFIX_TABLE {
143 if prefix.starts_with(pat) {
144 return Some(MicEDevice { manufacturer: mfr.to_string(), model: model.to_string() });
145 }
146 }
147 None
148}
149
150fn lookup_byonics_suffix(comment: &[u8]) -> Option<(MicEDevice, &[u8])> {
151 if comment.ends_with(b"|3") {
152 return Some((MicEDevice { manufacturer: "Byonics".to_string(), model: "TinyTrak3".to_string() }, &comment[..comment.len()-2]));
153 }
154 if comment.ends_with(b"|4") {
155 return Some((MicEDevice { manufacturer: "Byonics".to_string(), model: "TinyTrak4".to_string() }, &comment[..comment.len()-2]));
156 }
157 None
158}
159
160#[derive(Debug, Clone, PartialEq)]
169#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
170pub struct AprsMicE {
171 pub latitude: Latitude,
172 pub longitude: Longitude,
173 pub precision: Precision,
174 pub message: MicEMessage,
175 pub speed: MicESpeed,
176 pub course: MicECourse,
177 pub symbol_code: char,
178 pub symbol_table: char,
179 pub comment: Vec<u8>,
181 pub is_current: bool,
183 pub altitude_m: Option<f64>,
185 pub device: Option<MicEDevice>,
187 pub raw_mfg: Option<Vec<u8>>,
189}
190
191impl AprsMicE {
192 pub(crate) fn parse(info: &[u8], to: &Callsign) -> Result<Self, AprsError> {
195 let dti = *info.first().ok_or(AprsError::EmptyPacket)?;
196 let is_current = matches!(dti, b'`' | 0x1D);
197
198 let (latitude, precision, message, lon_offset_100, lon_east) =
200 decode_dest(to).ok_or_else(|| AprsError::InvalidMicEDestination { raw: to.as_str().as_bytes().to_vec() })?;
201
202 let b = info.get(1..).ok_or(AprsError::MicETooShort { len: info.len() })?;
205 if b.len() < 8 {
206 return Err(AprsError::MicETooShort { len: info.len() });
207 }
208
209 let longitude = decode_longitude(&b[0..3], lon_offset_100, lon_east)
210 .ok_or(AprsError::MicETooShort { len: info.len() })?;
211 let (speed, course) = decode_speed_course(&b[3..6])
212 .ok_or(AprsError::MicETooShort { len: info.len() })?;
213
214 let symbol_code = b[6] as char;
215 let symbol_table = b[7] as char;
216
217 let rest = b.get(8..).unwrap_or_default();
219
220 let (raw_mfg, altitude_m, comment_raw) = parse_rest(rest);
222 let device = raw_mfg.as_deref().and_then(lookup_device);
223
224 let (device, comment) = if device.is_none() {
226 if let Some((dev, trimmed)) = lookup_byonics_suffix(comment_raw) {
227 (Some(dev), trimmed.to_vec())
228 } else {
229 (device, comment_raw.to_vec())
230 }
231 } else {
232 (device, comment_raw.to_vec())
233 };
234
235 Ok(Self {
236 latitude,
237 longitude,
238 precision,
239 message,
240 speed,
241 course,
242 symbol_code,
243 symbol_table,
244 comment,
245 is_current,
246 altitude_m,
247 device,
248 raw_mfg,
249 })
250 }
251
252 pub fn encode(&self) -> Vec<u8> {
254 let mut out = Vec::new();
255 out.push(if self.is_current { b'`' } else { b'\'' });
256 encode_longitude(self.longitude, &mut out);
257 encode_speed_course(self.speed, self.course, &mut out);
258 out.push(self.symbol_code as u8);
259 out.push(self.symbol_table as u8);
260 if let Some(ref mfg) = self.raw_mfg {
261 out.extend_from_slice(mfg);
262 }
263 if let Some(alt_m) = self.altitude_m {
264 encode_altitude(alt_m, &mut out);
265 }
266 out.extend_from_slice(&self.comment);
267 out
268 }
269
270 pub fn encode_destination(&self) -> Result<Callsign, AprsError> {
272 let mut lat_buf = Vec::new();
273 self.latitude.encode_uncompressed(&mut lat_buf, self.precision);
274 if lat_buf.len() != 8 {
275 return Err(AprsError::EncodeError { detail: "MIC-E latitude encode failed" });
276 }
277 let is_north = self.latitude.value() >= 0.0;
278 let (lon_deg, _, _, is_east) = self.longitude.dmh();
279 let lon_offset_100 = lon_deg == 0 || lon_deg >= 100;
280 let (a, b, c) = self.message.to_bits();
281
282 let bytes = [
283 encode_dest_012(lat_buf[0], a),
284 encode_dest_012(lat_buf[1], b),
285 encode_dest_012(lat_buf[2], c),
286 encode_dest_bit3(lat_buf[3], is_north),
287 encode_dest_bit4(lat_buf[5], lon_offset_100),
288 encode_dest_bit5(lat_buf[6], !is_east),
289 ];
290
291 let call_str = std::str::from_utf8(&bytes)
292 .map_err(|_| AprsError::EncodeError { detail: "MIC-E destination is not ASCII" })?;
293 Callsign::decode_textual(call_str.as_bytes())
294 .map_err(|_| AprsError::EncodeError { detail: "MIC-E destination invalid callsign" })
295 }
296}
297
298fn decode_dest(c: &Callsign) -> Option<(Latitude, Precision, MicEMessage, bool, bool)> {
301 let data = c.as_str().as_bytes();
302 if data.len() != 6 { return None; }
303
304 let lat_bytes = [
305 lat_digit(data[0])?,
306 lat_digit(data[1])?,
307 lat_digit(data[2])?,
308 lat_digit(data[3])?,
309 b'.',
310 lat_digit(data[4])?,
311 lat_digit(data[5])?,
312 lat_dir_byte(data[3])?,
313 ];
314 let (lat, prec) = Latitude::parse_uncompressed(&lat_bytes).ok()?;
315
316 let a = MsgBit::from_byte(data[0])?;
317 let b = MsgBit::from_byte(data[1])?;
318 let c = MsgBit::from_byte(data[2])?;
319 let msg = MicEMessage::from_bits(a, b, c);
320
321 let lon_offset_100 = matches!(data[4], b'P'..=b'Z');
323 let lon_east = matches!(data[5], b'0'..=b'9' | b'L');
325
326 Some((lat, prec, msg, lon_offset_100, lon_east))
327}
328
329fn lat_digit(c: u8) -> Option<u8> {
330 match c {
331 b'0'..=b'9' => Some(c),
332 b'A'..=b'J' => Some(c - 17),
333 b'K' | b'L' | b'Z' => Some(b' '),
334 b'P'..=b'Y' => Some(c - 32),
335 _ => None,
336 }
337}
338
339fn lat_dir_byte(c: u8) -> Option<u8> {
340 match c {
341 b'0'..=b'9' | b'L' => Some(b'S'),
342 b'P'..=b'Z' => Some(b'N'),
343 _ => None,
344 }
345}
346
347fn decode_longitude(b: &[u8], offset_100: bool, is_east: bool) -> Option<Longitude> {
350 let mut d = b[0].checked_sub(28)?;
351 if offset_100 { d = d.checked_add(100)?; }
352 if (180..=189).contains(&d) { d -= 80; }
353 else if (190..=199).contains(&d) { d -= 190; }
354
355 let mut m = b[1].checked_sub(28)?;
356 if m >= 60 { m -= 60; }
357
358 let h = b[2].checked_sub(28)?;
359
360 Longitude::new(
361 f64::from(d) + f64::from(m) / 60.0 + f64::from(h) / 6000.0
362 ).map(|lon| if is_east { lon } else {
363 Longitude::new(-lon.value()).unwrap_or(lon)
364 })
365}
366
367fn encode_longitude(lon: Longitude, out: &mut Vec<u8>) {
368 let (d, m, h, is_east) = lon.dmh();
369 let d = d as u8;
370 let m = m as u8;
371 let h = h as u8;
372 let enc_d = match d {
373 0..=9 => d + 118, 10..=99 => d + 28,
375 100..=109 => d - 72, _ => d - 72,
377 };
378 let _ = is_east; out.push(enc_d);
383 out.push(if m < 10 { m + 88 } else { m + 28 });
384 out.push(h + 28);
385}
386
387fn decode_speed_course(b: &[u8]) -> Option<(MicESpeed, MicECourse)> {
390 let sp = u32::from(b[0].checked_sub(28)?);
391 let dc = u32::from(b[1].checked_sub(28)?);
392 let se = u32::from(b[2].checked_sub(28)?);
393
394 let mut speed = sp * 10 + dc / 10;
395 if speed >= 800 { speed -= 800; }
396
397 let mut course = (dc % 10) * 100 + se;
398 if course >= 400 { course -= 400; }
399
400 Some((MicESpeed(speed), MicECourse(course)))
401}
402
403fn encode_speed_course(speed: MicESpeed, course: MicECourse, out: &mut Vec<u8>) {
404 let knots = speed.knots();
405 let deg = course.degrees();
406 let tens = (knots / 10) as u8;
407 let units = (knots % 10) as u8;
408 let h_course = (deg / 100) as u8;
409 let u_course = (deg % 100) as u8;
410
411 let sp = if tens < 20 { tens + 80 } else { tens };
412 let dc = units * 10 + h_course + 4;
413 out.push(sp + 28);
414 out.push(dc + 28);
415 out.push(u_course + 28);
416}
417
418fn parse_rest(rest: &[u8]) -> (Option<Vec<u8>>, Option<f64>, &[u8]) {
421 if let Some(idx) = rest.iter().position(|&b| b == b'}')
423 && idx >= 3
424 {
425 let mfg_bytes = &rest[..idx - 3];
427 let alt_bytes = &rest[idx - 3..idx];
428 let mut alt_val: i32 = 0;
429 for &byte in alt_bytes {
430 alt_val = alt_val * 91 + (byte.saturating_sub(33)) as i32;
431 }
432 let alt_m_corrected = alt_val as f64 - 10000.0;
434 let raw_mfg = if mfg_bytes.is_empty() { None } else { Some(mfg_bytes.to_vec()) };
435 let comment = rest.get(idx + 1..).unwrap_or_default();
436 return (raw_mfg, Some(alt_m_corrected), comment);
437 }
438 (None, None, rest)
442}
443
444fn encode_altitude(alt_m: f64, out: &mut Vec<u8>) {
445 let val = (alt_m + 10000.0).round() as u32;
446 let b0 = (val / 91 / 91 % 91) as u8 + 33;
447 let b1 = (val / 91 % 91) as u8 + 33;
448 let b2 = (val % 91) as u8 + 33;
449 out.push(b0);
450 out.push(b1);
451 out.push(b2);
452 out.push(b'}');
453}
454
455fn encode_dest_012(lat_digit: u8, bit: MsgBit) -> u8 {
458 match (bit, lat_digit == b' ') {
459 (MsgBit::Zero, false) => lat_digit,
460 (MsgBit::Zero, true) => b'L',
461 (MsgBit::Custom, false) => lat_digit + 17,
462 (MsgBit::Custom, true) => b'K',
463 (MsgBit::Standard, false) => lat_digit + 32,
464 (MsgBit::Standard, true) => b'Z',
465 }
466}
467
468fn encode_dest_bit3(lat_digit: u8, is_north: bool) -> u8 {
469 match (is_north, lat_digit == b' ') {
470 (true, false) => lat_digit + 32,
471 (true, true) => b'Z',
472 (false, false) => lat_digit,
473 (false, true) => b'L',
474 }
475}
476
477fn encode_dest_bit4(lat_digit: u8, lon_offset_100: bool) -> u8 {
478 match (lon_offset_100, lat_digit == b' ') {
479 (true, false) => lat_digit + 32,
480 (true, true) => b'Z',
481 (false, false) => lat_digit,
482 (false, true) => b'L',
483 }
484}
485
486fn encode_dest_bit5(lat_digit: u8, is_west: bool) -> u8 {
487 match (is_west, lat_digit == b' ') {
488 (true, false) => lat_digit + 32,
489 (true, true) => b'Z',
490 (false, false) => lat_digit,
491 (false, true) => b'L',
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 fn callsign(s: &str) -> Callsign {
500 Callsign::decode_textual(s.as_bytes()).unwrap()
501 }
502
503 #[test]
504 fn decode_speed_course_basic() {
505 let b = &[b'n', b'"', b'O'];
508 let (spd, crs) = decode_speed_course(b).unwrap();
509 assert_eq!(spd.knots(), 20);
510 assert_eq!(crs.degrees(), 251);
511 }
512
513 #[test]
514 fn device_lookup_kenwood_thd7a() {
515 let dev = lookup_device(b">").unwrap();
516 assert_eq!(dev.manufacturer, "Kenwood");
517 assert_eq!(dev.model, "TH-D7A");
518 }
519
520 #[test]
521 fn device_lookup_kenwood_thd72() {
522 let dev = lookup_device(b">=").unwrap();
523 assert_eq!(dev.manufacturer, "Kenwood");
524 assert_eq!(dev.model, "TH-D72");
525 }
526
527 #[test]
528 fn device_lookup_kenwood_tmd700() {
529 let dev = lookup_device(b"]").unwrap();
530 assert_eq!(dev.manufacturer, "Kenwood");
531 assert_eq!(dev.model, "TM-D700");
532 }
533
534 #[test]
535 fn device_lookup_yaesu_vx8() {
536 let dev = lookup_device(b"_ ").unwrap();
537 assert_eq!(dev.manufacturer, "Yaesu");
538 assert_eq!(dev.model, "VX-8");
539 }
540
541 #[test]
542 fn byonics_suffix_detection() {
543 let (dev, rest) = lookup_byonics_suffix(b"Hello world!|3").unwrap();
544 assert_eq!(dev.manufacturer, "Byonics");
545 assert_eq!(dev.model, "TinyTrak3");
546 assert_eq!(rest, b"Hello world!");
547 }
548
549 #[test]
550 fn decode_from_spec_example() {
551 let info = br#"`(_fn"Oj/Hello world!"#;
553 let to = callsign("PPPPPP");
554 let m = AprsMicE::parse(info, &to).unwrap();
555 assert!(m.is_current);
556 assert_eq!(m.symbol_code, 'j');
557 assert_eq!(m.symbol_table, '/');
558 assert_eq!(m.comment, b"Hello world!");
559 assert!(m.device.is_none());
560 }
561
562 #[test]
563 fn encode_destination_round_trip() {
564 let info = br#"`(_fn"Oj/Hello world!"#;
565 let to = callsign("PPPPPP");
566 let m = AprsMicE::parse(info, &to).unwrap();
567 let reenc_dest = m.encode_destination().unwrap();
568 assert_eq!(reenc_dest.as_str(), "PPPPPP");
569 }
570
571 #[test]
572 fn decode_kenwood_device() {
573 let info = br#"`(_fn"Oj/>`"49}Hello"#; let to = callsign("S32U6T");
576 let m = AprsMicE::parse(info, &to).unwrap();
577 let _ = m; }
581}