use std::str::FromStr;
use std::sync::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::engine::decode::{DecodedField, FieldValue};
pub const GPS_ROLLOVER_DAYS: u16 = 7168;
const MAX_DATE_DAY: u32 = 0xfffc;
const MIN_REFERENCE_DAY: u16 = 20454;
const MAX_DEVICE_ADDRESS: u8 = 251;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Device {
Address(u8),
Product { manufacturer: u16, unique: u32 },
Name(u64),
}
impl FromStr for Device {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
return Err("empty device".to_string());
}
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
return match u64::from_str_radix(hex, 16) {
Ok(0) => Err(format!("'{s}' is not a device's ISO NAME")),
Ok(name) => Ok(Device::Name(name)),
Err(_) => Err(format!("'{s}' is not a hexadecimal ISO NAME")),
};
}
if let Some((m, u)) = s.split_once(':') {
let manufacturer: u16 = m
.trim()
.parse()
.ok()
.filter(|&v| v < (1 << 11))
.ok_or_else(|| format!("'{m}' is not a manufacturer code (0-2047)"))?;
let unique: u32 = u
.trim()
.parse()
.ok()
.filter(|&v| v < (1 << 21))
.ok_or_else(|| format!("'{u}' is not a unique number (0-2097151)"))?;
return Ok(Device::Product {
manufacturer,
unique,
});
}
match s.parse::<u64>() {
Ok(a) if a <= u64::from(MAX_DEVICE_ADDRESS) => Ok(Device::Address(a as u8)),
Ok(_) => Err(format!(
"'{s}' is not a source address (0-{MAX_DEVICE_ADDRESS}); to name a device by \
its NAME use <manufacturer>:<unique number> or 0x<hex NAME>"
)),
Err(_) => Err(format!(
"'{s}' is not a device: use a source address, <manufacturer>:<unique \
number>, 0x<hex NAME>, or all"
)),
}
}
}
impl Device {
fn matches(self, src: u8, name: Option<u64>) -> bool {
match self {
Device::Address(a) => a == src,
Device::Name(n) => name == Some(n),
Device::Product {
manufacturer,
unique,
} => name.is_some_and(|n| {
(n & 0x1f_ffff) as u32 == unique && ((n >> 21) & 0x7ff) as u16 == manufacturer
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Target {
#[default]
Gnss,
Devices(Vec<Device>),
All,
}
impl Target {
pub fn parse(args: Option<&str>) -> Result<Target, String> {
let Some(args) = args else {
return Ok(Target::Gnss);
};
if args.trim().eq_ignore_ascii_case("all") {
return Ok(Target::All);
}
let devices = args
.split(',')
.map(|d| {
if d.trim().eq_ignore_ascii_case("all") {
Err("'all' cannot be combined with a device list".to_string())
} else {
d.parse::<Device>()
}
})
.collect::<Result<Vec<_>, _>>()?;
if devices.is_empty() {
return Err("gps-rollover= needs a device list, or 'all'".to_string());
}
Ok(Target::Devices(devices))
}
}
struct GpsRollover {
reference_day: u16,
target: Target,
names: [u64; 256],
}
static GPS_ROLLOVER_ON: AtomicBool = AtomicBool::new(false);
static GPS_ROLLOVER: RwLock<Option<GpsRollover>> = RwLock::new(None);
pub fn enable_gps_rollover(target: Target) {
enable_gps_rollover_at(target, today().max(MIN_REFERENCE_DAY));
}
pub fn enable_gps_rollover_at(target: Target, reference_day: u16) {
*GPS_ROLLOVER.write().unwrap_or_else(|e| e.into_inner()) = Some(GpsRollover {
reference_day,
target,
names: [0; 256],
});
GPS_ROLLOVER_ON.store(true, Ordering::Release);
}
pub fn disable_gps_rollover() {
GPS_ROLLOVER_ON.store(false, Ordering::Release);
*GPS_ROLLOVER.write().unwrap_or_else(|e| e.into_inner()) = None;
}
#[cfg(test)]
pub fn gps_rollover_reference_day() -> Option<u16> {
GPS_ROLLOVER
.read()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
.map(|q| q.reference_day)
}
fn today() -> u16 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| (d.as_secs() / 86400).min(u16::MAX as u64) as u16)
.unwrap_or(0)
}
pub fn corrected_gps_date(days: u16, reference_day: u16) -> u16 {
let behind = u32::from(reference_day).saturating_sub(u32::from(days));
let epoch = u32::from(GPS_ROLLOVER_DAYS);
let epochs = (behind + epoch / 2) / epoch;
if epochs == 0 {
return days;
}
let corrected = u32::from(days) + epochs * epoch;
if corrected > MAX_DATE_DAY {
return days;
}
corrected as u16
}
const SYSTEM_TIME_SOURCE_GPS: u64 = 0;
pub(crate) fn note_address_claim(src: u8, payload: &[u8]) {
if !GPS_ROLLOVER_ON.load(Ordering::Relaxed) {
return;
}
let Some(bytes) = payload.get(..8) else {
return;
};
let name = u64::from_le_bytes(bytes.try_into().expect("8 bytes"));
if let Some(q) = GPS_ROLLOVER
.write()
.unwrap_or_else(|e| e.into_inner())
.as_mut()
{
for slot in q.names.iter_mut() {
if *slot == name {
*slot = 0;
}
}
q.names[usize::from(src)] = name;
}
}
impl GpsRollover {
fn name_of(&self, src: u8) -> Option<u64> {
match self.names[usize::from(src)] {
0 => None,
n => Some(n),
}
}
}
impl Target {
fn lists(&self, src: u8, name: Option<u64>) -> bool {
match self {
Target::Gnss => false,
Target::All => true,
Target::Devices(devices) => devices.iter().any(|d| d.matches(src, name)),
}
}
}
pub fn gps_rollover_device_listed(src: u8) -> bool {
if !GPS_ROLLOVER_ON.load(Ordering::Relaxed) {
return false;
}
GPS_ROLLOVER
.read()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
.is_some_and(|q| q.target.lists(src, q.name_of(src)))
}
pub(crate) fn apply(pgn: u32, src: u8, fields: &mut [DecodedField]) {
if !GPS_ROLLOVER_ON.load(Ordering::Relaxed) {
return;
}
let guard = GPS_ROLLOVER.read().unwrap_or_else(|e| e.into_inner());
if let Some(q) = guard.as_ref() {
apply_at(pgn, src, q.name_of(src), fields, &q.target, q.reference_day);
}
}
fn apply_at(
pgn: u32,
src: u8,
name: Option<u64>,
fields: &mut [DecodedField],
target: &Target,
reference_day: u16,
) {
let correct = if target.lists(src, name) {
!matches!(pgn, 129793 | 129794 | 127258)
} else {
is_gnss_receiver_date(pgn, fields)
};
if !correct {
return;
}
for f in fields.iter_mut() {
if let FieldValue::Date(days) = f.value {
f.value = FieldValue::Date(corrected_gps_date(days, reference_day));
}
}
}
fn is_gnss_receiver_date(pgn: u32, fields: &[DecodedField]) -> bool {
match pgn {
129029 | 129033 => true,
126992 => fields
.iter()
.find(|f| f.id() == "source")
.and_then(|f| f.value.lookup_value())
.is_some_and(|v| v == SYSTEM_TIME_SOURCE_GPS),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn day(y: i32, m: u32, d: u32) -> u16 {
let y = if m <= 2 { y - 1 } else { y };
let era = y.div_euclid(400);
let yoe = (y - era * 400) as u32;
let mp = (m + 9) % 12;
let doy = (153 * mp + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
(era as i64 * 146097 + doe as i64 - 719468) as u16
}
#[test]
fn one_epoch_behind_is_corrected() {
let reported = day(2002, 11, 18);
let reference = day(2022, 7, 4);
assert_eq!(corrected_gps_date(reported, reference), day(2022, 7, 4));
}
#[test]
fn two_epochs_behind_is_corrected() {
let reference = day(2026, 8, 29);
let reported = reference - 2 * GPS_ROLLOVER_DAYS;
assert_eq!(corrected_gps_date(reported, reference), reference);
}
#[test]
fn a_current_date_is_left_alone() {
let reference = day(2026, 8, 29);
assert_eq!(corrected_gps_date(reference, reference), reference);
assert_eq!(
corrected_gps_date(reference - 30, reference),
reference - 30
);
}
#[test]
fn a_merely_old_date_is_left_alone() {
let reference = day(2026, 8, 29);
let reported = day(2017, 8, 29);
assert_eq!(corrected_gps_date(reported, reference), reported);
}
#[test]
fn a_stale_reference_still_corrects() {
let real_today = day(2030, 6, 1);
let reported = real_today - GPS_ROLLOVER_DAYS;
assert_eq!(corrected_gps_date(reported, MIN_REFERENCE_DAY), real_today);
}
#[test]
fn the_sentinel_range_is_never_entered() {
let late = 0xfffcu16 - 100;
assert_eq!(corrected_gps_date(late, u16::MAX), late);
}
const VHF_NAME: u64 = 491603 | 1851 << 21 | 190 << 40 | 70 << 49 | 4 << 60 | 1 << 63;
#[test]
fn device_syntax() {
assert_eq!("4".parse::<Device>(), Ok(Device::Address(4)));
assert_eq!(" 251 ".parse::<Device>(), Ok(Device::Address(251)));
assert_eq!(
"1851:491603".parse::<Device>(),
Ok(Device::Product {
manufacturer: 1851,
unique: 491603
})
);
assert_eq!(
format!("0x{VHF_NAME:016x}").parse::<Device>(),
Ok(Device::Name(VHF_NAME))
);
assert_eq!("0XFF".parse::<Device>(), Ok(Device::Name(0xff)));
}
#[test]
fn device_syntax_rejects_what_is_not_a_device() {
for bad in [
"",
"252",
"255",
"300",
"-1",
"4.5",
"vhf",
"0x",
"0xnope",
"0x0",
"0x0000000000000000",
"1851",
"2048:1",
"1851:2097152",
"1851:",
":5",
] {
assert!(bad.parse::<Device>().is_err(), "{bad:?} should be rejected");
}
}
#[test]
fn target_syntax() {
assert_eq!(Target::parse(None), Ok(Target::Gnss));
assert_eq!(Target::parse(Some("all")), Ok(Target::All));
assert_eq!(Target::parse(Some("ALL")), Ok(Target::All));
assert_eq!(
Target::parse(Some("4, 1851:491603,0x10")),
Ok(Target::Devices(vec![
Device::Address(4),
Device::Product {
manufacturer: 1851,
unique: 491603
},
Device::Name(0x10),
]))
);
assert!(Target::parse(Some("")).is_err());
assert!(Target::parse(Some("4,")).is_err());
assert!(Target::parse(Some("all,4")).is_err());
assert!(Target::parse(Some("4,all")).is_err());
}
#[test]
fn product_matches_only_its_own_name() {
let d: Device = "1851:491603".parse().unwrap();
assert!(d.matches(4, Some(VHF_NAME)));
assert!(!d.matches(4, None));
assert!(!d.matches(4, Some(VHF_NAME ^ (1 << 21))));
assert!(!d.matches(4, Some(VHF_NAME ^ 1)));
assert!(d.matches(9, Some(VHF_NAME ^ (1 << 40))));
assert!(Device::Name(VHF_NAME).matches(9, Some(VHF_NAME)));
assert!(!Device::Name(VHF_NAME).matches(9, Some(VHF_NAME ^ (1 << 40))));
}
static SWITCH: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn date_after(
frame: &crate::engine::RawFrame,
name: Option<u64>,
target: &Target,
reference_day: u16,
) -> Option<u16> {
let _guard = SWITCH.lock().unwrap_or_else(|e| e.into_inner());
let mut decoded = crate::engine::PgnDatabase::embedded(crate::engine::Units::Si)
.decode(frame)
.expect("decodes");
apply_at(
frame.pgn,
frame.src,
name,
&mut decoded.fields,
target,
reference_day,
);
decoded.fields.iter().find_map(|f| match f.value {
FieldValue::Date(d) => Some(d),
_ => None,
})
}
fn system_time(src: u8, source: u8, date: u16) -> crate::engine::RawFrame {
let d = date.to_le_bytes();
let data = [0x36, 0xf0 | source, d[0], d[1], 0x10, 0x6d, 0xff, 0x19];
crate::engine::RawFrame::new(None, 3, 126992, src, 255, data)
}
fn dsc_call() -> crate::engine::RawFrame {
let data: Vec<u8> =
"74,6c,00,16,29,02,28,64,7e,39,30,30,30,31,36,ff,ff,ff,ff,ff,ff,02,01,ff,ff,ff,\
7f,ff,ff,ff,7f,ff,ff,ff,ff,ff,ff,ff,ff,ff,7f,fc,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,\
ff,ff,40,f2,b5,17,d4,34,00,00"
.split(',')
.map(|h| u8::from_str_radix(h, 16).unwrap())
.collect();
crate::engine::RawFrame::new(None, 4, 129808, 4, 255, data)
}
fn ais_static(src: u8, date: u16) -> crate::engine::RawFrame {
let mut data = vec![0xffu8; 76];
data[0] = 0x05; data[1..5].copy_from_slice(&244_660_000u32.to_le_bytes());
data[45..47].copy_from_slice(&date.to_le_bytes());
crate::engine::RawFrame::new(None, 6, 129794, src, 255, data)
}
#[test]
fn system_time_is_corrected_only_for_a_gps_source() {
let reference = day(2022, 7, 4);
let reported = day(2002, 11, 18);
let gnss = Target::Gnss;
assert_eq!(
date_after(&system_time(12, 0, reported), None, &gnss, reference),
Some(reference)
);
assert_eq!(
date_after(&system_time(12, 5, reported), None, &gnss, reference),
Some(reported)
);
}
#[test]
fn a_listed_device_has_every_date_corrected() {
let reference = day(2026, 8, 27);
let dsc = dsc_call();
let rolled = day(2007, 1, 11);
assert_eq!(
date_after(&dsc, None, &Target::Gnss, reference),
Some(rolled)
);
let by_address = Target::Devices(vec![Device::Address(4)]);
assert_eq!(
date_after(&dsc, None, &by_address, reference),
Some(reference)
);
let by_product = Target::parse(Some("1851:491603")).unwrap();
assert_eq!(
date_after(&dsc, Some(VHF_NAME), &by_product, reference),
Some(reference)
);
assert_eq!(
date_after(&dsc, None, &Target::All, reference),
Some(reference)
);
assert_eq!(
date_after(&system_time(4, 5, rolled), None, &by_address, reference),
Some(reference)
);
}
#[test]
fn a_name_is_only_matched_once_the_claim_was_seen() {
let reference = day(2026, 8, 27);
let by_product = Target::parse(Some("1851:491603")).unwrap();
assert_eq!(
date_after(&dsc_call(), None, &by_product, reference),
Some(day(2007, 1, 11))
);
assert_eq!(
date_after(&dsc_call(), Some(VHF_NAME ^ 1), &by_product, reference),
Some(day(2007, 1, 11))
);
}
#[test]
fn an_unlisted_device_still_gets_the_gnss_rule() {
let reference = day(2022, 7, 4);
let reported = day(2002, 11, 18);
let by_address = Target::Devices(vec![Device::Address(4)]);
assert_eq!(
date_after(&system_time(115, 0, reported), None, &by_address, reference),
Some(reference)
);
assert_eq!(
date_after(&system_time(115, 5, reported), None, &by_address, reference),
Some(reported)
);
}
#[test]
fn relayed_ais_dates_are_never_corrected() {
let reference = day(2026, 8, 27);
let eta = day(2007, 1, 11);
for target in [
Target::Devices(vec![Device::Address(4)]),
Target::All,
Target::Gnss,
] {
assert_eq!(
date_after(&ais_static(4, eta), None, &target, reference),
Some(eta),
"{target:?}"
);
}
}
#[test]
fn the_decoder_learns_names_from_address_claims() {
let _guard = SWITCH.lock().unwrap_or_else(|e| e.into_inner());
let reference = day(2026, 8, 27);
enable_gps_rollover_at(Target::parse(Some("1851:491603")).unwrap(), reference);
let db = crate::engine::PgnDatabase::embedded(crate::engine::Units::Si);
let date_of = |d: crate::engine::DecodedPgn| {
d.fields.iter().find_map(|f| match f.value {
FieldValue::Date(d) => Some(d),
_ => None,
})
};
assert_eq!(
date_of(db.decode(&dsc_call()).unwrap()),
Some(day(2007, 1, 11))
);
let claim = crate::engine::RawFrame::new(None, 6, 60928, 4, 255, VHF_NAME.to_le_bytes());
db.decode(&claim).unwrap();
assert_eq!(date_of(db.decode(&dsc_call()).unwrap()), Some(reference));
let claim = crate::engine::RawFrame::new(None, 6, 60928, 9, 255, VHF_NAME.to_le_bytes());
db.decode(&claim).unwrap();
assert!(!gps_rollover_device_listed(4));
assert_eq!(
date_of(db.decode(&dsc_call()).unwrap()),
Some(day(2007, 1, 11))
);
let mut moved = dsc_call();
moved.src = 9;
assert_eq!(date_of(db.decode(&moved).unwrap()), Some(reference));
assert!(gps_rollover_device_listed(9));
assert!(!gps_rollover_device_listed(4));
disable_gps_rollover();
assert_eq!(gps_rollover_reference_day(), None);
assert!(!gps_rollover_device_listed(9));
}
}