Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions dnp3/src/master/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
80 changes: 76 additions & 4 deletions dnp3/src/master/tasks/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use tokio::time::Instant;

enum State {
MeasureDelay(Option<Instant>),
WriteAbsoluteTime(Timestamp),
WriteAbsoluteTime(Option<Timestamp>),
RecordCurrentTime(Option<Timestamp>),
WriteLastRecordedTime(Timestamp),
}
Expand All @@ -37,6 +37,7 @@ impl TimeSyncProcedure {
match self {
TimeSyncProcedure::Lan => State::RecordCurrentTime(None),
TimeSyncProcedure::NonLan => State::MeasureDelay(None),
TimeSyncProcedure::DirectWriteAbsTime => State::WriteAbsoluteTime(None),
}
}
}
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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 }),
}
Expand Down Expand Up @@ -210,7 +226,7 @@ impl TimeSyncTask {
};

Ok(Some(
self.change_state(State::WriteAbsoluteTime(timestamp))
self.change_state(State::WriteAbsoluteTime(Some(timestamp)))
.wrap(),
))
}
Expand Down Expand Up @@ -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<Result<(), TimeSyncError>>,
) {
time_sync_setup(TimeSyncProcedure::DirectWriteAbsTime, |time| {
Box::new(SingleTimestampTestHandler::new(time))
})
}
}
}
2 changes: 2 additions & 0 deletions ffi/dnp3-ffi/src/master/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,7 @@ fn convert_auto_time_sync(config: &ffi::AutoTimeSync) -> Option<TimeSyncProcedur
ffi::AutoTimeSync::None => None,
ffi::AutoTimeSync::Lan => Some(TimeSyncProcedure::Lan),
ffi::AutoTimeSync::NonLan => Some(TimeSyncProcedure::NonLan),
ffi::AutoTimeSync::DirectWriteAbsTime => Some(TimeSyncProcedure::DirectWriteAbsTime),
}
}

Expand Down Expand Up @@ -1218,6 +1219,7 @@ impl From<ffi::TimeSyncMode> for TimeSyncProcedure {
match x {
ffi::TimeSyncMode::Lan => Self::Lan,
ffi::TimeSyncMode::NonLan => Self::NonLan,
ffi::TimeSyncMode::DirectWriteAbsTime => Self::DirectWriteAbsTime,
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions ffi/dnp3-schema/src/master/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?;

Expand Down Expand Up @@ -1492,6 +1496,10 @@ fn define_time_sync_mode(lib: &mut LibraryBuilder) -> BackTraced<EnumHandle> {
"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()?;

Expand Down