use super::AprsError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OverlayId(u8);
impl OverlayId {
pub const fn new(byte: u8) -> Result<Self, AprsError> {
match byte {
b'0'..=b'9' | b'A'..=b'Z' => Ok(Self(byte)),
_ => Err(AprsError::BadOverlay { got: byte }),
}
}
#[must_use]
pub const fn get(self) -> u8 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolTable {
Primary,
Alternate,
Overlay(OverlayId),
}
impl SymbolTable {
#[must_use]
pub const fn to_wire(self) -> u8 {
match self {
SymbolTable::Primary => b'/',
SymbolTable::Alternate => b'\\',
SymbolTable::Overlay(id) => id.get(),
}
}
#[must_use]
pub const fn from_wire(byte: u8) -> Option<Self> {
match byte {
b'/' => Some(SymbolTable::Primary),
b'\\' => Some(SymbolTable::Alternate),
b'0'..=b'9' | b'A'..=b'Z' => match OverlayId::new(byte) {
Ok(id) => Some(SymbolTable::Overlay(id)),
Err(_) => None,
},
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SymbolCode(u8);
impl SymbolCode {
pub const fn new(byte: u8) -> Result<Self, AprsError> {
match byte {
0x21..=0x7E => Ok(Self(byte)),
_ => Err(AprsError::BadSymbolCode { got: byte }),
}
}
#[must_use]
pub const fn as_byte(self) -> u8 {
self.0
}
#[must_use]
pub const fn as_char(self) -> char {
self.0 as char
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SymbolRepr {
Valid {
table: SymbolTable,
code: SymbolCode,
},
Raw {
table: u8,
code: u8,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Symbol {
repr: SymbolRepr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolDescription {
Known(&'static str),
Unknown,
}
const fn standard(table: SymbolTable, code: u8) -> Symbol {
Symbol {
repr: SymbolRepr::Valid {
table,
code: SymbolCode(code),
},
}
}
impl Symbol {
pub const CAR: Self = standard(SymbolTable::Primary, b'>');
pub const HOUSE: Self = standard(SymbolTable::Primary, b'-');
pub const JOGGER: Self = standard(SymbolTable::Primary, b'[');
pub const BICYCLE: Self = standard(SymbolTable::Primary, b'b');
pub const MOTORCYCLE: Self = standard(SymbolTable::Primary, b'<');
pub const TRUCK: Self = standard(SymbolTable::Primary, b'k');
pub const BUS: Self = standard(SymbolTable::Primary, b'U');
pub const BOAT: Self = standard(SymbolTable::Primary, b'Y');
pub const BALLOON: Self = standard(SymbolTable::Primary, b'O');
pub const AIRCRAFT: Self = standard(SymbolTable::Primary, b'\'');
pub const HELICOPTER: Self = standard(SymbolTable::Primary, b'X');
pub const DIGI: Self = standard(SymbolTable::Primary, b'#');
pub const IGATE: Self = standard(SymbolTable::Primary, b'&');
pub const WEATHER_STATION: Self = standard(SymbolTable::Primary, b'_');
pub const DOT: Self = standard(SymbolTable::Primary, b'/');
pub const CAMPGROUND: Self = standard(SymbolTable::Primary, b';');
pub const TENT: Self = Self::CAMPGROUND;
pub const AMBULANCE: Self = standard(SymbolTable::Primary, b'a');
pub const FIRE_STATION: Self = standard(SymbolTable::Primary, b':');
pub const POLICE: Self = standard(SymbolTable::Primary, b'!');
pub const PHONE: Self = standard(SymbolTable::Primary, b'$');
pub const SATELLITE: Self = standard(SymbolTable::Alternate, b'S');
pub const RED_CROSS: Self = standard(SymbolTable::Primary, b'+');
#[must_use]
pub const fn new(table: SymbolTable, code: SymbolCode) -> Self {
Symbol {
repr: SymbolRepr::Valid { table, code },
}
}
#[must_use]
pub const fn primary(code: SymbolCode) -> Self {
Self::new(SymbolTable::Primary, code)
}
#[must_use]
pub const fn alternate(code: SymbolCode) -> Self {
Self::new(SymbolTable::Alternate, code)
}
#[must_use]
pub const fn overlay(id: OverlayId, code: SymbolCode) -> Self {
Self::new(SymbolTable::Overlay(id), code)
}
#[must_use]
pub const fn from_wire(table: u8, code: u8) -> Self {
match (SymbolTable::from_wire(table), SymbolCode::new(code)) {
(Some(t), Ok(c)) => Self::new(t, c),
(Some(_), Err(_)) | (None, Ok(_)) | (None, Err(_)) => Symbol {
repr: SymbolRepr::Raw { table, code },
},
}
}
#[must_use]
pub const fn to_wire(self) -> (u8, u8) {
match self.repr {
SymbolRepr::Valid { table, code } => (table.to_wire(), code.as_byte()),
SymbolRepr::Raw { table, code } => (table, code),
}
}
#[must_use]
pub const fn table(self) -> Option<SymbolTable> {
match self.repr {
SymbolRepr::Valid { table, code: _ } => Some(table),
SymbolRepr::Raw { .. } => None,
}
}
#[must_use]
pub const fn code(self) -> Option<SymbolCode> {
match self.repr {
SymbolRepr::Valid { table: _, code } => Some(code),
SymbolRepr::Raw { .. } => None,
}
}
#[must_use]
pub const fn describe(self) -> SymbolDescription {
match self.repr {
SymbolRepr::Valid { table, code } => {
let alternate = match table {
SymbolTable::Primary => false,
SymbolTable::Alternate | SymbolTable::Overlay(_) => true,
};
describe_glyph(alternate, code.as_byte())
}
SymbolRepr::Raw { .. } => SymbolDescription::Unknown,
}
}
}
const fn mnemonic(x: u8, y: u8) -> Option<(bool, u8)> {
let (alternate, first_code, first_y, len) = match x {
b'B' => (false, b'!', b'B', 15),
b'O' => (true, b'!', b'B', 15),
b'M' => (false, b':', b'R', 7),
b'N' => (true, b':', b'R', 7),
b'H' => (false, b'[', b'S', 6),
b'D' => (true, b'[', b'S', 6),
b'L' => (false, b'a', b'A', 26),
b'S' => (true, b'a', b'A', 26),
b'J' => (false, b'{', b'1', 4),
b'Q' => (true, b'{', b'1', 4),
b'P' | b'A' => {
return match y {
b'0'..=b'9' | b'A'..=b'Z' => Some((x == b'A', y)),
_ => None,
};
}
_ => return None,
};
if y < first_y {
return None;
}
let offset = y - first_y;
if offset >= len {
return None;
}
Some((alternate, first_code + offset))
}
#[must_use]
pub const fn from_destination(callsign: &[u8]) -> Option<Symbol> {
let mut len = callsign.len();
while len > 0 && callsign[len - 1] == b' ' {
len -= 1;
}
if len < 5 || len > 6 {
return None;
}
let generic = matches!(
(callsign[0], callsign[1], callsign[2]),
(b'G', b'P', b'S') | (b'S', b'P', b'C') | (b'S', b'Y', b'M')
);
if !generic {
return None;
}
if len == 6 && callsign[0] == b'G' {
let numeric_table = match callsign[3] {
b'C' => Some(SymbolTable::Primary),
b'E' => Some(SymbolTable::Alternate),
_ => None,
};
if let Some(table) = numeric_table {
let (tens, ones) = (callsign[4], callsign[5]);
if tens.is_ascii_digit() && ones.is_ascii_digit() {
let nn = (tens - b'0') * 10 + (ones - b'0');
if nn >= 1 && nn <= 94 {
return Some(standard(table, b'!' + (nn - 1)));
}
}
return None;
}
}
let (alternate, code) = match mnemonic(callsign[3], callsign[4]) {
Some(pair) => pair,
None => return None,
};
let z = if len == 6 { callsign[5] } else { b' ' };
let table = if alternate {
match OverlayId::new(z) {
Ok(id) => SymbolTable::Overlay(id),
Err(_) if z == b' ' => SymbolTable::Alternate,
Err(_) => return None,
}
} else if z == b' ' {
SymbolTable::Primary
} else {
return None;
};
Some(standard(table, code))
}
#[must_use]
pub const fn from_source_ssid(ssid: u8) -> Option<Symbol> {
const CODES: [u8; 16] = [
b' ', b'a', b'U', b'f', b'b', b'Y', b'X', b'\'', b's', b'>', b'<', b'O', b'j', b'R', b'k', b'v', ];
if ssid == 0 || ssid > 15 {
return None;
}
Some(standard(SymbolTable::Primary, CODES[ssid as usize]))
}
#[must_use]
pub const fn resolve(
information: Option<Symbol>,
destination: &[u8],
source_ssid: u8,
) -> Option<Symbol> {
match information {
Some(symbol) => Some(symbol),
None => match from_destination(destination) {
Some(symbol) => Some(symbol),
None => from_source_ssid(source_ssid),
},
}
}
const fn describe_glyph(alternate: bool, code: u8) -> SymbolDescription {
let known = if alternate {
match code {
b'#' => "Digipeater (alternate)",
b'&' => "Gateway / igate",
b'S' => "Satellite",
b'_' => "Weather site",
_ => return SymbolDescription::Unknown,
}
} else {
match code {
b'!' => "Police station",
b'#' => "Digipeater",
b'$' => "Telephone",
b'&' => "Gateway station (HF gateway / igate)",
b'\'' => "Small aircraft",
b'(' => "Mobile satellite station",
b'+' => "Red cross",
b'-' => "House",
b'/' => "Dot",
b':' => "Fire",
b';' => "Campground / tent",
b'<' => "Motorcycle",
b'=' => "Railroad engine",
b'>' => "Car",
b'A' => "Aid station",
b'K' => "School",
b'O' => "Balloon",
b'R' => "Recreational vehicle",
b'U' => "Bus",
b'X' => "Helicopter",
b'Y' => "Sailboat",
b'[' => "Jogger",
b'_' => "Weather station",
b'a' => "Ambulance",
b'b' => "Bicycle",
b'f' => "Fire truck",
b'h' => "Hospital",
b'j' => "Jeep",
b'k' => "Truck",
b'r' => "Repeater tower",
b's' => "Power boat / ship",
b'u' => "Semi-trailer truck",
b'v' => "Van",
_ => return SymbolDescription::Unknown,
}
};
SymbolDescription::Known(known)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_wire_to_wire_round_trips_every_pair() {
for table in 0..=u8::MAX {
for code in 0..=u8::MAX {
let sym = Symbol::from_wire(table, code);
assert_eq!(sym.to_wire(), (table, code));
}
}
}
#[test]
fn overlay_validation_edges() {
assert!(OverlayId::new(b'A').is_ok());
assert!(OverlayId::new(b'Z').is_ok());
assert!(OverlayId::new(b'0').is_ok());
assert!(OverlayId::new(b'9').is_ok());
for byte in [
b'A' - 1,
b'Z' + 1,
b'0' - 1,
b'9' + 1,
b'a',
b'z',
b'/',
b'\\',
0x00,
0xFF,
] {
assert_eq!(
OverlayId::new(byte),
Err(AprsError::BadOverlay { got: byte })
);
}
}
#[test]
fn symbol_code_validation_edges() {
assert!(SymbolCode::new(0x21).is_ok());
assert!(SymbolCode::new(0x7E).is_ok());
for byte in [0x20, 0x7F, 0x00, 0xFF] {
assert_eq!(
SymbolCode::new(byte),
Err(AprsError::BadSymbolCode { got: byte })
);
}
}
#[test]
fn table_wire_round_trip() {
for byte in 0..=u8::MAX {
if let Some(table) = SymbolTable::from_wire(byte) {
assert_eq!(table.to_wire(), byte);
}
}
assert_eq!(SymbolTable::from_wire(b'/'), Some(SymbolTable::Primary));
assert_eq!(SymbolTable::from_wire(b'\\'), Some(SymbolTable::Alternate));
assert_eq!(SymbolTable::from_wire(b'a'), None);
}
#[test]
fn every_named_constant_is_standard_and_known() {
let constants = [
Symbol::CAR,
Symbol::HOUSE,
Symbol::JOGGER,
Symbol::BICYCLE,
Symbol::MOTORCYCLE,
Symbol::TRUCK,
Symbol::BUS,
Symbol::BOAT,
Symbol::BALLOON,
Symbol::AIRCRAFT,
Symbol::HELICOPTER,
Symbol::DIGI,
Symbol::IGATE,
Symbol::WEATHER_STATION,
Symbol::DOT,
Symbol::CAMPGROUND,
Symbol::TENT,
Symbol::AMBULANCE,
Symbol::FIRE_STATION,
Symbol::POLICE,
Symbol::PHONE,
Symbol::SATELLITE,
Symbol::RED_CROSS,
];
for sym in constants {
assert!(sym.table().is_some(), "constant is not standard: {sym:?}");
assert!(sym.code().is_some(), "constant is not standard: {sym:?}");
assert!(
matches!(sym.describe(), SymbolDescription::Known(_)),
"constant is not Known: {sym:?}"
);
let (t, c) = sym.to_wire();
assert_eq!(Symbol::from_wire(t, c), sym);
}
assert_eq!(Symbol::CAR.to_wire(), (b'/', b'>'));
assert_eq!(Symbol::HOUSE.to_wire(), (b'/', b'-'));
assert_eq!(Symbol::DIGI.to_wire(), (b'/', b'#'));
assert_eq!(Symbol::WEATHER_STATION.to_wire(), (b'/', b'_'));
}
#[test]
fn describe_is_total() {
for table in 0..=u8::MAX {
for code in [0u8, b' ', b'!', b'>', b'~', 0x7F, 0xFF] {
let d = Symbol::from_wire(table, code).describe();
match d {
SymbolDescription::Known(s) => assert!(!s.is_empty()),
SymbolDescription::Unknown => {}
}
}
}
assert_eq!(
Symbol::from_wire(0x01, 0xFF).describe(),
SymbolDescription::Unknown
);
assert_eq!(
Symbol::from_wire(b'/', b'~').describe(),
SymbolDescription::Unknown
);
assert_eq!(
Symbol::from_wire(b'W', b'#').describe(),
SymbolDescription::Known("Digipeater (alternate)")
);
}
#[test]
fn accessors_expose_typed_parts() {
let sym = Symbol::from_wire(b'3', b'>');
match sym.table() {
Some(SymbolTable::Overlay(id)) => assert_eq!(id.get(), b'3'),
Some(SymbolTable::Primary) | Some(SymbolTable::Alternate) | None => {
panic!("expected an overlay table")
}
}
let code = sym.code();
assert!(code.is_some());
if let Some(c) = code {
assert_eq!(c.as_byte(), b'>');
assert_eq!(c.as_char(), '>');
}
let raw = Symbol::from_wire(0x00, b'>');
assert_eq!(raw.table(), None);
assert_eq!(raw.code(), None);
}
}