use core::fmt;
use super::extension::{CommentTelemetry, Dao, comment_telemetry, dao};
use super::symbol::Symbol;
use crate::ax25::Address;
use crate::geo::{
Ambiguity, Coordinates, Latitude, Longitude, UNITS_PER_DEGREE, UNITS_PER_HUNDREDTH_MINUTE,
UNITS_PER_MINUTE,
};
const ALTITUDE_OFFSET: i32 = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MicEFix {
Current,
Old,
}
impl MicEFix {
#[must_use]
pub const fn type_byte(self) -> u8 {
match self {
MicEFix::Current => b'`',
MicEFix::Old => b'\'',
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MicEMessage {
OffDuty,
EnRoute,
InService,
Returning,
Committed,
Special,
Priority,
Emergency,
Custom0,
Custom1,
Custom2,
Custom3,
Custom4,
Custom5,
Custom6,
}
impl MicEMessage {
#[must_use]
pub const fn bits(self) -> (u8, bool) {
match self {
MicEMessage::OffDuty => (0b111, false),
MicEMessage::EnRoute => (0b110, false),
MicEMessage::InService => (0b101, false),
MicEMessage::Returning => (0b100, false),
MicEMessage::Committed => (0b011, false),
MicEMessage::Special => (0b010, false),
MicEMessage::Priority => (0b001, false),
MicEMessage::Emergency => (0b000, false),
MicEMessage::Custom0 => (0b111, true),
MicEMessage::Custom1 => (0b110, true),
MicEMessage::Custom2 => (0b101, true),
MicEMessage::Custom3 => (0b100, true),
MicEMessage::Custom4 => (0b011, true),
MicEMessage::Custom5 => (0b010, true),
MicEMessage::Custom6 => (0b001, true),
}
}
const fn from_bits(bits: u8, custom: bool) -> Self {
match (bits & 0b111, custom) {
(0b111, false) => MicEMessage::OffDuty,
(0b110, false) => MicEMessage::EnRoute,
(0b101, false) => MicEMessage::InService,
(0b100, false) => MicEMessage::Returning,
(0b011, false) => MicEMessage::Committed,
(0b010, false) => MicEMessage::Special,
(0b001, false) => MicEMessage::Priority,
(0b111, true) => MicEMessage::Custom0,
(0b110, true) => MicEMessage::Custom1,
(0b101, true) => MicEMessage::Custom2,
(0b100, true) => MicEMessage::Custom3,
(0b011, true) => MicEMessage::Custom4,
(0b010, true) => MicEMessage::Custom5,
(0b001, true) => MicEMessage::Custom6,
_ => MicEMessage::Emergency,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MicEError {
BadDestLength {
got: usize,
},
BadDestChar {
got: u8,
column: usize,
},
MixedMessageBits {
got: [u8; 3],
},
NonTrailingAmbiguity {
column: usize,
},
Truncated {
expected: usize,
got: usize,
},
InvalidDataType {
got: u8,
},
BadLongitudeByte {
got: u8,
position: usize,
},
BadSpeedCourseByte {
got: u8,
position: usize,
},
BadAltitudeChar {
got: u8,
position: usize,
},
BadAltitude {
got: i32,
},
BadDevicePrefix {
got: u8,
},
BadLatitude {
got: i64,
},
BadLongitude {
got: i64,
},
BadSpeed {
got: u16,
},
BadCourse {
got: u16,
},
BadAmbiguity {
got: u8,
},
BadSymbolTable {
got: u8,
},
BufferTooSmall {
needed: usize,
max: usize,
},
}
impl fmt::Display for MicEError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
MicEError::BadDestLength { got } => write!(
f,
"destination field of {got} bytes is invalid: exactly 6 characters are required"
),
MicEError::BadDestChar { got, column } => write!(
f,
"destination byte 0x{got:02X} in column {column} is outside the Mic-E alphabet"
),
MicEError::MixedMessageBits { got } => write!(
f,
"message-bit columns {:02X} {:02X} {:02X} mix the standard and custom sets",
got[0], got[1], got[2]
),
MicEError::NonTrailingAmbiguity { column } => write!(
f,
"latitude digit in column {column} follows an ambiguity space: ambiguity must be a contiguous suffix"
),
MicEError::Truncated { expected, got } => write!(
f,
"information field of {got} bytes is truncated: at least {expected} bytes are required"
),
MicEError::InvalidDataType { got } => write!(
f,
"data-type identifier 0x{got:02X} is not Mic-E: 0x60 '`' or 0x27 '\\'' is required"
),
MicEError::BadLongitudeByte { got, position } => write!(
f,
"longitude byte 0x{got:02X} at offset {position} decodes outside its legal range"
),
MicEError::BadSpeedCourseByte { got, position } => write!(
f,
"speed/course byte 0x{got:02X} at offset {position} is below the +28 encoding floor"
),
MicEError::BadAltitudeChar { got, position } => write!(
f,
"altitude byte 0x{got:02X} at offset {position} is outside the base-91 alphabet '!'..='{{'"
),
MicEError::BadAltitude { got } => write!(
f,
"altitude of {got} meters is out of range: -10000..=743570 fits three base-91 characters"
),
MicEError::BadDevicePrefix { got } => write!(
f,
"device-identifier prefix 0x{got:02X} is invalid: must be '>', ']', '`' or '\\''"
),
MicEError::BadLatitude { got } => write!(
f,
"latitude of {got} 1/100 arc-minutes is out of range: must be within \u{b1}90\u{b0} with minutes below 60"
),
MicEError::BadLongitude { got } => write!(
f,
"longitude of {got} 1/100 arc-minutes is out of Mic-E range: at most 179\u{b0} 59.99' with minutes below 60"
),
MicEError::BadSpeed { got } => write!(
f,
"speed of {got} knots is out of range: Mic-E encodes at most 799 knots"
),
MicEError::BadCourse { got } => write!(
f,
"course of {got} degrees is out of range: at most 360 degrees"
),
MicEError::BadAmbiguity { got } => write!(
f,
"position ambiguity of {got} digits is out of range: at most 4"
),
MicEError::BadSymbolTable { got } => write!(
f,
"symbol table byte 0x{got:02X} is invalid: must be '/', '\\' or an overlay character"
),
MicEError::BufferTooSmall { needed, max } => write!(
f,
"field of {needed} bytes does not fit: the buffer holds at most {max} bytes"
),
}
}
}
impl core::error::Error for MicEError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MicE<'a> {
pub latitude: Latitude,
pub longitude: Longitude,
pub speed: u16,
pub course: u16,
pub symbol: Symbol,
pub message: MicEMessage,
pub fix: MicEFix,
pub altitude: Option<i32>,
pub device_prefix: Option<u8>,
pub ambiguity: u8,
pub status: &'a [u8],
}
const fn split_dmh(abs: i64) -> (i64, i64, i64) {
let step = UNITS_PER_HUNDREDTH_MINUTE;
let hundredths = (abs + step / 2) / step;
(hundredths / 6000, hundredths / 100 % 60, hundredths % 100)
}
impl<'a> MicE<'a> {
pub fn new(
latitude: Latitude,
longitude: Longitude,
speed: u16,
course: u16,
symbol: Symbol,
message: MicEMessage,
) -> Result<Self, MicEError> {
let lon = longitude.units();
if lon.unsigned_abs() as i64 / UNITS_PER_DEGREE > 179 {
return Err(MicEError::BadLongitude { got: lon });
}
if speed > 799 {
return Err(MicEError::BadSpeed { got: speed });
}
if course > 360 {
return Err(MicEError::BadCourse { got: course });
}
check_symbol_table(symbol.to_wire().0)?;
Ok(Self {
latitude,
longitude,
speed,
course,
symbol,
message,
fix: MicEFix::Current,
altitude: None,
device_prefix: None,
ambiguity: 0,
status: b"",
})
}
#[must_use]
pub const fn with_status(self, status: &'a [u8]) -> Self {
Self { status, ..self }
}
#[must_use]
pub const fn with_fix(self, fix: MicEFix) -> Self {
Self { fix, ..self }
}
#[must_use]
pub const fn with_altitude(self, altitude: Option<i32>) -> Self {
Self { altitude, ..self }
}
pub const fn with_device_prefix(self, device_prefix: Option<u8>) -> Result<Self, MicEError> {
if let Some(byte) = device_prefix
&& !is_device_prefix(byte)
{
return Err(MicEError::BadDevicePrefix { got: byte });
}
Ok(Self {
device_prefix,
..self
})
}
pub const fn with_ambiguity(self, ambiguity: u8) -> Result<Self, MicEError> {
if ambiguity > 4 {
return Err(MicEError::BadAmbiguity { got: ambiguity });
}
Ok(Self { ambiguity, ..self })
}
#[must_use]
pub fn coordinates(&self) -> Coordinates {
let digits = if self.ambiguity > 4 {
4
} else {
self.ambiguity
};
let ambiguity = match Ambiguity::new(digits) {
Ok(value) => value,
Err(_) => Ambiguity::EXACT,
};
let mut lat_units = ambiguity.mask(self.latitude.units());
let mut lon_units = ambiguity.mask(self.longitude.units());
if ambiguity == Ambiguity::EXACT
&& let Some(refinement) = dao(self.status)
{
lat_units += lat_units.signum() * refinement.latitude_units;
lon_units += lon_units.signum() * refinement.longitude_units;
}
let latitude = match Latitude::new(lat_units) {
Ok(value) => value,
Err(_) => self.latitude,
};
let longitude = match Longitude::new(lon_units) {
Ok(value) => value,
Err(_) => self.longitude,
};
Coordinates::new(latitude, longitude).with_ambiguity(ambiguity)
}
#[must_use]
pub fn comment_telemetry(&self) -> Option<CommentTelemetry> {
comment_telemetry(self.status)
}
#[must_use]
pub fn dao(&self) -> Option<Dao> {
dao(self.status)
}
pub fn encode(&self, dest: &mut [u8], info: &mut [u8]) -> Result<usize, MicEError> {
self.encode_destination(dest)?;
self.encode_info(info)
}
pub fn encode_destination(&self, dest: &mut [u8]) -> Result<(), MicEError> {
if dest.len() < 6 {
return Err(MicEError::BufferTooSmall {
needed: 6,
max: dest.len(),
});
}
if self.ambiguity > 4 {
return Err(MicEError::BadAmbiguity {
got: self.ambiguity,
});
}
let lat = self.latitude.units();
let north = lat >= 0;
let (deg, min, hund) = split_dmh(lat.unsigned_abs() as i64);
let digits = [deg / 10, deg % 10, min / 10, min % 10, hund / 10, hund % 10];
let lon_west = self.longitude.units() < 0;
let lon_deg = self.longitude.units().unsigned_abs() as i64 / UNITS_PER_DEGREE;
let lon_offset = !(10..=99).contains(&lon_deg);
let (bits, custom) = self.message.bits();
for (col, out) in dest.iter_mut().enumerate().take(6) {
let blank = col >= 6 - self.ambiguity as usize;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let digit = digits[col] as u8;
let one = match col {
0 => bits & 0b100 != 0,
1 => bits & 0b010 != 0,
2 => bits & 0b001 != 0,
3 => north,
4 => lon_offset,
_ => lon_west,
};
*out = dest_char(digit, blank, one, col < 3 && custom);
}
Ok(())
}
pub fn encode_info(&self, info: &mut [u8]) -> Result<usize, MicEError> {
let lon = self.longitude.units();
let (deg, min, hund) = split_dmh(lon.unsigned_abs() as i64);
if deg > 179 {
return Err(MicEError::BadLongitude { got: lon });
}
if self.speed > 799 {
return Err(MicEError::BadSpeed { got: self.speed });
}
if self.course > 360 {
return Err(MicEError::BadCourse { got: self.course });
}
check_symbol_table(self.symbol.to_wire().0)?;
if let Some(byte) = self.device_prefix
&& !is_device_prefix(byte)
{
return Err(MicEError::BadDevicePrefix { got: byte });
}
let needed = 9
+ usize::from(self.device_prefix.is_some())
+ if self.altitude.is_some() { 4 } else { 0 }
+ self.status.len();
if info.len() < needed {
return Err(MicEError::BufferTooSmall {
needed,
max: info.len(),
});
}
info[0] = self.fix.type_byte();
let d28 = match deg {
0..=9 => deg + 118,
10..=99 => deg + 28,
100..=109 => deg + 8,
_ => deg - 72,
};
let m28 = if min <= 9 { min + 88 } else { min + 28 };
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
{
info[1] = d28 as u8;
info[2] = m28 as u8;
info[3] = (hund + 28) as u8;
}
let tens = u32::from(self.speed) / 10;
let units = u32::from(self.speed) % 10;
let cs = u32::from(self.course) + 400;
#[allow(clippy::cast_possible_truncation)]
{
info[4] = (tens + if tens < 20 { 108 } else { 28 }) as u8;
info[5] = (units * 10 + cs / 100 + 28) as u8;
info[6] = (cs % 100 + 28) as u8;
}
info[7] = self.symbol.to_wire().1;
info[8] = self.symbol.to_wire().0;
let mut at = 9;
if let Some(byte) = self.device_prefix {
info[at] = byte;
at += 1;
}
if let Some(alt) = self.altitude {
let v = alt
.checked_add(ALTITUDE_OFFSET)
.filter(|v| (0..91 * 91 * 91).contains(v))
.ok_or(MicEError::BadAltitude { got: alt })?;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
{
info[at] = (v / (91 * 91)) as u8 + 33;
info[at + 1] = (v / 91 % 91) as u8 + 33;
info[at + 2] = (v % 91) as u8 + 33;
}
info[at + 3] = b'}';
at += 4;
}
info[at..at + self.status.len()].copy_from_slice(self.status);
Ok(needed)
}
}
const fn check_symbol_table(byte: u8) -> Result<(), MicEError> {
match byte {
b'/' | b'\\' | b'0'..=b'9' | b'A'..=b'Z' => Ok(()),
got => Err(MicEError::BadSymbolTable { got }),
}
}
struct DestCol {
digit: u8,
blank: bool,
one: bool,
custom: Option<bool>,
}
fn dest_col(byte: u8, column: usize) -> Result<DestCol, MicEError> {
let (digit, blank, one, custom) = match byte {
b'0'..=b'9' => (byte - b'0', false, false, None),
b'L' => (0, true, false, None),
b'P'..=b'Y' => (byte - b'P', false, true, Some(false)),
b'Z' => (0, true, true, Some(false)),
b'A'..=b'J' if column < 3 => (byte - b'A', false, true, Some(true)),
b'K' if column < 3 => (0, true, true, Some(true)),
got => return Err(MicEError::BadDestChar { got, column }),
};
Ok(DestCol {
digit,
blank,
one,
custom,
})
}
pub fn decode<'a>(dest: &[u8], info: &'a [u8]) -> Result<MicE<'a>, MicEError> {
if dest.len() != 6 {
return Err(MicEError::BadDestLength { got: dest.len() });
}
let mut digits = [0u8; 6];
let mut bits = 0u8;
let mut custom: Option<bool> = None;
let mut blanks = 0u8;
let mut cols = [false; 6];
for (column, &byte) in dest.iter().enumerate() {
let col = dest_col(byte, column)?;
digits[column] = col.digit;
cols[column] = col.blank;
if col.blank {
blanks += 1;
}
if column < 3 {
if col.one {
bits |= 4 >> column;
}
if let Some(set) = col.custom {
match custom {
Some(prev) if prev != set => {
return Err(MicEError::MixedMessageBits {
got: [dest[0], dest[1], dest[2]],
});
}
_ => custom = Some(set),
}
}
}
}
for column in 1..6 {
if cols[column - 1] && !cols[column] {
return Err(MicEError::NonTrailingAmbiguity { column });
}
}
if blanks > 4 {
return Err(MicEError::BadAmbiguity { got: blanks });
}
let north = dest[3] >= b'P';
let lon_offset = dest[4] >= b'P';
let west = dest[5] >= b'P';
let message = MicEMessage::from_bits(bits, custom.unwrap_or(false));
decode_info(info, digits, north, lon_offset, west, message, blanks)
}
pub fn decode_address<'a>(dest: Address, info: &'a [u8]) -> Result<MicE<'a>, MicEError> {
decode(&dest.callsign.as_padded(), info)
}
fn decode_info<'a>(
info: &'a [u8],
digits: [u8; 6],
north: bool,
lon_offset: bool,
west: bool,
message: MicEMessage,
ambiguity: u8,
) -> Result<MicE<'a>, MicEError> {
if info.len() < 9 {
return Err(MicEError::Truncated {
expected: 9,
got: info.len(),
});
}
let fix = match info[0] {
b'`' => MicEFix::Current,
b'\'' => MicEFix::Old,
got => return Err(MicEError::InvalidDataType { got }),
};
let lat_deg = i64::from(digits[0]) * 10 + i64::from(digits[1]);
let lat_min = i64::from(digits[2]) * 10 + i64::from(digits[3]);
let lat_hund = i64::from(digits[4]) * 10 + i64::from(digits[5]);
let lat_abs = lat_deg * UNITS_PER_DEGREE
+ lat_min * UNITS_PER_MINUTE
+ lat_hund * UNITS_PER_HUNDREDTH_MINUTE;
if lat_deg > 90 || lat_min >= 60 || (lat_deg == 90 && (lat_min != 0 || lat_hund != 0)) {
return Err(MicEError::BadLatitude {
got: if north { lat_abs } else { -lat_abs },
});
}
let latitude = Latitude::new(if north { lat_abs } else { -lat_abs })
.map_err(|_| MicEError::BadLatitude { got: lat_abs })?;
let d = i32::from(info[1]);
#[allow(clippy::cast_lossless)]
let mut lon_deg = i64::from(d) - 28;
if !(0..=99).contains(&lon_deg) {
return Err(MicEError::BadLongitudeByte {
got: info[1],
position: 1,
});
}
if lon_offset {
lon_deg += 100;
}
if (180..=189).contains(&lon_deg) {
lon_deg -= 80;
} else if lon_deg >= 190 {
lon_deg -= 190;
}
let mut lon_min = i64::from(info[2]) - 28;
if !(0..=69).contains(&lon_min) {
return Err(MicEError::BadLongitudeByte {
got: info[2],
position: 2,
});
}
if lon_min >= 60 {
lon_min -= 60;
}
let lon_hund = i64::from(info[3]) - 28;
if !(0..=99).contains(&lon_hund) {
return Err(MicEError::BadLongitudeByte {
got: info[3],
position: 3,
});
}
let lon_abs = lon_deg * UNITS_PER_DEGREE
+ lon_min * UNITS_PER_MINUTE
+ lon_hund * UNITS_PER_HUNDREDTH_MINUTE;
let longitude = Longitude::new(if west { -lon_abs } else { lon_abs })
.map_err(|_| MicEError::BadLongitude { got: lon_abs })?;
let mut sp_dc_se = [0i32; 3];
for (i, v) in sp_dc_se.iter_mut().enumerate() {
let raw = i32::from(info[4 + i]) - 28;
if raw < 0 {
return Err(MicEError::BadSpeedCourseByte {
got: info[4 + i],
position: 4 + i,
});
}
*v = raw;
}
let mut speed = sp_dc_se[0] * 10 + sp_dc_se[1] / 10;
let mut course = (sp_dc_se[1] % 10) * 100 + sp_dc_se[2];
if speed >= 800 {
speed -= 800;
}
if course >= 400 {
course -= 400;
}
if course > 360 {
course = 0;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let (speed, course) = (speed as u16, course as u16);
let tail = split_altitude(&info[9..])?;
Ok(MicE {
latitude,
longitude,
speed,
course,
symbol: Symbol::from_wire(info[8], info[7]),
message,
fix,
altitude: tail.altitude,
device_prefix: tail.device_prefix,
ambiguity,
status: tail.status,
})
}
const fn is_device_prefix(byte: u8) -> bool {
matches!(byte, b'>' | b']' | b'`' | b'\'')
}
const fn has_altitude_shape(bytes: &[u8]) -> bool {
matches!(bytes, [_, _, _, b'}', ..])
}
fn split_altitude(rest: &[u8]) -> Result<AltitudeSplit<'_>, MicEError> {
let (device_prefix, body) = match rest {
[first, tail @ ..] if is_device_prefix(*first) => (Some(*first), tail),
all => (None, all),
};
if device_prefix.is_none() {
if !has_altitude_shape(rest) {
return Ok(AltitudeSplit::all_status(None, rest));
}
let mut value = 0i32;
for (i, &byte) in rest.iter().enumerate().take(3) {
if !(b'!'..=b'{').contains(&byte) {
return Err(MicEError::BadAltitudeChar {
got: byte,
position: 9 + i,
});
}
value = value * 91 + i32::from(byte - b'!');
}
return Ok(AltitudeSplit {
device_prefix: None,
altitude: Some(value - ALTITUDE_OFFSET),
status: &rest[4..],
});
}
if has_altitude_shape(body)
&& let Some(meters) = base91_altitude(body)
{
return Ok(AltitudeSplit {
device_prefix,
altitude: Some(meters),
status: &body[4..],
});
}
if has_altitude_shape(rest)
&& let Some(meters) = base91_altitude(rest)
{
return Ok(AltitudeSplit {
device_prefix: None,
altitude: Some(meters),
status: &rest[4..],
});
}
Ok(AltitudeSplit::all_status(device_prefix, body))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct AltitudeSplit<'a> {
device_prefix: Option<u8>,
altitude: Option<i32>,
status: &'a [u8],
}
impl<'a> AltitudeSplit<'a> {
const fn all_status(device_prefix: Option<u8>, status: &'a [u8]) -> Self {
Self {
device_prefix,
altitude: None,
status,
}
}
}
fn base91_altitude(body: &[u8]) -> Option<i32> {
let mut value = 0i32;
for &byte in body.iter().take(3) {
if !(b'!'..=b'{').contains(&byte) {
return None;
}
value = value * 91 + i32::from(byte - b'!');
}
Some(value - ALTITUDE_OFFSET)
}
const fn dest_char(digit: u8, blank: bool, one: bool, custom: bool) -> u8 {
match (one, blank, custom) {
(false, false, _) => b'0' + digit,
(false, true, _) => b'L',
(true, false, false) => b'P' + digit,
(true, true, false) => b'Z',
(true, false, true) => b'A' + digit,
(true, true, true) => b'K',
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_dmh_known_answers() {
assert_eq!(
split_dmh((33 * 6000 + 2564) * UNITS_PER_HUNDREDTH_MINUTE),
(33, 25, 64)
);
assert_eq!(split_dmh(0), (0, 0, 0));
assert_eq!(
split_dmh(90 * 6000 * UNITS_PER_HUNDREDTH_MINUTE),
(90, 0, 0)
);
assert_eq!(
split_dmh((179 * 6000 + 5999) * UNITS_PER_HUNDREDTH_MINUTE),
(179, 59, 99)
);
}
#[test]
fn dest_char_table() {
assert_eq!(dest_char(3, false, false, false), b'3');
assert_eq!(dest_char(0, true, false, false), b'L');
assert_eq!(dest_char(3, false, true, false), b'S');
assert_eq!(dest_char(0, true, true, false), b'Z');
assert_eq!(dest_char(3, false, true, true), b'D');
assert_eq!(dest_char(0, true, true, true), b'K');
}
#[test]
fn dest_col_inverts_dest_char() {
for byte in [b'0', b'9', b'L', b'P', b'Y', b'Z', b'A', b'J', b'K'] {
let col = match dest_col(byte, 0) {
Ok(c) => c,
Err(e) => panic!("{e}"),
};
assert_eq!(
dest_char(col.digit, col.blank, col.one, col.custom == Some(true)),
byte
);
}
assert!(matches!(
dest_col(b'A', 3),
Err(MicEError::BadDestChar {
got: b'A',
column: 3
})
));
assert!(matches!(
dest_col(b'O', 0),
Err(MicEError::BadDestChar {
got: b'O',
column: 0
})
));
}
#[test]
fn coordinates_pair_the_fields() {
let latitude = match Latitude::new((33 * 6000 + 2564) * UNITS_PER_HUNDREDTH_MINUTE) {
Ok(l) => l,
Err(e) => panic!("{e}"),
};
let longitude = match Longitude::new(-(112 * 6000 + 1229) * UNITS_PER_HUNDREDTH_MINUTE) {
Ok(l) => l,
Err(e) => panic!("{e}"),
};
let report = match MicE::new(
latitude,
longitude,
20,
251,
Symbol::from_wire(b'/', b'>'),
MicEMessage::EnRoute,
) {
Ok(r) => r,
Err(e) => panic!("{e}"),
};
assert_eq!(report.coordinates(), Coordinates::new(latitude, longitude));
assert_eq!(report.coordinates().latitude, report.latitude);
assert_eq!(report.coordinates().longitude, report.longitude);
}
#[test]
fn message_bits_round_trip() {
let all = [
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,
];
for msg in all {
let (bits, custom) = msg.bits();
assert_eq!(MicEMessage::from_bits(bits, custom), msg);
}
assert_eq!(MicEMessage::from_bits(0, true), MicEMessage::Emergency);
assert_eq!(MicEMessage::from_bits(0, false), MicEMessage::Emergency);
}
#[test]
fn altitude_splitter() {
let rest = b"\"4T}Hello";
let split = match split_altitude(rest) {
Ok(v) => v,
Err(e) => panic!("{e}"),
};
assert_eq!(split.device_prefix, None);
assert_eq!(split.altitude, Some(61));
assert_eq!(split.status, b"Hello");
for prefix in [b'>', b']', b'`', b'\''] {
let mut rest = [0u8; 10];
rest[0] = prefix;
rest[1..10].copy_from_slice(b"\"4T}Hello");
assert_eq!(
split_altitude(&rest),
Ok(AltitudeSplit {
device_prefix: Some(prefix),
altitude: Some(61),
status: b"Hello",
}),
"altitude behind prefix {:?}",
prefix as char
);
}
for text in [
&b"just text"[..],
&b"ab"[..],
&b""[..],
] {
assert_eq!(
split_altitude(text),
Ok(AltitudeSplit::all_status(None, text)),
"{:?} carries neither prefix nor altitude",
core::str::from_utf8(text).unwrap_or("<binary>")
);
}
for (text, prefix, status) in [
(&b">Hello there"[..], b'>', &b"Hello there"[..]),
(&b">\x7fzz}tail"[..], b'>', &b"\x7fzz}tail"[..]),
(&b">"[..], b'>', &b""[..]),
(&b"]"[..], b']', &b""[..]),
(&b"]Stopped\r"[..], b']', &b"Stopped\r"[..]),
] {
assert_eq!(
split_altitude(text),
Ok(AltitudeSplit::all_status(Some(prefix), status)),
"{:?} is a prefix with no altitude",
core::str::from_utf8(text).unwrap_or("<binary>")
);
}
assert_eq!(
split_altitude(b"]^]}up"),
Ok(AltitudeSplit {
device_prefix: None,
altitude: Some(492_471),
status: b"up",
})
);
assert!(matches!(
split_altitude(b"\x7fzz}"),
Err(MicEError::BadAltitudeChar { got: 0x7f, .. })
));
}
#[test]
fn device_prefixed_altitude_round_trips_byte_exactly() {
let dest = *b"S32U6T";
for prefix in [b'>', b']', b'`', b'\''] {
let mut info = [0u8; 32];
info[..9].copy_from_slice(b"`(_fn\"Oj/");
info[9] = prefix;
info[10..19].copy_from_slice(b"\"4T}Hello");
let report = decode(&dest, &info[..19]).expect("decode");
assert_eq!(report.device_prefix, Some(prefix));
assert_eq!(report.altitude, Some(61));
assert_eq!(report.status, b"Hello");
let mut out = [0u8; 32];
let len = report.encode_info(&mut out).expect("encode");
assert_eq!(&out[..len], &info[..19], "prefix {:?}", prefix as char);
}
}
#[test]
fn encode_rejects_an_invented_device_prefix() {
let dest = *b"S32U6T";
let mut info = [0u8; 32];
info[..9].copy_from_slice(b"`(_fn\"Oj/");
let mut report = decode(&dest, &info[..9]).expect("decode");
report.device_prefix = Some(b'X');
assert_eq!(
report.encode_info(&mut [0u8; 32]),
Err(MicEError::BadDevicePrefix { got: b'X' })
);
assert_eq!(
report.with_device_prefix(Some(b'X')),
Err(MicEError::BadDevicePrefix { got: b'X' })
);
assert!(report.with_device_prefix(Some(b']')).is_ok());
assert!(report.with_device_prefix(None).is_ok());
}
#[test]
fn symbol_table_check() {
for ok in [b'/', b'\\', b'0', b'9', b'A', b'Z'] {
assert_eq!(check_symbol_table(ok), Ok(()));
}
for bad in [b' ', b'a', b'~'] {
assert_eq!(
check_symbol_table(bad),
Err(MicEError::BadSymbolTable { got: bad })
);
}
}
}