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