#![cfg(feature = "micE")]
use yodel::aprs::mic_e::{self, MicE, MicEError, MicEFix, MicEMessage};
use yodel::aprs::{Ambiguity, Latitude, Longitude, Symbol};
fn hundredths(units: i64) -> i64 {
let step = yodel::geo::UNITS_PER_HUNDREDTH_MINUTE;
let half = if units < 0 { -step / 2 } else { step / 2 };
(units + half) / step
}
fn report(lat: i64, lon: i64) -> MicE<'static> {
MicE {
latitude: Latitude::new(lat * yodel::geo::UNITS_PER_HUNDREDTH_MINUTE).unwrap(),
longitude: Longitude::new(lon * yodel::geo::UNITS_PER_HUNDREDTH_MINUTE).unwrap(),
speed: 0,
course: 0,
symbol: Symbol::CAR,
message: MicEMessage::OffDuty,
fix: MicEFix::Current,
altitude: None,
device_prefix: None,
ambiguity: 0,
status: b"",
}
}
#[test]
fn spec_vector_encodes() {
let mut r = report(33 * 6000 + 2564, -(112 * 6000 + 774));
r.speed = 20;
r.course = 251;
r.symbol = Symbol::from_wire(b'/', b'j');
r.message = MicEMessage::Returning;
let mut dest = [0u8; 6];
let mut info = [0u8; 32];
let len = r.encode(&mut dest, &mut info).unwrap();
assert_eq!(&dest, b"S32UVT");
assert_eq!(&info[..len], b"`(_fn\"Oj/");
}
#[test]
fn spec_vector_decodes() {
let got = mic_e::decode(b"S32UVT", b"`(_fn\"Oj/").unwrap();
assert_eq!(hundredths(got.latitude.units()), 33 * 6000 + 2564);
assert_eq!(hundredths(got.longitude.units()), -(112 * 6000 + 774));
assert_eq!(got.speed, 20);
assert_eq!(got.course, 251);
assert_eq!(got.symbol.to_wire(), (b'/', b'j'));
assert_eq!(got.message, MicEMessage::Returning);
assert_eq!(got.fix, MicEFix::Current);
assert_eq!(got.altitude, None);
assert_eq!(got.ambiguity, 0);
assert_eq!(got.status, b"");
}
#[test]
fn south_east_custom_vector() {
let mut r = report(-(5 * 6000 + 607), 8 * 6000 + 901);
r.message = MicEMessage::Custom2;
let mut dest = [0u8; 6];
let mut info = [0u8; 32];
let len = r.encode(&mut dest, &mut info).unwrap();
assert_eq!(&dest, b"A5A6P7");
assert_eq!(&info[..len], b"`~a\x1dl \x1c>/");
let got = mic_e::decode(&dest, &info[..len]).unwrap();
assert_eq!(got, r);
}
#[test]
fn altitude_and_status() {
let mut r = report(200_564, -672_774);
r.altitude = Some(61);
r.status = b"Test 001234";
let mut dest = [0u8; 6];
let mut info = [0u8; 40];
let len = r.encode(&mut dest, &mut info).unwrap();
assert_eq!(&info[9..13], b"\"4T}");
assert_eq!(&info[13..len], b"Test 001234");
let got = mic_e::decode(&dest, &info[..len]).unwrap();
assert_eq!(got, r);
}
#[test]
fn old_fix_type_byte() {
let mut r = report(200_564, -672_774);
r.fix = MicEFix::Old;
let mut dest = [0u8; 6];
let mut info = [0u8; 16];
let len = r.encode(&mut dest, &mut info).unwrap();
assert_eq!(info[0], b'\'');
let got = mic_e::decode(&dest, &info[..len]).unwrap();
assert_eq!(got.fix, MicEFix::Old);
}
const ALL_MESSAGES: [MicEMessage; 15] = [
MicEMessage::OffDuty,
MicEMessage::EnRoute,
MicEMessage::InService,
MicEMessage::Returning,
MicEMessage::Committed,
MicEMessage::Special,
MicEMessage::Priority,
MicEMessage::Emergency,
MicEMessage::Custom0,
MicEMessage::Custom1,
MicEMessage::Custom2,
MicEMessage::Custom3,
MicEMessage::Custom4,
MicEMessage::Custom5,
MicEMessage::Custom6,
];
#[test]
fn message_types_exhaustive() {
for msg in ALL_MESSAGES {
let mut r = report(200_564, -672_774);
r.message = msg;
let mut dest = [0u8; 6];
let mut info = [0u8; 16];
let len = r.encode(&mut dest, &mut info).unwrap();
let got = mic_e::decode(&dest, &info[..len]).unwrap();
assert_eq!(got.message, msg, "dest {dest:?}");
}
}
#[test]
fn hemispheres_and_offset() {
for &lat in &[200_564i64, -200_564] {
for &lon_deg in &[0i64, 5, 9, 10, 42, 99, 100, 105, 109, 110, 179] {
for &east in &[false, true] {
let lon = (lon_deg * 6000 + 1234) * if east { 1 } else { -1 };
let r = report(lat, lon);
let mut dest = [0u8; 6];
let mut info = [0u8; 16];
let len = r.encode(&mut dest, &mut info).unwrap();
let got = mic_e::decode(&dest, &info[..len]).unwrap();
assert_eq!(got, r, "lat {lat} lon {lon}");
}
}
}
}
#[test]
fn ambiguity_levels() {
for amb in 0u8..=4 {
let mut r = report(33 * 6000 + 2564, -672_774);
r.ambiguity = amb;
let mut dest = [0u8; 6];
let mut info = [0u8; 16];
let len = r.encode(&mut dest, &mut info).unwrap();
let got = mic_e::decode(&dest, &info[..len]).unwrap();
assert_eq!(got.ambiguity, amb);
let digits = [3i32, 3, 2, 5, 6, 4];
let mut kept = 0i64;
for (i, d) in digits.iter().enumerate() {
if i < 6 - amb as usize {
kept += i64::from(*d) * [100_000, 10_000, 1_000, 100, 10, 1][i];
}
}
let deg = kept / 10_000;
let rest = kept % 10_000;
assert_eq!(
hundredths(got.latitude.units()),
deg * 6000 + rest,
"ambiguity {amb}"
);
}
let mut r = report(200_564, -672_774);
r.ambiguity = 4;
r.message = MicEMessage::OffDuty;
let mut dest = [0u8; 6];
let mut info = [0u8; 16];
r.encode(&mut dest, &mut info).unwrap();
assert_eq!(&dest[2..6], b"ZZZZ");
r.message = MicEMessage::Custom0;
r.encode(&mut dest, &mut info).unwrap();
assert_eq!(&dest[2..6], b"KZZZ");
let mut r = report(-200_564, 300_000);
r.ambiguity = 4;
r.message = MicEMessage::Emergency;
r.encode(&mut dest, &mut info).unwrap();
assert_eq!(&dest[2..6], b"LLLL");
}
#[test]
fn declared_ambiguity_reaches_the_coordinates() {
for amb in 0u8..=4 {
let r = report(33 * 6000 + 2564, -672_774)
.with_ambiguity(amb)
.unwrap();
let mut dest = [0u8; 6];
let mut info = [0u8; 16];
let len = r.encode(&mut dest, &mut info).unwrap();
let got = mic_e::decode(&dest, &info[..len]).unwrap();
let at = got.coordinates();
assert_eq!(
at.ambiguity,
Ambiguity::new(amb).unwrap(),
"ambiguity {amb}"
);
assert_eq!(at.ambiguity.digits(), amb, "ambiguity {amb}");
assert_eq!(at.ambiguity.is_exact(), amb == 0, "ambiguity {amb}");
let level = Ambiguity::new(amb).unwrap();
assert_eq!(
at.latitude.units(),
level.mask(got.latitude.units()),
"ambiguity {amb}"
);
assert_eq!(
at.longitude.units(),
level.mask(got.longitude.units()),
"ambiguity {amb}"
);
assert!(at.longitude.units().abs() <= got.longitude.units().abs());
assert_eq!(
at.longitude.units() == got.longitude.units(),
amb == 0,
"only an exact report may pass the longitude through: {amb}"
);
}
}
#[test]
fn declared_ambiguity_survives_a_real_wire_payload() {
let got = mic_e::decode(b"F2A9ZL", b"'v:&lg!-/\"6)}hello").unwrap();
assert_eq!(got.ambiguity, 2);
let at = got.coordinates();
assert_eq!(at.ambiguity, Ambiguity::new(2).unwrap());
assert_eq!(at.ambiguity.digits(), 2);
assert!(!at.ambiguity.is_exact());
assert_eq!(hundredths(at.latitude.units()), -(52 * 6000 + 900));
assert_eq!(hundredths(got.latitude.units()), -(52 * 6000 + 900));
assert_eq!(hundredths(got.longitude.units()), 30 * 100 + 10);
assert_eq!(hundredths(at.longitude.units()), 30 * 100);
}
#[test]
fn spec_chapter_10_ignores_the_low_longitude_digits() {
let got = mic_e::decode(b"T4SQZZ", b"'(_f \x1c>/]").expect("chapter 10's own vector");
assert_eq!(got.ambiguity, 2, "the destination declares two digits");
assert_eq!(
hundredths(got.longitude.units()),
-(112 * 6000 + 7 * 100 + 74)
);
let at = got.coordinates();
assert_eq!(
hundredths(at.longitude.units()),
-(112 * 6000 + 7 * 100),
"chapter 10: the longitude will be 112 degrees 7 minutes"
);
assert_eq!(at.ambiguity, Ambiguity::new(2).unwrap());
}
#[test]
fn coordinates_carries_the_declared_ambiguity() {
const BLURRED: MicE<'static> = MicE {
latitude: match Latitude::new(-(52 * 6000 + 900) * yodel::geo::UNITS_PER_HUNDREDTH_MINUTE) {
Ok(l) => l,
Err(_) => panic!("in range"),
},
longitude: match Longitude::new(3010 * yodel::geo::UNITS_PER_HUNDREDTH_MINUTE) {
Ok(l) => l,
Err(_) => panic!("in range"),
},
speed: 0,
course: 0,
symbol: Symbol::CAR,
message: MicEMessage::OffDuty,
fix: MicEFix::Current,
altitude: None,
device_prefix: None,
ambiguity: 2,
status: b"",
};
let ambiguity: Ambiguity = BLURRED.coordinates().ambiguity;
assert_eq!(ambiguity.digits(), 2);
assert!(!ambiguity.is_exact());
}
#[test]
fn out_of_range_ambiguity_saturates_at_four_digits() {
for bogus in [5u8, 6, 100, u8::MAX] {
let mut r = report(33 * 6000 + 2564, -672_774);
r.ambiguity = bogus;
let at = r.coordinates();
assert_eq!(
at.ambiguity,
Ambiguity::new(4).unwrap(),
"ambiguity {bogus}"
);
assert_eq!(at.ambiguity.digits(), 4, "ambiguity {bogus}");
assert!(!at.ambiguity.is_exact(), "ambiguity {bogus}");
let four = Ambiguity::new(4).unwrap();
assert_eq!(
at.latitude.units(),
four.mask(r.latitude.units()),
"ambiguity {bogus}"
);
assert_eq!(
at.longitude.units(),
four.mask(r.longitude.units()),
"ambiguity {bogus}"
);
let mut dest = [0u8; 6];
let mut info = [0u8; 16];
assert_eq!(
r.encode(&mut dest, &mut info),
Err(MicEError::BadAmbiguity { got: bogus })
);
assert_eq!(
r.with_ambiguity(bogus),
Err(MicEError::BadAmbiguity { got: bogus })
);
}
}
#[test]
fn speed_course_boundaries() {
for &(speed, course) in &[
(0u16, 0u16),
(0, 360),
(1, 1),
(199, 359),
(200, 360),
(28, 100),
(799, 0),
(799, 360),
] {
let mut r = report(200_564, -672_774);
r.speed = speed;
r.course = course;
let mut dest = [0u8; 6];
let mut info = [0u8; 16];
let len = r.encode(&mut dest, &mut info).unwrap();
let got = mic_e::decode(&dest, &info[..len]).unwrap();
assert_eq!((got.speed, got.course), (speed, course));
}
}
#[test]
fn every_accepted_speed_and_course_encodes_to_seven_bit_ascii() {
const MIN_CASES: usize = 800 * 361;
let mut cases = 0usize;
for speed in 0..=799u16 {
for course in 0..=360u16 {
let mut r = report(33 * 6000 + 2564, -(112 * 6000 + 774));
r.speed = speed;
r.course = course;
let mut dest = [0u8; 6];
let mut info = [0u8; 16];
let len = r
.encode(&mut dest, &mut info)
.unwrap_or_else(|e| panic!("{speed} kn / {course} deg rejected: {e:?}"));
for (i, &b) in info[..len].iter().enumerate() {
assert!(
b <= 0x7F,
"{speed} kn / {course} deg put 0x{b:02X} at info[{i}]: an APRS \
information field is 7-bit ASCII"
);
}
for (i, &b) in dest.iter().enumerate() {
assert!(
b <= 0x7F,
"{speed} kn / {course} deg put 0x{b:02X} at dest[{i}]"
);
}
let got = mic_e::decode(&dest, &info[..len])
.unwrap_or_else(|e| panic!("{speed} kn / {course} deg failed to decode: {e:?}"));
assert_eq!((got.speed, got.course), (speed, course));
cases += 1;
}
}
assert!(
cases >= MIN_CASES,
"the sweep checked {cases} speed/course pairs, floor is {MIN_CASES} -- \
the domain narrowed and the law stopped proving anything"
);
}
#[test]
fn sp28_drops_the_offset_column_above_one_hundred_ninety_nine_knots() {
#[rustfmt::skip]
let rows = [
(0u16, 0x6Cu8, 0x20u8), (40, 0x70, 0x20), (190, 0x7F, 0x20), (199, 0x7F, 0x7A), (200, 0x30, 0x20), (799, 0x6B, 0x7A), ];
for &(speed, sp, dc) in &rows {
let mut r = report(33 * 6000 + 2564, -(112 * 6000 + 774));
r.speed = speed;
let mut dest = [0u8; 6];
let mut info = [0u8; 16];
let len = r.encode(&mut dest, &mut info).expect("in range");
assert_eq!(info[4], sp, "SP+28 for {speed} kn");
assert_eq!(info[5], dc, "DC+28 for {speed} kn");
assert_eq!(info[6], 28, "SE+28 for course 0");
let got = mic_e::decode(&dest, &info[..len]).expect("decodes");
assert_eq!((got.speed, got.course), (speed, 0));
}
let mut r = report(33 * 6000 + 2564, -(112 * 6000 + 774));
r.speed = 800;
let mut dest = [0u8; 6];
let mut info = [0u8; 16];
assert_eq!(
r.encode(&mut dest, &mut info),
Err(MicEError::BadSpeed { got: 800 })
);
assert_eq!(
r.encode_info(&mut info),
Err(MicEError::BadSpeed { got: 800 })
);
assert_eq!(
MicE::new(r.latitude, r.longitude, 800, 0, r.symbol, r.message),
Err(MicEError::BadSpeed { got: 800 })
);
assert!(MicE::new(r.latitude, r.longitude, 799, 360, r.symbol, r.message).is_ok());
}
#[test]
fn single_column_speeds_adjudicate_against_the_reference() {
for &(speed, course, want) in &[
(200u16, 251u16, b"`(_f0\"Oj/".as_slice()),
(799, 360, b"`(_fk}Xj/".as_slice()),
] {
let mut r = report(33 * 6000 + 2564, -(112 * 6000 + 774));
r.speed = speed;
r.course = course;
r.symbol = Symbol::from_wire(b'/', b'j');
r.message = MicEMessage::Returning;
let mut dest = [0u8; 6];
let mut info = [0u8; 32];
let len = r.encode(&mut dest, &mut info).expect("in range");
assert_eq!(&dest, b"S32UVT", "{speed} kn / {course} deg");
assert_eq!(&info[..len], want, "{speed} kn / {course} deg");
assert!(info[..len].iter().all(|&b| b <= 0x7F));
let got = mic_e::decode(&dest, &info[..len]).expect("decodes");
assert_eq!((got.speed, got.course), (speed, course));
}
}
#[test]
fn round_trip_sweep() {
let mut state = 0x2545_F491_4F6C_DD1Du64;
let mut next = move |bound: u64| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(state >> 33) % bound
};
const PREFIXES: [Option<u8>; 5] = [None, Some(b'>'), Some(b']'), Some(b'`'), Some(b'\'')];
for i in 0..2000 {
let lat = next(2 * 540_001) as i64 - 540_000;
let lon = next(2 * 1_079_999 + 1) as i64 - 1_079_999;
let altitude = if next(2) == 0 {
Some(next(753_571) as i32 - 10_000)
} else {
None
};
let r = MicE {
latitude: Latitude::new(lat * yodel::geo::UNITS_PER_HUNDREDTH_MINUTE).unwrap(),
longitude: Longitude::new(lon * yodel::geo::UNITS_PER_HUNDREDTH_MINUTE).unwrap(),
speed: next(800) as u16,
course: next(361) as u16,
symbol: Symbol::from_wire(
[b'/', b'\\', b'3', b'Q'][next(4) as usize],
(next(94) + 33) as u8,
),
message: ALL_MESSAGES[next(15) as usize],
fix: if next(2) == 0 {
MicEFix::Current
} else {
MicEFix::Old
},
altitude,
device_prefix: PREFIXES[next(5) as usize],
ambiguity: 0,
status: b"status text",
};
let mut dest = [0u8; 6];
let mut info = [0u8; 32];
let len = r.encode(&mut dest, &mut info).unwrap();
let got = mic_e::decode(&dest, &info[..len]).unwrap();
assert_eq!(got, r, "iteration {i}");
}
}
#[test]
fn spec_three_spellings_of_one_altitude() {
for (tail, prefix) in [
(&b"\"4T}"[..], None),
(&b">\"4T}"[..], Some(b'>')),
(&b"]\"4T}"[..], Some(b']')),
] {
let mut info = [0u8; 32];
info[..9].copy_from_slice(b"`(_fn\"Oj/");
info[9..9 + tail.len()].copy_from_slice(tail);
let got = mic_e::decode(b"S32UVT", &info[..9 + tail.len()]).unwrap();
assert_eq!(got.altitude, Some(61), "{:?}", tail.escape_ascii());
assert_eq!(got.device_prefix, prefix, "{:?}", tail.escape_ascii());
assert_eq!(got.status, b"");
}
}
#[test]
fn spec_maidenhead_status_is_a_prefix_with_no_altitude() {
for prefix in [b'>', b']'] {
let mut info = [0u8; 40];
info[..9].copy_from_slice(b"`(_fn\"Oj/");
info[9] = prefix;
info[10..29].copy_from_slice(b"IO91SX/G Helloworld");
let got = mic_e::decode(b"S32UVT", &info[..29]).unwrap();
assert_eq!(got.device_prefix, Some(prefix));
assert_eq!(got.altitude, None);
assert_eq!(got.status, b"IO91SX/G Helloworld");
let mut out = [0u8; 40];
let n = got.encode_info(&mut out).unwrap();
assert_eq!(&out[..n], &info[..29]);
}
}
#[test]
fn corpus_kenwood_spells_its_prefix_both_ways() {
let with_alt = mic_e::decode(b"S4PXYW", b"'._|l tv/]\"6[}\r").unwrap();
assert_eq!(with_alt.device_prefix, Some(b']'));
assert_eq!(with_alt.altitude, Some(250));
assert_eq!(with_alt.status, b"\r");
let without = mic_e::decode(b"STPYVU", b"'._wlzzv/]Stopped\r").unwrap();
assert_eq!(without.device_prefix, Some(b']'));
assert_eq!(without.altitude, None);
assert_eq!(without.status, b"Stopped\r");
assert_eq!(with_alt.symbol.to_wire(), without.symbol.to_wire());
for (dest, wire) in [
(&b"S4PXYW"[..], &b"'._|l tv/]\"6[}\r"[..]),
(&b"STPYVU"[..], &b"'._wlzzv/]Stopped\r"[..]),
] {
let dest: [u8; 6] = dest.try_into().unwrap();
let got = mic_e::decode(&dest, wire).unwrap();
let mut out = [0u8; 64];
let n = got.encode_info(&mut out).unwrap();
assert_eq!(&out[..n], wire);
}
}
#[test]
fn corpus_bare_prefix_frames() {
for (dest, wire, status) in [
(&b"S4PWPW"[..], &b"'-M(l \x1cK\\]\r"[..], &b"\r"[..]),
(
&b"S3PYTW"[..],
&b"'-'Tl \x1c#/]Palomar REACT Digi\r"[..],
&b"Palomar REACT Digi\r"[..],
),
(
&b"S3URPP"[..],
&b"'._0l \x1c-/]Ted@Home in Lakewood,CA.USA\r"[..],
&b"Ted@Home in Lakewood,CA.USA\r"[..],
),
(
&b"S3PRWP"[..],
&b"',Pdl\"Gk/]Tania's Crusin\r"[..],
&b"Tania's Crusin\r"[..],
),
] {
let dest: [u8; 6] = dest.try_into().unwrap();
let got = mic_e::decode(&dest, wire).unwrap();
assert_eq!(
(got.device_prefix, got.altitude, got.status),
(Some(b']'), None, status),
"{:?}",
wire.escape_ascii()
);
let mut out = [0u8; 64];
let n = got.encode_info(&mut out).unwrap();
assert_eq!(&out[..n], wire, "{:?}", wire.escape_ascii());
}
}
#[test]
fn prefix_without_altitude_round_trips_byte_exactly() {
for prefix in [b'>', b']', b'`', b'\''] {
for status in [&b""[..], &b"Stopped"[..], &b"Mobile station, 25 W"[..]] {
let r = report(200_564, -672_774)
.with_status(status)
.with_device_prefix(Some(prefix))
.unwrap();
let mut dest = [0u8; 6];
let mut info = [0u8; 64];
let len = r.encode(&mut dest, &mut info).unwrap();
assert_eq!(info[9], prefix);
assert_eq!(&info[10..len], status);
let got = mic_e::decode(&dest, &info[..len]).unwrap();
assert_eq!(got, r, "{:?} + {:?}", prefix as char, status.escape_ascii());
let mut out = [0u8; 64];
let n = got.encode_info(&mut out).unwrap();
assert_eq!(&out[..n], &info[..len]);
}
}
}
#[test]
fn unprefixed_altitude_outranks_a_prefix_shaped_first_digit() {
for (metres, wire) in [
(39_686i32, &b"'!!}"[..]),
(40_000, &b"'$J}"[..]),
(230_149, &b">!!}"[..]),
(492_471, &b"]^]}"[..]),
(511_703, &b"`!!}"[..]),
] {
let r = report(200_564, -672_774)
.with_altitude(Some(metres))
.with_status(b"up");
let mut dest = [0u8; 6];
let mut info = [0u8; 32];
let len = r.encode(&mut dest, &mut info).unwrap();
assert_eq!(&info[9..13], wire, "{metres} m");
let got = mic_e::decode(&dest, &info[..len]).unwrap();
assert_eq!(got.altitude, Some(metres), "{metres} m");
assert_eq!(got.device_prefix, None, "{metres} m");
assert_eq!(got.status, b"up", "{metres} m");
assert_eq!(got, r, "{metres} m");
}
}
#[test]
fn status_beginning_with_a_prefix_byte_is_the_bounded_cost() {
let r = report(200_564, -672_774).with_status(b">not a radio");
let mut dest = [0u8; 6];
let mut info = [0u8; 32];
let len = r.encode(&mut dest, &mut info).unwrap();
let got = mic_e::decode(&dest, &info[..len]).unwrap();
assert_ne!(got, r, "the struct is deliberately not a fixed point");
assert_eq!(got.device_prefix, Some(b'>'));
assert_eq!(got.status, b"not a radio");
let mut out = [0u8; 32];
let n = got.encode_info(&mut out).unwrap();
assert_eq!(&out[..n], &info[..len]);
let mut rejoined = [0u8; 12];
rejoined[0] = got.device_prefix.unwrap();
rejoined[1..].copy_from_slice(got.status);
assert_eq!(&rejoined[..], r.status);
}
const INFO: &[u8] = b"`(_fn\"Oj/";
#[test]
fn decode_errors() {
assert_eq!(
mic_e::decode(b"S32UV", INFO),
Err(MicEError::BadDestLength { got: 5 })
);
assert_eq!(
mic_e::decode(b"a32UVT", INFO),
Err(MicEError::BadDestChar {
got: b'a',
column: 0
})
);
assert_eq!(
mic_e::decode(b"S32AVT", INFO),
Err(MicEError::BadDestChar {
got: b'A',
column: 3
})
);
assert_eq!(
mic_e::decode(b"SA2UVT", INFO),
Err(MicEError::MixedMessageBits {
got: [b'S', b'A', b'2']
})
);
assert_eq!(
mic_e::decode(b"S3L5VT", INFO),
Err(MicEError::NonTrailingAmbiguity { column: 3 })
);
assert_eq!(
mic_e::decode(b"SZLLLL", INFO),
Err(MicEError::BadAmbiguity { got: 5 })
);
assert_eq!(
mic_e::decode(b"S32UVT", b"`(_fn"),
Err(MicEError::Truncated {
expected: 9,
got: 5
})
);
assert_eq!(
mic_e::decode(b"S32UVT", b"!(_fn\"Oj/"),
Err(MicEError::InvalidDataType { got: b'!' })
);
assert_eq!(
mic_e::decode(b"S32UVT", b"`\x1b_fn\"Oj/"),
Err(MicEError::BadLongitudeByte {
got: 0x1B,
position: 1
})
);
assert_eq!(
mic_e::decode(b"S32UVT", b"`(bfn\"Oj/"),
Err(MicEError::BadLongitudeByte {
got: b'b',
position: 2
})
);
assert_eq!(
mic_e::decode(b"S32UVT", b"`(_\x80n\"Oj/"),
Err(MicEError::BadLongitudeByte {
got: 0x80,
position: 3
})
);
assert_eq!(
mic_e::decode(b"S32UVT", b"`(_f\x1b\"Oj/"),
Err(MicEError::BadSpeedCourseByte {
got: 0x1B,
position: 4
})
);
let odd = mic_e::decode(b"S32UVT", b"`(_fn\"Oj~").expect("position still decodes");
assert_eq!(odd.symbol.to_wire(), (b'~', b'j'));
assert!(odd.symbol.table().is_none(), "'~' is not a nameable table");
assert_eq!(
MicE::new(
odd.latitude,
odd.longitude,
odd.speed,
odd.course,
odd.symbol,
odd.message
),
Err(MicEError::BadSymbolTable { got: b'~' })
);
assert_eq!(
mic_e::decode(b"S32UVT", b"`(_fn\"Oj/\x20AB}"),
Err(MicEError::BadAltitudeChar {
got: 0x20,
position: 9
})
);
assert_eq!(
mic_e::decode(b"Y92UVT", INFO),
Err(MicEError::BadLatitude {
got: 596_564 * yodel::geo::UNITS_PER_HUNDREDTH_MINUTE
})
);
}
#[test]
fn encode_errors() {
let mut dest = [0u8; 6];
let mut info = [0u8; 32];
let r = report(200_564, 1_080_000);
assert_eq!(
r.encode(&mut dest, &mut info),
Err(MicEError::BadLongitude {
got: 1_080_000 * yodel::geo::UNITS_PER_HUNDREDTH_MINUTE
})
);
let mut r = report(200_564, -672_774);
r.speed = 800;
assert_eq!(
r.encode(&mut dest, &mut info),
Err(MicEError::BadSpeed { got: 800 })
);
r.speed = 0;
r.course = 361;
assert_eq!(
r.encode(&mut dest, &mut info),
Err(MicEError::BadCourse { got: 361 })
);
let mut r = report(200_564, -672_774);
r.ambiguity = 5;
assert_eq!(
r.encode(&mut dest, &mut info),
Err(MicEError::BadAmbiguity { got: 5 })
);
let mut r = report(200_564, -672_774);
r.symbol = Symbol::from_wire(b'~', b'>');
assert_eq!(
r.encode(&mut dest, &mut info),
Err(MicEError::BadSymbolTable { got: b'~' })
);
let mut r = report(200_564, -672_774);
r.altitude = Some(743_571);
assert_eq!(
r.encode(&mut dest, &mut info),
Err(MicEError::BadAltitude { got: 743_571 })
);
let r = report(200_564, -672_774);
assert_eq!(
r.encode(&mut dest[..5], &mut info),
Err(MicEError::BufferTooSmall { needed: 6, max: 5 })
);
assert_eq!(
r.encode(&mut dest, &mut info[..8]),
Err(MicEError::BufferTooSmall { needed: 9, max: 8 })
);
}
#[test]
fn aprs_parse_rejects_mic_e_ids() {
use yodel::aprs::{AprsError, AprsPacket};
assert_eq!(
AprsPacket::parse(INFO),
Err(AprsError::InvalidDataType { got: b'`' })
);
assert_eq!(
AprsPacket::parse(b"'(_fn\"Oj/"),
Err(AprsError::InvalidDataType { got: b'\'' })
);
}
#[test]
fn decoded_needs_the_destination_and_decode_frame_supplies_it() {
use yodel::aprs::{Decoded, DecodedKind};
use yodel::ax25::Address;
for info in [INFO, b"'(_fn\"Oj/"] {
let d = Decoded::decode(info);
assert_eq!(d.info, info);
assert_eq!(
d.kind,
DecodedKind::NeedsDestination { dti: info[0] },
"decode() must ask for the destination, not call Mic-E unsupported"
);
assert!(!d.is_typed());
assert!(d.mic_e().is_none());
assert!(d.packet().is_none());
}
let expected = mic_e::decode(b"S32UVT", INFO).expect("spec vector decodes");
for ssid in [0u8, 15] {
let dest = Address::new(b"S32UVT", ssid).expect("valid destination");
let d = Decoded::decode_frame(dest, INFO);
assert_eq!(d.info, INFO);
assert!(d.is_typed());
assert!(d.packet().is_none(), "a Mic-E report is not an AprsPacket");
assert_eq!(d.mic_e(), Some(&expected));
assert!(matches!(d.kind, DecodedKind::MicE(_)));
}
let generic = Address::new(b"APRS", 0).expect("valid destination");
assert_eq!(
Decoded::decode_frame(generic, INFO).kind,
DecodedKind::Malformed {
dti: b'`',
error: MicEError::MixedMessageBits {
got: [b'A', b'P', b'R'],
}
.into(),
}
);
let bad = Address::new(b"S32AVT", 0).expect("valid destination");
assert_eq!(
Decoded::decode_frame(bad, INFO).kind,
DecodedKind::Malformed {
dti: b'`',
error: MicEError::BadDestChar {
got: b'A',
column: 3,
}
.into(),
}
);
}
#[test]
fn an_out_of_range_longitude_byte_is_refused_rather_than_repaired() {
use yodel::aprs::{Decoded, DecodedKind};
use yodel::ax25::Address;
let dest = Address::new(b"S4PXYX", 0).expect("valid destination");
const WIRE: &[u8] = b"\x60\xbe\x5f\x7f\x6c\x23\x35\x3e\x2f\x5d\x22\x36\x6e\x7d";
let decoded = Decoded::decode_frame(dest, WIRE);
assert!(
matches!(
decoded.kind,
DecodedKind::Malformed { .. } | DecodedKind::Unsupported { .. }
),
"0xBE is outside chapter 10's 38..=127, got {:?}",
decoded.kind
);
assert!(!decoded.is_typed());
assert_eq!(decoded.info, WIRE, "the bytes are still handed back");
let mut bit7 = WIRE.to_vec();
bit7[1] = 0x3e; let mut bits74 = WIRE.to_vec();
bits74[1] = 0x2e;
let one_bit = Decoded::decode_frame(dest, &bit7);
let DecodedKind::MicE(ref out_to_sea) = one_bit.kind else {
panic!("clearing bit 7 produces a well-formed report, which is the hazard");
};
let two_bit = Decoded::decode_frame(dest, &bits74);
let DecodedKind::MicE(ref on_land) = two_bit.kind else {
panic!("clearing bits 7 and 4 reaches the station's real position");
};
let sea = hundredths(out_to_sea.longitude.units());
let land = hundredths(on_land.longitude.units());
assert_eq!(land, -(118 * 6000 + 7 * 100 + 99), "118 07.99 W, as sent");
assert_eq!(
sea,
-(134 * 6000 + 7 * 100 + 99),
"134 07.99 W, the Pacific"
);
assert_eq!(
(land - sea) / 6000,
16,
"the two repairs differ by 16 degrees of longitude, and only one \
of them is checkable against this station's other frames"
);
}
#[test]
fn a_course_outside_the_field_is_reported_as_unknown() {
for (sp, dc, se, raw) in [(0x21, 0x25, 0x7f, 999), (0x21, 0x25, 0x40, 936)] {
let info = [
0x60, 0x7d, 0x38, 0x67, sp, dc, se, 0x3e, 0x2f, 0x5d, 0x22, 0x35, 0x68, 0x7d,
];
let report = mic_e::decode(b"S32UVT", &info).expect("the position still decodes");
assert_eq!(
report.course, 0,
"a raw course of {raw} is not a direction and must not be published"
);
assert!(
report.course <= 360,
"MicE::course documents 0..=360 and the decoder must honour it"
);
assert_ne!(report.latitude.units(), 0);
assert_ne!(report.longitude.units(), 0);
}
for (se, want) in [(0x53u8, 355u16), (0x58, 360)] {
let info = [
0x60, 0x7d, 0x38, 0x67, 0x26, 0x29, se, 0x3e, 0x2f, 0x5d, 0x22, 0x35, 0x68, 0x7d,
];
let report = mic_e::decode(b"S32UVT", &info).expect("decodes");
assert_eq!(report.course, want, "a legal course must survive verbatim");
}
}