diff --git a/dnp3/src/master/request.rs b/dnp3/src/master/request.rs index a9383f0f..f491db1a 100644 --- a/dnp3/src/master/request.rs +++ b/dnp3/src/master/request.rs @@ -34,11 +34,19 @@ pub enum CommandMode { feature = "serialization", derive(serde::Serialize, serde::Deserialize) )] +#[non_exhaustive] pub enum TimeSyncProcedure { /// Master will use the LAN procedure: RECORD_CURRENT_TIME followed by WRITE g50v3 Lan, /// Master will use the non-LAN procedure: DELAY_MEASUREMENT followed by WRITE g50v1 NonLan, + /// Master will directly WRITE g50v1 (absolute time) without delay measurement + /// + /// **WARNING**: This is a non-conformant workaround for outstations that do not support + /// the DELAY_MEASURE function code. This method does NOT account for network latency + /// and may result in inaccurate time synchronization. Use only when the outstation + /// does not support standard time synchronization procedures. + DirectWriteAbsTime, } /// struct recording which event classes are enabled diff --git a/dnp3/src/master/tasks/time.rs b/dnp3/src/master/tasks/time.rs index 93618e27..66292d27 100644 --- a/dnp3/src/master/tasks/time.rs +++ b/dnp3/src/master/tasks/time.rs @@ -16,7 +16,7 @@ use tokio::time::Instant; enum State { MeasureDelay(Option), - WriteAbsoluteTime(Timestamp), + WriteAbsoluteTime(Option), RecordCurrentTime(Option), WriteLastRecordedTime(Timestamp), } @@ -37,6 +37,7 @@ impl TimeSyncProcedure { match self { TimeSyncProcedure::Lan => State::RecordCurrentTime(None), TimeSyncProcedure::NonLan => State::MeasureDelay(None), + TimeSyncProcedure::DirectWriteAbsTime => State::WriteAbsoluteTime(None), } } } @@ -74,7 +75,20 @@ impl TimeSyncTask { } } } - State::WriteAbsoluteTime(_) => Some(self), + State::WriteAbsoluteTime(time) => { + if time.is_none() { + *time = association.get_system_time(); + match time { + Some(_) => Some(self), + None => { + self.report_error(association, TimeSyncError::SystemTimeNotAvailable); + None + } + } + } else { + Some(self) + } + } State::RecordCurrentTime(time) => { *time = association.get_system_time(); @@ -102,7 +116,9 @@ impl TimeSyncTask { pub(crate) fn write(&self, writer: &mut HeaderWriter) -> Result<(), scursor::WriteError> { match self.state { State::MeasureDelay(_) => Ok(()), - State::WriteAbsoluteTime(x) => writer.write_count_of_one(Group50Var1 { time: x }), + State::WriteAbsoluteTime(x) => writer.write_count_of_one(Group50Var1 { + time: x.expect("WriteAbsoluteTime timestamp must be set by start()"), + }), State::RecordCurrentTime(_) => Ok(()), State::WriteLastRecordedTime(x) => writer.write_count_of_one(Group50Var3 { time: x }), } @@ -210,7 +226,7 @@ impl TimeSyncTask { }; Ok(Some( - self.change_state(State::WriteAbsoluteTime(timestamp)) + self.change_state(State::WriteAbsoluteTime(Some(timestamp))) .wrap(), )) } @@ -969,4 +985,60 @@ mod tests { task } } + + mod direct_write_abs_time { + use super::*; + + #[test] + fn transitions_to_write_absolute_time() { + let (task, system_time, mut association, _rx) = direct_write_setup(); + let task = task.start(&mut association).unwrap(); + + // Should have transitioned to WriteAbsoluteTime + assert_eq!(task.function(), FunctionCode::Write); + + // Verify it writes g50v1 with the system time + let mut buffer = [0; 20]; + let mut cursor = WriteCursor::new(&mut buffer); + let mut writer = start_request( + ControlField::request(Sequence::default()), + task.function(), + &mut cursor, + ) + .unwrap(); + task.write(&mut writer).unwrap(); + let request = writer.to_parsed().to_request().unwrap(); + + let header = request.objects.unwrap().get_only_header().unwrap(); + match header.details.count().unwrap() { + CountVariation::Group50Var1(seq) => { + assert_eq!(seq.single().unwrap().time, system_time); + } + _ => panic!("expected g50v1, not g50v3"), + } + } + + #[test] + fn no_system_time_available() { + let (task, _system_time, mut association, mut rx) = direct_write_setup(); + association.get_system_time(); // Empty the time + + assert!(task.start(&mut association).is_none()); + assert_eq!( + rx.try_recv().unwrap(), + Err(TimeSyncError::SystemTimeNotAvailable) + ); + } + + fn direct_write_setup() -> ( + NonReadTask, + Timestamp, + Association, + tokio::sync::oneshot::Receiver>, + ) { + time_sync_setup(TimeSyncProcedure::DirectWriteAbsTime, |time| { + Box::new(SingleTimestampTestHandler::new(time)) + }) + } + } } diff --git a/ffi/dnp3-ffi/src/master/functions.rs b/ffi/dnp3-ffi/src/master/functions.rs index acce3ddb..16f415d0 100644 --- a/ffi/dnp3-ffi/src/master/functions.rs +++ b/ffi/dnp3-ffi/src/master/functions.rs @@ -1047,6 +1047,7 @@ fn convert_auto_time_sync(config: &ffi::AutoTimeSync) -> Option None, ffi::AutoTimeSync::Lan => Some(TimeSyncProcedure::Lan), ffi::AutoTimeSync::NonLan => Some(TimeSyncProcedure::NonLan), + ffi::AutoTimeSync::DirectWriteAbsTime => Some(TimeSyncProcedure::DirectWriteAbsTime), } } @@ -1218,6 +1219,7 @@ impl From for TimeSyncProcedure { match x { ffi::TimeSyncMode::Lan => Self::Lan, ffi::TimeSyncMode::NonLan => Self::NonLan, + ffi::TimeSyncMode::DirectWriteAbsTime => Self::DirectWriteAbsTime, } } } diff --git a/ffi/dnp3-schema/src/master/mod.rs b/ffi/dnp3-schema/src/master/mod.rs index 466a073f..b8af709e 100644 --- a/ffi/dnp3-schema/src/master/mod.rs +++ b/ffi/dnp3-schema/src/master/mod.rs @@ -861,6 +861,10 @@ fn define_association_config( "non_lan", "Perform automatic time sync with Delay Measurement (0x17) function code", )? + .push( + "direct_write_abs_time", + "Directly write absolute time (g50v1) without delay measurement. WARNING: This is a non-conformant workaround for outstations that do not support DELAY_MEASURE. Does not account for network latency.", + )? .doc("Automatic time synchronization configuration")? .build()?; @@ -1492,6 +1496,10 @@ fn define_time_sync_mode(lib: &mut LibraryBuilder) -> BackTraced { "non_lan", "Perform a non-LAN time sync with Delay Measurement (0x17) function code", )? + .push( + "direct_write_abs_time", + "Directly write absolute time (g50v1) without delay measurement. WARNING: This is a non-conformant workaround for outstations that do not support DELAY_MEASURE. Does not account for network latency.", + )? .doc("Time synchronization mode")? .build()?;