1use crate::error::AprsError;
9use crate::packet::{AprsData, PositionAmbiguity, parse_aprs_extensions};
10use crate::position::AprsPosition;
11use crate::weather::extract_position_weather;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum MiceMessage {
21 OffDuty,
23 EnRoute,
25 InService,
27 Returning,
29 Committed,
31 Special,
33 Priority,
35 Emergency,
37}
38
39pub fn parse_mice_position(destination: &str, info: &[u8]) -> Result<AprsPosition, AprsError> {
58 let header = info.get(..9).ok_or(AprsError::InvalidFormat)?;
59 let dest = destination.as_bytes();
60 let dest_head = dest.get(..6).ok_or(AprsError::InvalidFormat)?;
61
62 let data_type = *header.first().ok_or(AprsError::InvalidFormat)?;
63 if data_type != b'`' && data_type != b'\'' && data_type != 0x1C && data_type != 0x1D {
64 return Err(AprsError::InvalidFormat);
65 }
66
67 let lon_bytes = header.get(1..4).ok_or(AprsError::InvalidFormat)?;
69 for &b in lon_bytes {
70 if b < 28 {
71 return Err(AprsError::InvalidCoordinates);
72 }
73 }
74
75 let mut lat_digits = [0u8; 6];
79 let mut north = true;
80 let mut lon_offset = 0i16;
81
82 for (i, &ch) in dest_head.iter().enumerate() {
83 let (digit, is_custom) = mice_dest_digit(ch)?;
84 if let Some(slot) = lat_digits.get_mut(i) {
85 *slot = digit;
86 }
87
88 if i == 3 {
91 north = is_custom;
92 }
93 if i == 4 && is_custom {
95 lon_offset = 100;
96 }
97 }
99
100 let d0 = f64::from(*lat_digits.first().ok_or(AprsError::InvalidCoordinates)?);
101 let d1 = f64::from(*lat_digits.get(1).ok_or(AprsError::InvalidCoordinates)?);
102 let d2 = f64::from(*lat_digits.get(2).ok_or(AprsError::InvalidCoordinates)?);
103 let d3 = f64::from(*lat_digits.get(3).ok_or(AprsError::InvalidCoordinates)?);
104 let d4 = f64::from(*lat_digits.get(4).ok_or(AprsError::InvalidCoordinates)?);
105 let d5 = f64::from(*lat_digits.get(5).ok_or(AprsError::InvalidCoordinates)?);
106 let lat_deg = d0.mul_add(10.0, d1);
107 let lat_min = d2.mul_add(10.0, d3) + d4 / 10.0 + d5 / 100.0;
108 let mut latitude = lat_deg + lat_min / 60.0;
109 if !north {
110 latitude = -latitude;
111 }
112
113 let d_byte = *lon_bytes.first().ok_or(AprsError::InvalidCoordinates)?;
116 let m_byte = *lon_bytes.get(1).ok_or(AprsError::InvalidCoordinates)?;
117 let h_byte = *lon_bytes.get(2).ok_or(AprsError::InvalidCoordinates)?;
118 let d = i16::from(d_byte) - 28;
119 let m = i16::from(m_byte) - 28;
120 let h = i16::from(h_byte) - 28;
121
122 let mut lon_deg = d + lon_offset;
123 if (180..=189).contains(&lon_deg) {
124 lon_deg -= 80;
125 } else if (190..=199).contains(&lon_deg) {
126 lon_deg -= 190;
127 }
128
129 let lon_min = if m >= 60 { m - 60 } else { m };
130 let longitude_abs = f64::from(lon_deg) + (f64::from(lon_min) + f64::from(h) / 100.0) / 60.0;
131
132 let dest5 = *dest_head.get(5).ok_or(AprsError::InvalidCoordinates)?;
134 let west = mice_dest_is_custom(dest5);
135 let longitude = if west { -longitude_abs } else { longitude_abs };
136
137 let (speed_knots, course_degrees) = match (header.get(4), header.get(5), header.get(6)) {
142 (Some(&speed_raw), Some(&course_hi), Some(&course_lo)) => {
143 let sp = u16::from(speed_raw).saturating_sub(28);
144 let dc = u16::from(course_hi).saturating_sub(28);
145 let se = u16::from(course_lo).saturating_sub(28);
146 let speed = sp * 10 + dc / 10;
147 let course_raw = (dc % 10) * 100 + se;
148 let speed_opt = if speed < 800 { Some(speed) } else { None };
149 let course_opt = if course_raw > 0 && course_raw <= 360 {
150 Some(course_raw)
151 } else {
152 None
153 };
154 (speed_opt, course_opt)
155 }
156 _ => (None, None),
157 };
158
159 let symbol_code = header.get(7).map_or('/', |&b| b as char);
161 let symbol_table = header.get(8).map_or('/', |&b| b as char);
162
163 let comment = info.get(9..).map_or_else(String::new, |rest| {
164 String::from_utf8_lossy(rest).into_owned()
165 });
166
167 let c0 = *dest_head.first().ok_or(AprsError::InvalidCoordinates)?;
170 let c1 = *dest_head.get(1).ok_or(AprsError::InvalidCoordinates)?;
171 let c2 = *dest_head.get(2).ok_or(AprsError::InvalidCoordinates)?;
172 let mice_message = mice_decode_message([c0, c1, c2]);
173
174 let mice_altitude_m = mice_decode_altitude(&comment);
177
178 let weather = extract_position_weather(symbol_code, &comment);
179 let extensions = parse_aprs_extensions(&comment);
180 Ok(AprsPosition {
181 latitude,
182 longitude,
183 symbol_table,
184 symbol_code,
185 speed_knots,
186 course_degrees,
187 comment,
188 weather,
189 extensions,
190 mice_message,
191 mice_altitude_m,
192 ambiguity: PositionAmbiguity::None,
194 })
195}
196
197const fn mice_dest_digit(ch: u8) -> Result<(u8, bool), AprsError> {
202 match ch {
203 b'0'..=b'9' => Ok((ch - b'0', false)),
204 b'A'..=b'J' => Ok((ch - b'A', true)), b'K' | b'L' | b'Z' => Ok((0, true)), b'P'..=b'Y' => Ok((ch - b'P', true)), _ => Err(AprsError::InvalidCoordinates),
208 }
209}
210
211const fn mice_dest_is_custom(ch: u8) -> bool {
215 matches!(ch, b'A'..=b'L' | b'P'..=b'Z')
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228enum MiceMsgClass {
229 Std0,
230 Std1,
231 Custom,
232}
233
234const fn mice_msg_class(ch: u8) -> Option<MiceMsgClass> {
235 match ch {
236 b'0'..=b'9' | b'L' => Some(MiceMsgClass::Std0),
237 b'P'..=b'Y' | b'Z' => Some(MiceMsgClass::Std1),
238 b'A'..=b'K' => Some(MiceMsgClass::Custom),
239 _ => None,
240 }
241}
242
243fn mice_decode_message(chars: [u8; 3]) -> Option<MiceMessage> {
250 let c0 = mice_msg_class(*chars.first()?)?;
251 let c1 = mice_msg_class(*chars.get(1)?)?;
252 let c2 = mice_msg_class(*chars.get(2)?)?;
253 if matches!(
254 (c0, c1, c2),
255 (MiceMsgClass::Custom, _, _) | (_, MiceMsgClass::Custom, _) | (_, _, MiceMsgClass::Custom)
256 ) {
257 return None;
258 }
259 let bit = |c| u8::from(matches!(c, MiceMsgClass::Std1));
260 let idx = (bit(c0) << 2) | (bit(c1) << 1) | bit(c2);
261 Some(match idx {
262 0b111 => MiceMessage::OffDuty,
263 0b110 => MiceMessage::EnRoute,
264 0b101 => MiceMessage::InService,
265 0b100 => MiceMessage::Returning,
266 0b011 => MiceMessage::Committed,
267 0b010 => MiceMessage::Special,
268 0b001 => MiceMessage::Priority,
269 _ => MiceMessage::Emergency, })
271}
272
273#[must_use]
281pub const fn mice_message_bits(msg: MiceMessage) -> (bool, bool, bool) {
282 match msg {
283 MiceMessage::OffDuty => (true, true, true), MiceMessage::EnRoute => (true, true, false), MiceMessage::InService => (true, false, true), MiceMessage::Returning => (true, false, false), MiceMessage::Committed => (false, true, true), MiceMessage::Special => (false, true, false), MiceMessage::Priority => (false, false, true), MiceMessage::Emergency => (false, false, false), }
292}
293
294fn mice_decode_altitude(comment: &str) -> Option<i32> {
304 let bytes = comment.as_bytes();
305 if bytes.len() < 4 {
306 return None;
307 }
308 for i in 0..=bytes.len() - 4 {
309 let window = bytes.get(i..i + 4)?;
310 if window.get(3) != Some(&b'}') {
311 continue;
312 }
313 let b0 = *window.first()?;
314 let b1 = *window.get(1)?;
315 let b2 = *window.get(2)?;
316 if !(33..=126).contains(&b0) || !(33..=126).contains(&b1) || !(33..=126).contains(&b2) {
317 continue;
318 }
319 let val = i32::from(b0 - 33) * 91 * 91 + i32::from(b1 - 33) * 91 + i32::from(b2 - 33);
320 return Some(val - 10_000);
321 }
322 None
323}
324
325pub fn parse_aprs_data_full(info: &[u8], destination: &str) -> Result<AprsData, AprsError> {
338 let first = *info.first().ok_or(AprsError::InvalidFormat)?;
339
340 match first {
341 b'`' | b'\'' | 0x1C | 0x1D => {
343 parse_mice_position(destination, info).map(AprsData::Position)
344 }
345 _ => crate::packet::parse_aprs_data(info),
346 }
347}
348
349#[cfg(test)]
354mod tests {
355 use super::*;
356
357 type TestResult = Result<(), Box<dyn std::error::Error>>;
358
359 #[test]
362 fn parse_mice_basic() -> TestResult {
363 let dest = "SUQU5P";
365 let info: &[u8] = &[
366 0x60, 125, 73, 58, 40, 40, 40, b'>', b'/', ];
376
377 let pos = parse_mice_position(dest, info)?;
378 assert!((pos.latitude - 35.258).abs() < 0.01, "lat={}", pos.latitude);
379 assert!(
380 (pos.longitude - (-97.755)).abs() < 0.01,
381 "lon={}",
382 pos.longitude
383 );
384 assert_eq!(pos.symbol_code, '>');
385 assert_eq!(pos.symbol_table, '/');
386 assert_eq!(pos.speed_knots, Some(121));
387 assert_eq!(pos.course_degrees, Some(212));
388 Ok(())
389 }
390
391 #[test]
392 fn parse_mice_invalid_type() {
393 assert!(
394 parse_mice_position("SUQU5P", b"!test data").is_err(),
395 "non-mic-e type"
396 );
397 }
398
399 #[test]
400 fn parse_mice_too_short() {
401 assert!(
402 parse_mice_position("SHORT", &[0x60, 1, 2]).is_err(),
403 "too-short rejected",
404 );
405 }
406
407 #[test]
408 fn parse_mice_speed_ge_800_rejected() -> TestResult {
409 let dest = "SUQU5P";
412 let info: &[u8] = &[0x60, 125, 73, 58, 108, 28, 28, b'>', b'/'];
413 let pos = parse_mice_position(dest, info)?;
414 assert_eq!(pos.speed_knots, None);
415 Ok(())
416 }
417
418 #[test]
419 fn mice_decode_message_off_duty() {
420 assert_eq!(mice_decode_message(*b"PPP"), Some(MiceMessage::OffDuty));
421 }
422
423 #[test]
424 fn mice_decode_message_emergency() {
425 assert_eq!(mice_decode_message(*b"000"), Some(MiceMessage::Emergency));
426 }
427
428 #[test]
429 fn mice_decode_message_in_service() {
430 assert_eq!(mice_decode_message(*b"P0P"), Some(MiceMessage::InService));
431 }
432
433 #[test]
434 fn mice_decode_message_custom_returns_none() {
435 assert_eq!(mice_decode_message(*b"APP"), None);
436 assert_eq!(mice_decode_message(*b"PKP"), None);
437 }
438
439 #[test]
440 fn mice_decode_altitude_sea_level() -> TestResult {
441 let altitude = mice_decode_altitude("\"3r}").ok_or("missing altitude")?;
443 assert_eq!(altitude, 0);
444 Ok(())
445 }
446
447 #[test]
448 fn mice_decode_altitude_absent() {
449 assert_eq!(mice_decode_altitude("no altitude here"), None);
450 assert_eq!(mice_decode_altitude(""), None);
451 assert_eq!(mice_decode_altitude("abc"), None);
452 }
453
454 #[test]
455 fn parse_mice_populates_message_and_altitude() -> TestResult {
456 let mut info = vec![0x60u8, 125, 73, 58, 40, 40, 40, b'>', b'/'];
457 info.extend_from_slice(b"\"3r}");
458 let pos = parse_mice_position("SUQU5P", &info)?;
459 assert_eq!(pos.mice_message, Some(MiceMessage::OffDuty));
460 assert_eq!(pos.mice_altitude_m, Some(0));
461 Ok(())
462 }
463
464 #[test]
465 fn parse_mice_course_zero_is_none() -> TestResult {
466 let dest = "SUQU5P";
469 let info: &[u8] = &[0x60, 125, 73, 58, 28, 28, 28, b'>', b'/'];
470 let pos = parse_mice_position(dest, info)?;
471 assert_eq!(pos.speed_knots, Some(0));
472 assert_eq!(pos.course_degrees, None);
473 Ok(())
474 }
475
476 #[test]
479 fn mice_rejects_low_longitude_bytes() {
480 let dest = "SUQU5P";
482 let info: &[u8] = &[0x60, 27, 73, 58, 40, 40, 40, b'>', b'/'];
483 assert_eq!(
484 parse_mice_position(dest, info),
485 Err(AprsError::InvalidCoordinates)
486 );
487 }
488
489 #[test]
490 fn mice_rejects_zero_longitude_byte() {
491 let dest = "SUQU5P";
492 let info: &[u8] = &[0x60, 125, 0, 58, 40, 40, 40, b'>', b'/'];
493 assert_eq!(
494 parse_mice_position(dest, info),
495 Err(AprsError::InvalidCoordinates)
496 );
497 }
498
499 #[test]
500 fn mice_accepts_minimum_valid_byte() {
501 let dest = "SUQU5P";
502 let info: &[u8] = &[0x60, 28, 28, 28, 40, 40, 40, b'>', b'/'];
503 assert!(parse_mice_position(dest, info).is_ok(), "min bytes ok");
504 }
505
506 #[test]
509 fn full_dispatch_mice_current() -> TestResult {
510 let dest = "SUQU5P";
511 let info: &[u8] = &[0x60, 125, 73, 58, 40, 40, 40, b'>', b'/'];
512 let result = parse_aprs_data_full(info, dest)?;
513 assert!(matches!(result, AprsData::Position(_)));
514 Ok(())
515 }
516
517 #[test]
518 fn full_dispatch_mice_old() -> TestResult {
519 let dest = "SUQU5P";
520 let info: &[u8] = &[b'\'', 125, 73, 58, 40, 40, 40, b'>', b'/'];
521 let result = parse_aprs_data_full(info, dest)?;
522 assert!(matches!(result, AprsData::Position(_)));
523 Ok(())
524 }
525
526 #[test]
527 fn full_dispatch_mice_0x1c() -> TestResult {
528 let dest = "SUQU5P";
529 let info: &[u8] = &[0x1C, 125, 73, 58, 40, 40, 40, b'>', b'/'];
530 let result = parse_aprs_data_full(info, dest)?;
531 assert!(matches!(result, AprsData::Position(_)));
532 Ok(())
533 }
534
535 #[test]
536 fn full_dispatch_mice_0x1d() -> TestResult {
537 let dest = "SUQU5P";
538 let info: &[u8] = &[0x1D, 125, 73, 58, 40, 40, 40, b'>', b'/'];
539 let result = parse_aprs_data_full(info, dest)?;
540 assert!(matches!(result, AprsData::Position(_)));
541 Ok(())
542 }
543
544 #[test]
545 fn full_dispatch_non_mice_delegates() -> TestResult {
546 let info = b"!4903.50N/07201.75W-Test";
547 let result = parse_aprs_data_full(info, "APRS")?;
548 assert!(matches!(result, AprsData::Position(_)));
549 Ok(())
550 }
551
552 #[test]
553 fn full_dispatch_empty_info() {
554 assert!(parse_aprs_data_full(b"", "APRS").is_err(), "empty rejected");
555 }
556}