use std::collections::VecDeque;
use std::fmt::{Arguments, Write};
use crate::chip::Timeline;
use crate::chip::uart::Peer;
use crate::snapshot::store;
use crate::time::VirtualNanos;
const PERIOD: VirtualNanos = VirtualNanos(200_000_000);
const EPOCH_YEAR: u64 = 2026;
const NANOS_PER_SECOND: u64 = 1_000_000_000;
const SECONDS_PER_DAY: u64 = 86_400;
const MICRODEGREES_TENTHS: u64 = 10_000_000;
const HUNDREDTHS_OF_A_KNOT_PER_MILLIMETER_SECOND: (u64, u64) = (1_943_844, 10_000_000);
const HUNDREDTHS_OF_A_KILOMETER_HOUR_PER_MILLIMETER_SECOND: (u64, u64) = (36, 100);
const SATELLITES: u32 = 10;
const HDOP: &str = "0.90";
const EPOCH_BYTES: usize = 256;
const CENTIDEGREES_PER_MICRORADIAN: (i64, i64) = (18_000, 3_141_593);
const SCALE: i64 = 1_000_000;
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
)]
pub struct Position {
pub latitude: i32,
pub longitude: i32,
pub altitude: i32,
}
impl crate::link::Wire for Position {
fn encode(&self, out: &mut Vec<u8>) {
self.latitude.encode(out);
self.longitude.encode(out);
self.altitude.encode(out);
}
fn decode(bytes: &mut &[u8]) -> Result<Self, crate::link::WireError> {
Ok(Self {
latitude: crate::link::Wire::decode(bytes)?,
longitude: crate::link::Wire::decode(bytes)?,
altitude: crate::link::Wire::decode(bytes)?,
})
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Receiver {
next: VirtualNanos,
position: Position,
velocity: [i32; 3],
text: String,
pending: VecDeque<u8>,
}
impl Receiver {
#[must_use]
pub fn new() -> Self {
Self {
next: VirtualNanos(0),
position: Position::default(),
velocity: [0; 3],
text: String::with_capacity(EPOCH_BYTES),
pending: VecDeque::with_capacity(EPOCH_BYTES),
}
}
pub fn set_position(&mut self, position: Position) {
self.position = position;
}
pub fn set_velocity(&mut self, velocity: [i32; 3]) {
self.velocity = velocity;
}
fn write_epoch(&self, now: VirtualNanos, into: &mut String) {
let seconds = now.0 / NANOS_PER_SECOND;
let hundredths = (now.0 / (NANOS_PER_SECOND / 100)) % 100;
let (year, month, day) = calendar(seconds / SECONDS_PER_DAY);
let in_day = seconds % SECONDS_PER_DAY;
let (hours, minutes, seconds) = (in_day / 3600, in_day % 3600 / 60, in_day % 60);
let [north, east, _] = self.velocity;
let ground = (u64::from(north.unsigned_abs()).pow(2)
+ u64::from(east.unsigned_abs()).pow(2))
.isqrt();
let knots = hundredths_of(ground, HUNDREDTHS_OF_A_KNOT_PER_MILLIMETER_SECOND);
let kilometers =
hundredths_of(ground, HUNDREDTHS_OF_A_KILOMETER_HOUR_PER_MILLIMETER_SECOND);
let course = course(i64::from(north), i64::from(east));
let altitude = self.position.altitude;
let time = |into: &mut String| {
put(
into,
format_args!("{hours:02}{minutes:02}{seconds:02}.{hundredths:02}"),
);
};
let fix = |into: &mut String| {
angle(into, self.position.latitude, 2, ['N', 'S']);
into.push(',');
angle(into, self.position.longitude, 3, ['E', 'W']);
};
sentence(into, |into| {
into.push_str("GPGGA,");
time(into);
into.push(',');
fix(into);
put(
into,
format_args!(
",1,{SATELLITES:02},{HDOP},{}{}.{:03},M,0.0,M,,",
if altitude < 0 { "-" } else { "" },
altitude.unsigned_abs() / 1000,
altitude.unsigned_abs() % 1000
),
);
});
sentence(into, |into| {
into.push_str("GPRMC,");
time(into);
into.push_str(",A,");
fix(into);
put(
into,
format_args!(
",{}.{:02},{}.{:02},{day:02}{month:02}{:02},,,A",
knots / 100,
knots % 100,
course / 100,
course % 100,
year % 100
),
);
});
sentence(into, |into| {
put(
into,
format_args!(
"GPVTG,{}.{:02},T,,M,{}.{:02},N,{}.{:02},K,A",
course / 100,
course % 100,
knots / 100,
knots % 100,
kilometers / 100,
kilometers % 100
),
);
});
}
}
impl Default for Receiver {
fn default() -> Self {
Self::new()
}
}
store!(Receiver);
impl Peer for Receiver {
fn receive(&mut self, _word: u16, _context: &mut Timeline<'_, '_>) {}
fn transmit(&mut self, now: VirtualNanos) -> Option<u16> {
if self.pending.is_empty() && now >= self.next {
let due = VirtualNanos(now.0 - (now.0 - self.next.0) % PERIOD.0);
let mut text = std::mem::take(&mut self.text);
text.clear();
self.write_epoch(due, &mut text);
self.pending.extend(text.bytes());
self.text = text;
self.next = VirtualNanos(due.0 + PERIOD.0);
}
self.pending.pop_front().map(u16::from)
}
}
fn put(into: &mut String, text: Arguments<'_>) {
into.write_fmt(text)
.expect("a string takes what is written");
}
fn sentence(into: &mut String, body: impl FnOnce(&mut String)) {
into.push('$');
let start = into.len();
body(into);
let checksum = into.as_bytes()[start..]
.iter()
.fold(0, |sum, byte| sum ^ byte);
put(into, format_args!("*{checksum:02X}\r\n"));
}
fn angle(into: &mut String, tenths_of_a_microdegree: i32, digits: usize, hemispheres: [char; 2]) {
let whole = u64::from(tenths_of_a_microdegree.unsigned_abs());
let degrees = whole / MICRODEGREES_TENTHS;
let minutes = (whole % MICRODEGREES_TENTHS) * 60 / 100;
let hemisphere = hemispheres[usize::from(tenths_of_a_microdegree < 0)];
put(
into,
format_args!(
"{degrees:0digits$}{:02}.{:05},{hemisphere}",
minutes / 100_000,
minutes % 100_000
),
);
}
fn hundredths_of(millimeters_a_second: u64, ratio: (u64, u64)) -> u64 {
millimeters_a_second * ratio.0 / ratio.1
}
fn course(north: i64, east: i64) -> u32 {
if north == 0 && east == 0 {
return 0;
}
let (along, across) = (north.unsigned_abs(), east.unsigned_abs());
let from_north = if across <= along {
arctangent(across, along)
} else {
9_000 - arctangent(along, across)
};
let course = match (north >= 0, east >= 0) {
(true, true) => from_north,
(false, true) => 18_000 - from_north,
(false, false) => 18_000 + from_north,
(true, false) => 36_000 - from_north,
};
course % 36_000
}
fn arctangent(rise: u64, run: u64) -> u32 {
let x = i64::try_from(rise).expect("a speed") * SCALE / i64::try_from(run).expect("a speed");
let quarter_pi = 785_398;
let microradians =
quarter_pi * x / SCALE - x * (x - SCALE) / SCALE * (244_700 + 66_300 * x / SCALE) / SCALE;
u32::try_from(microradians * CENTIDEGREES_PER_MICRORADIAN.0 / CENTIDEGREES_PER_MICRORADIAN.1)
.expect("an angle within the octant")
}
fn calendar(days: u64) -> (u64, u64, u64) {
let leap = |year: u64| {
year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
};
let (mut year, mut left) = (EPOCH_YEAR, days);
loop {
let in_year = if leap(year) { 366 } else { 365 };
if left < in_year {
break;
}
left -= in_year;
year += 1;
}
let lengths = [
31,
if leap(year) { 29 } else { 28 },
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let mut month = 0;
while left >= lengths[month] {
left -= lengths[month];
month += 1;
}
(
year,
u64::try_from(month).expect("twelve months") + 1,
left + 1,
)
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
const KANSAS: Position = Position {
latitude: 398_283_000,
longitude: -985_795_000,
altitude: 500_000,
};
fn written(receiver: &Receiver, now: VirtualNanos) -> String {
let mut text = String::new();
receiver.write_epoch(now, &mut text);
text
}
fn at_kansas() -> Receiver {
let mut receiver = Receiver::new();
receiver.set_position(KANSAS);
receiver
}
fn lines(epoch: &str) -> Vec<&str> {
epoch
.split("\r\n")
.filter(|line| !line.is_empty())
.collect()
}
#[test]
fn each_epoch_is_three_sentences_with_their_checksums() {
let epoch = written(&at_kansas(), VirtualNanos(0));
let lines = lines(&epoch);
assert_eq!(lines.len(), 3);
for (line, name) in lines.iter().zip(["$GPGGA", "$GPRMC", "$GPVTG"]) {
assert!(line.starts_with(name), "{line}");
let (body, checksum) = line
.strip_prefix('$')
.and_then(|rest| rest.split_once('*'))
.expect("a checksum");
let expected = body.bytes().fold(0, |sum, byte| sum ^ byte);
assert_eq!(checksum, format!("{expected:02X}"), "{line}");
}
assert!(epoch.ends_with("\r\n"));
}
#[test]
fn the_position_is_written_in_degrees_and_minutes_with_its_hemisphere() {
let epoch = written(&at_kansas(), VirtualNanos(0));
assert_eq!(
lines(&epoch)[0],
"$GPGGA,000000.00,3949.69800,N,09834.77000,W,1,10,0.90,500.000,M,0.0,M,,*75"
);
let mut southern = Receiver::new();
southern.set_position(Position {
latitude: -338_688_000,
longitude: 1_512_093_000,
altitude: -2_500,
});
assert!(
lines(&written(&southern, VirtualNanos(0)))[0]
.contains(",3352.12800,S,15112.55800,E,1,10,0.90,-2.500,M,")
);
}
#[test]
fn the_time_and_date_count_from_the_epoch() {
let receiver = at_kansas();
let rmc = |nanos| lines(&written(&receiver, VirtualNanos(nanos)))[1].to_owned();
assert!(rmc(0).starts_with("$GPRMC,000000.00,A,"));
assert!(rmc(0).contains(",010126,,,A*"));
let later = 45 * SECONDS_PER_DAY * NANOS_PER_SECOND + 3_723_450_000_000;
assert!(
rmc(later).starts_with("$GPRMC,010203.45,A,"),
"{}",
rmc(later)
);
assert!(rmc(later).contains(",150226,,,A*"), "{}", rmc(later));
let leap_day = (365 + 31 + 28) * SECONDS_PER_DAY * NANOS_PER_SECOND;
assert!(
rmc(leap_day).contains(",010327,,,A*"),
"2027 is not a leap year, so the first of March"
);
let far = (365 * 2 + 31 + 28) * SECONDS_PER_DAY * NANOS_PER_SECOND;
assert!(rmc(far).contains(",290228,,,A*"), "2028 is");
let new_year = 365 * SECONDS_PER_DAY * NANOS_PER_SECOND;
assert!(rmc(new_year).contains(",010127,,,A*"), "{}", rmc(new_year));
let century = (74 * 365 + 18 + 31 + 28) * SECONDS_PER_DAY * NANOS_PER_SECOND;
assert!(
rmc(century).contains(",010300,,,A*"),
"2100 is not a leap year: {}",
rmc(century)
);
}
#[test]
fn the_speed_and_course_come_off_the_horizontal_velocity() {
let mut receiver = at_kansas();
for (velocity, knots, kilometers, course) in [
([0, 0, 0], "0.00", "0.00", 0),
([10_000, 0, -3_000], "19.43", "36.00", 0),
([0, 10_000, 0], "19.43", "36.00", 9_000),
([-3_000, -4_000, 0], "9.71", "18.00", 23_313),
([3_000, -3_000, 0], "8.24", "15.27", 31_500),
] {
receiver.set_velocity(velocity);
let epoch = written(&receiver, VirtualNanos(0));
let rmc: Vec<&str> = lines(&epoch)[1].split(',').collect();
let vtg: Vec<&str> = lines(&epoch)[2].split(',').collect();
assert_eq!(rmc[7], knots, "{velocity:?}");
assert_eq!(vtg[5], knots, "{velocity:?}");
assert_eq!(vtg[7], kilometers, "{velocity:?}");
let written = |field: &str| {
let (whole, fraction) = field.split_once('.').expect("two places");
whole.parse::<i64>().unwrap() * 100 + fraction.parse::<i64>().unwrap()
};
assert_eq!(rmc[8], vtg[1]);
assert!(
(written(rmc[8]) - course).abs() <= 10,
"{velocity:?}: {} against {course}",
rmc[8]
);
}
assert!(course(1000, 1000).abs_diff(4500) <= 10);
assert!(course(1000, 577).abs_diff(3000) <= 10, "thirty degrees");
assert!(
course(-577, 1000).abs_diff(12000) <= 10,
"a hundred and twenty"
);
}
#[test]
fn sentences_come_at_the_rate_on_the_receivers_own_grid() {
let mut receiver = at_kansas();
let drain = |receiver: &mut Receiver, now| {
std::iter::from_fn(|| {
receiver
.transmit(now)
.map(|word| u8::try_from(word).expect("a byte"))
})
.take(4096)
.collect::<Vec<u8>>()
};
let first = drain(&mut receiver, VirtualNanos(0));
assert_eq!(first, written(&receiver, VirtualNanos(0)).into_bytes());
assert!(drain(&mut receiver, VirtualNanos(199_999_999)).is_empty());
let second = drain(&mut receiver, VirtualNanos(200_000_000));
assert!(
String::from_utf8(second)
.unwrap()
.starts_with("$GPGGA,000000.20,")
);
assert_eq!(
receiver.transmit(VirtualNanos(400_000_000)),
Some(u16::from(b'$'))
);
for (later, byte) in [(700_000_000, b'G'), (1_000_000_000, b'P')] {
assert_eq!(
receiver.transmit(VirtualNanos(later)),
Some(u16::from(byte)),
"the epoch in flight goes on"
);
}
let rest = String::from_utf8(drain(&mut receiver, VirtualNanos(1_300_000_000))).unwrap();
assert!(rest.starts_with("GGA,000000.40,"), "{rest}");
assert!(
rest.contains("$GPGGA,000001.20,")
&& !rest.contains("000001.30")
&& !rest.contains("000000.70")
&& !rest.contains("000001.00"),
"the next epoch is written when the last has gone, once, stamped when it was due: {rest}"
);
}
fn parsed(field: &str) -> i64 {
let (whole, fraction) = field.split_once('.').expect("five places");
let (degrees, minutes) = whole.split_at(whole.len() - 2);
let fraction = fraction.parse::<i64>().expect("digits") * 10_000_000 / 100_000;
degrees.parse::<i64>().expect("digits") * 10_000_000
+ minutes.parse::<i64>().expect("digits") * 10_000_000 / 60
+ fraction / 60
}
proptest! {
#[test]
fn an_angle_reads_back_to_within_a_ten_millionth_of_a_degree(
tenths in -1_800_000_000_i32..=1_800_000_000,
) {
let mut written = String::new();
angle(&mut written, tenths, 3, ['E', 'W']);
let (field, hemisphere) = written.split_once(',').expect("a hemisphere");
let sign = if hemisphere == "W" { -1 } else { 1 };
prop_assert!((sign * parsed(field) - i64::from(tenths)).abs() <= 2, "{written}");
}
#[test]
#[expect(
clippy::disallowed_methods,
reason = "the oracle of a test, which nothing in a run computes"
)]
fn the_course_is_within_a_tenth_of_a_degree_of_the_arctangent(
north in i32::MIN..=i32::MAX,
east in i32::MIN..=i32::MAX,
) {
let expected = f64::from(east).atan2(f64::from(north)).to_degrees().rem_euclid(360.0);
let written = f64::from(course(i64::from(north), i64::from(east))) / 100.0;
let apart = (written - expected).abs().min(360.0 - (written - expected).abs());
prop_assert!(apart <= 0.1, "{north} {east}: {written} against {expected}");
}
}
}