use crate::engine::quirk::gps_rollover_device_listed;
use crate::engine::{ADDR_GLOBAL, DecodedPgn, FieldValue, RawFrame};
const NETWORK_MANAGEMENT: &[u32] = &[
59392, 59904, 60160, 60416, 60928, 65240, 126208, 126464, 126993, 126996, 126998, ];
const MAX_RELAY_PAYLOAD: usize = 223;
pub struct GpsRelay {
announced: [bool; 256],
}
impl Default for GpsRelay {
fn default() -> Self {
Self::new()
}
}
impl GpsRelay {
pub fn new() -> Self {
Self {
announced: [false; 256],
}
}
pub fn process(&mut self, d: &DecodedPgn, own_addr: Option<u8>) -> Option<RawFrame> {
if Some(d.src) == own_addr
|| d.src >= crate::engine::ADDR_NULL
|| d.dst != ADDR_GLOBAL
|| NETWORK_MANAGEMENT.contains(&d.pgn)
|| d.data.len() > MAX_RELAY_PAYLOAD
|| !gps_rollover_device_listed(d.src)
{
return None;
}
if !std::mem::replace(&mut self.announced[usize::from(d.src)], true) {
log::info!(
"quirk(gps-relay): relaying everything from src {} as canboat's own, dates corrected",
d.src
);
}
Some(RawFrame::new(
d.timestamp.clone(),
d.prio,
d.pgn,
ADDR_GLOBAL,
ADDR_GLOBAL,
corrected_payload(d),
))
}
}
fn corrected_payload(d: &DecodedPgn) -> Vec<u8> {
let mut data = d.data.clone();
for f in &d.fields {
let FieldValue::Date(days) = f.value else {
continue;
};
let (Some(bit_offset), Some(16)) = (f.bit_offset, f.bit_length) else {
continue;
};
if bit_offset % 8 != 0 {
continue;
}
let at = bit_offset as usize / 8;
if let Some(slot) = data.get_mut(at..at + 2) {
slot.copy_from_slice(&days.to_le_bytes());
}
}
data
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::quirk::{Device, Target, disable_gps_rollover, enable_gps_rollover_at};
use crate::engine::{PgnDatabase, Units};
const REFERENCE: u16 = 20692;
const ROLLED: u16 = REFERENCE - 7168;
fn dec(frame: &RawFrame) -> DecodedPgn {
PgnDatabase::embedded(Units::Si)
.decode(frame)
.expect("decodes")
}
fn bytes(hex: &str) -> Vec<u8> {
hex.split(',')
.map(|h| u8::from_str_radix(h, 16).unwrap())
.collect()
}
fn system_time(src: u8, dst: u8) -> RawFrame {
let d = ROLLED.to_le_bytes();
RawFrame::new(
Some("2026-08-27T11:04:00.100Z".into()),
3,
126992,
src,
dst,
[0x36, 0xf5, d[0], d[1], 0x10, 0x6d, 0xff, 0x19],
)
}
fn gnss_position(src: u8) -> RawFrame {
let mut data = bytes(
"88,a4,50,c0,99,13,10,40,52,84,77,54,17,74,04,00,3d,52,71,eb,12,23,13,c0,c6,2d,00,00,\
00,00,00,13,fc,0e,40,00,7d,00,0a,0f,00,00,00,ff,ff,ff,ff",
);
data[1..3].copy_from_slice(&ROLLED.to_le_bytes());
RawFrame::new(None, 3, 129029, src, 255, data)
}
fn with_device_4(body: impl FnOnce()) {
let _guard = super::super::tests::switch_guard();
enable_gps_rollover_at(Target::Devices(vec![Device::Address(4)]), REFERENCE);
body();
disable_gps_rollover();
}
#[test]
fn relays_a_listed_devices_frame_with_the_date_patched() {
with_device_4(|| {
let frame = system_time(4, 255);
let out = GpsRelay::new()
.process(&dec(&frame), Some(30))
.expect("relayed");
assert_eq!(out.src, ADDR_GLOBAL);
assert_eq!(out.dst, ADDR_GLOBAL);
assert_eq!(out.pgn, 126992);
assert_eq!(out.prio, 3);
assert_eq!(out.timestamp, frame.timestamp);
let mut expected = frame.data.to_vec();
expected[2..4].copy_from_slice(&REFERENCE.to_le_bytes());
assert_eq!(out.data.to_vec(), expected);
});
}
#[test]
fn patches_a_fast_packet_in_place() {
with_device_4(|| {
let frame = gnss_position(4);
let out = GpsRelay::new()
.process(&dec(&frame), None)
.expect("relayed");
let mut expected = frame.data.to_vec();
expected[1..3].copy_from_slice(&REFERENCE.to_le_bytes());
assert_eq!(out.data.to_vec(), expected, "only the date bytes change");
});
}
#[test]
fn relays_a_dateless_frame_unchanged() {
with_device_4(|| {
let frame = RawFrame::new(None, 2, 129025, 4, 255, bytes("40,52,84,77,54,17,74,04"));
let out = GpsRelay::new()
.process(&dec(&frame), None)
.expect("relayed");
assert_eq!(out.data, frame.data);
assert_eq!(out.src, ADDR_GLOBAL);
});
}
#[test]
fn leaves_everything_else_alone() {
with_device_4(|| {
let mut relay = GpsRelay::new();
assert!(relay.process(&dec(&system_time(5, 255)), None).is_none());
assert!(relay.process(&dec(&system_time(4, 30)), None).is_none());
assert!(
relay
.process(&dec(&system_time(30, 255)), Some(30))
.is_none()
);
let claim = RawFrame::new(None, 6, 60928, 4, 255, bytes("53,80,67,e7,00,be,8c,c0"));
assert!(relay.process(&dec(&claim), None).is_none());
let heartbeat =
RawFrame::new(None, 7, 126993, 4, 255, bytes("60,ea,03,ff,ff,ff,ff,ff"));
assert!(relay.process(&dec(&heartbeat), None).is_none());
});
}
#[test]
fn nothing_is_relayed_when_the_quirk_is_off() {
let _guard = super::super::tests::switch_guard();
disable_gps_rollover();
assert!(
GpsRelay::new()
.process(&dec(&system_time(4, 255)), None)
.is_none()
);
}
}