From c58cc7ad8bbfb8480bf6fd6aafb81dc59c3f980a Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Fri, 13 Jun 2025 14:36:54 +0100 Subject: [PATCH] Add option to configure max. gw time drift. Closes #666. --- chirpstack/src/cmd/configfile.rs | 9 +++ chirpstack/src/config.rs | 3 + chirpstack/src/uplink/helpers.rs | 101 +++++++++++++++++++++++++++++-- 3 files changed, 108 insertions(+), 5 deletions(-) diff --git a/chirpstack/src/cmd/configfile.rs b/chirpstack/src/cmd/configfile.rs index a86fb477..e04355b9 100644 --- a/chirpstack/src/cmd/configfile.rs +++ b/chirpstack/src/cmd/configfile.rs @@ -172,6 +172,15 @@ pub fn run() { # ChirpStack will be allowed. allow_unknown_gateways={{ gateway.allow_unknown_gateways }} + # RX timestamp max. drift. + # + # If the delta between the gateway reported RX timestamp vs ChirpStack + # server time is bigger than the configured value, then ChirpStack will + # ignore it. ChirpStack will then use the RX timestamp from the other + # receiving gateways, or failing that, will fall back onto the current + # server time. + rx_timestamp_max_drift="{{ gateway.rx_timestamp_max_drift }}" + # Network related configuration. [network] diff --git a/chirpstack/src/config.rs b/chirpstack/src/config.rs index 0c8d51e7..5ada4a4f 100644 --- a/chirpstack/src/config.rs +++ b/chirpstack/src/config.rs @@ -137,6 +137,8 @@ pub struct Gateway { pub ca_cert: String, pub ca_key: String, pub allow_unknown_gateways: bool, + #[serde(with = "humantime_serde")] + pub rx_timestamp_max_drift: Duration, } impl Default for Gateway { @@ -146,6 +148,7 @@ impl Default for Gateway { ca_cert: "".to_string(), ca_key: "".to_string(), allow_unknown_gateways: false, + rx_timestamp_max_drift: Duration::from_secs(30), } } } diff --git a/chirpstack/src/uplink/helpers.rs b/chirpstack/src/uplink/helpers.rs index f503593b..0dcb1855 100644 --- a/chirpstack/src/uplink/helpers.rs +++ b/chirpstack/src/uplink/helpers.rs @@ -3,12 +3,12 @@ use std::str::FromStr; use std::time::{Duration, SystemTime}; use anyhow::Result; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, TimeDelta, Utc}; -use crate::gpstime::ToDateTime; -use crate::region; use chirpstack_api::{common, gw}; +use crate::{config, gpstime::ToDateTime, region}; + pub fn get_uplink_dr( region_config_id: &str, tx_info: &chirpstack_api::gw::UplinkTxInfo, @@ -54,6 +54,9 @@ pub fn get_uplink_ch(region_config_id: &str, frequency: u32, dr: u8) -> Result SystemTime { + let conf = config::get(); + let rx_timestamp_max_drift = conf.gateway.rx_timestamp_max_drift; + // First search for time_since_gps_epoch. for rxi in rx_info { if let Some(gps_time) = &rxi.time_since_gps_epoch { @@ -71,7 +74,14 @@ pub fn get_rx_timestamp(rx_info: &[gw::UplinkRxInfo]) -> SystemTime { if let Some(ts) = &rxi.gw_time { let ts: Result> = (*ts).try_into().map_err(anyhow::Error::msg); if let Ok(ts) = ts { - return ts.into(); + let mut delta = Utc::now() - ts; + if delta < TimeDelta::default() { + delta = -delta; + } + let delta = delta.to_std().unwrap_or_default(); + if delta < rx_timestamp_max_drift { + return ts.into(); + } } } } @@ -81,6 +91,9 @@ pub fn get_rx_timestamp(rx_info: &[gw::UplinkRxInfo]) -> SystemTime { } pub fn get_rx_timestamp_chrono(rx_info: &[gw::UplinkRxInfo]) -> DateTime { + let conf = config::get(); + let rx_timestamp_max_drift = conf.gateway.rx_timestamp_max_drift; + // First search for time_since_gps_epoch. for rxi in rx_info { if let Some(gps_time) = &rxi.time_since_gps_epoch { @@ -98,7 +111,14 @@ pub fn get_rx_timestamp_chrono(rx_info: &[gw::UplinkRxInfo]) -> DateTime { if let Some(ts) = &rxi.gw_time { let ts: Result> = (*ts).try_into().map_err(anyhow::Error::msg); if let Ok(ts) = ts { - return ts; + let mut delta = Utc::now() - ts; + if delta < TimeDelta::default() { + delta = -delta; + } + let delta = delta.to_std().unwrap_or_default(); + if delta < rx_timestamp_max_drift { + return ts; + } } } } @@ -188,3 +208,74 @@ pub fn set_uplink_modulation( Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_get_rx_timestamp_no_drift() { + let now = Utc::now(); + let rx_info = gw::UplinkRxInfo { + gw_time: Some(now.try_into().unwrap()), + ..Default::default() + }; + + let res: DateTime = get_rx_timestamp(&[rx_info]).into(); + assert_eq!(res, now); + } + + #[test] + fn test_get_rx_timestamp_drift() { + let now = Utc::now() - chrono::Duration::seconds(60); + let rx_info = gw::UplinkRxInfo { + gw_time: Some(now.try_into().unwrap()), + ..Default::default() + }; + + let res: DateTime = get_rx_timestamp(&[rx_info]).into(); + assert_ne!(res, now); + + let now = Utc::now() + chrono::Duration::seconds(60); + let rx_info = gw::UplinkRxInfo { + gw_time: Some(now.try_into().unwrap()), + ..Default::default() + }; + + let res: DateTime = get_rx_timestamp(&[rx_info]).into(); + assert_ne!(res, now); + } + + #[test] + fn test_get_rx_timestamp_chrono_no_drift() { + let now = Utc::now(); + let rx_info = gw::UplinkRxInfo { + gw_time: Some(now.try_into().unwrap()), + ..Default::default() + }; + + let res = get_rx_timestamp_chrono(&[rx_info]); + assert_eq!(res, now); + } + + #[test] + fn test_get_rx_timestamp_chrono_drift() { + let now = Utc::now() - chrono::Duration::seconds(60); + let rx_info = gw::UplinkRxInfo { + gw_time: Some(now.try_into().unwrap()), + ..Default::default() + }; + + let res = get_rx_timestamp_chrono(&[rx_info]); + assert_ne!(res, now); + + let now = Utc::now() + chrono::Duration::seconds(60); + let rx_info = gw::UplinkRxInfo { + gw_time: Some(now.try_into().unwrap()), + ..Default::default() + }; + + let res = get_rx_timestamp_chrono(&[rx_info]); + assert_ne!(res, now); + } +}