use std::sync::OnceLock;
use std::time::{Duration, Instant};
use crate::engine::DecodedPgn;
#[cfg(test)]
use crate::engine::PgnDatabase;
use crate::n2kd::decoded::{self, Handles};
pub struct RateLimiter {
last_passed: [[Option<Instant>; RATE_COUNT]; 256],
enabled: bool,
last_sog_cog: Option<(f64, f64, Instant)>,
pub ais_seq: u8,
}
const RATE_COUNT: usize = 10;
impl RateLimiter {
pub fn new(enabled: bool) -> Self {
Self {
last_passed: [[None; RATE_COUNT]; 256],
enabled,
last_sog_cog: None,
ais_seq: 0,
}
}
#[inline]
pub(crate) fn enabled(&self) -> bool {
self.enabled
}
#[inline]
pub(crate) fn last_passed_slot(&mut self, src: usize, rate: usize) -> &mut Option<Instant> {
&mut self.last_passed[src][rate]
}
#[inline]
pub(crate) fn record_sog_cog(&mut self, sog_ms: f64, cog_deg: f64) {
self.last_sog_cog = Some((sog_ms, cog_deg, Instant::now()));
}
#[inline]
pub(crate) fn recent_sog_cog(&self) -> Option<(f64, f64)> {
let (sog, cog, ts) = self.last_sog_cog?;
if ts.elapsed() < Duration::from_secs(1) {
Some((sog, cog))
} else {
None
}
}
}
#[cfg(test)]
pub fn convert(out: &mut String, msg: &str, rate_limiter: &mut RateLimiter) -> usize {
let Some(decoded) =
crate::engine::json_to_decoded(msg, PgnDatabase::embedded(crate::engine::Units::Metric))
else {
return 0;
};
convert_decoded(out, &decoded, rate_limiter)
}
pub fn convert_decoded(
out: &mut String,
decoded: &DecodedPgn,
rate_limiter: &mut RateLimiter,
) -> usize {
decoded::convert_nmea0183(out, decoded, rate_limiter, handles())
}
fn handles() -> &'static Handles {
static HANDLES: OnceLock<Handles> = OnceLock::new();
HANDLES.get_or_init(Handles::new)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hdg_format() {
let msg = r#"{"pgn":127250,"src":7,"fields":{"Heading":90.0,"Reference":{"value":1,"name":"Magnetic"},"Deviation":2.0,"Variation":-3.0}}"#;
let mut out = String::new();
let mut rl = RateLimiter::new(false);
let n = convert(&mut out, msg, &mut rl);
assert_eq!(n, 1);
assert!(out.starts_with("$AH"), "got {out}");
assert!(out.contains("HDG,90.0,2.0,E,3.0,W*"));
assert!(out.ends_with("\r\n"));
}
#[test]
fn mwv_apparent_wind() {
let msg = r#"{"pgn":130306,"src":7,"fields":{"Wind Speed":5.0,"Wind Angle":90.0,"Reference":{"value":2,"name":"Apparent"}}}"#;
let mut out = String::new();
let mut rl = RateLimiter::new(false);
convert(&mut out, msg, &mut rl);
assert!(out.contains("MWV,90.0,R,18.0,K,A*"), "got {out}");
}
#[test]
fn mwv_from_camel_json() {
let msg = r#"{"windData":{"pgn":130306,"src":7,"fields":{"windSpeed":5.0,"windAngle":90.0,"reference":{"value":2,"name":"Apparent"}}}}"#;
let mut out = String::new();
let mut rl = RateLimiter::new(false);
convert(&mut out, msg, &mut rl);
assert!(out.contains("MWV,90.0,R,18.0,K,A*"), "got {out}");
}
#[test]
fn mwv_converts_from_si_radians() {
let msg = r#"{"pgn":130306,"src":7,"fields":{"Wind Speed":5.0,"Wind Angle":1.5708,"Reference":{"value":2,"name":"Apparent"}}}"#;
let decoded =
crate::engine::json_to_decoded(msg, PgnDatabase::embedded(crate::engine::Units::Si))
.expect("decode");
let mut out = String::new();
let mut rl = RateLimiter::new(false);
convert_decoded(&mut out, &decoded, &mut rl);
assert!(
out.contains("MWV,90.0,R,"),
"SI radians should render as 90.0°, got {out}"
);
}
#[test]
fn dpt_format() {
let msg = r#"{"pgn":128267,"src":3,"fields":{"Depth":12.3,"Offset":0.5}}"#;
let mut out = String::new();
let mut rl = RateLimiter::new(false);
convert(&mut out, msg, &mut rl);
assert!(out.contains("DPT,12.3,0.5*"), "got {out}");
}
#[test]
fn vtg_format() {
let msg = r#"{"pgn":129026,"src":3,"fields":{"SOG":5.144,"COG":180.0}}"#;
let mut out = String::new();
let mut rl = RateLimiter::new(false);
convert(&mut out, msg, &mut rl);
assert!(out.contains("VTG,180.0,T"), "got {out}");
assert!(out.contains(",10.00,N,"), "got {out}");
}
#[test]
fn rate_limit_drops_second_call_within_a_second() {
let msg = r#"{"pgn":127250,"src":7,"fields":{"Heading":0.0,"Reference":{"value":1,"name":"Magnetic"}}}"#;
let mut out = String::new();
let mut rl = RateLimiter::new(true);
assert_eq!(convert(&mut out, msg, &mut rl), 1);
let len_after_first = out.len();
convert(&mut out, msg, &mut rl);
assert_eq!(out.len(), len_after_first);
}
}