Skip to main content

OutstationApplication interface

OutstationApplication is how the outstation asks your application for what only it knows, and tells it about changes worth saving. Each callback covers one job:

JobCalled whenWhat your code does
IIN bitsThe outstation builds a responseReports the state of your device
Time synchronizationThe master writes the timeSets the clock, if it accepts the master's time
RestartsThe master asks for a cold or warm restartUsually reports that restarts aren't supported
Counter freezesThe master freezes countersCopies counters to frozen counters
Saving changesThe master writes deadbands, event classes or device attributesSaves them so they survive a restart
Event confirmationsThe master confirms eventsTracks which events were delivered, if it needs to

IIN bits​

The outstation calls getApplicationIin() for every response and merges your bits with the ones it manages itself. You control four:

  • needTime: the outstation needs time synchronization
  • localControl: some output points are in local mode
  • deviceTrouble: the device has a problem
  • configCorrupt: the device configuration is corrupt

Time synchronization​

Whichever procedure the master uses, the outstation calls writeAbsoluteTime(..) with the time in milliseconds since January 1st, 1970, 00:00:00 UTC.

Set needTime when your clock needs synchronizing, typically at an interval that depends on its accuracy, and clear it in writeAbsoluteTime(..). If the bit is still set in the response, the master assumes the synchronization failed, and some masters then write the time over and over.

An outstation that gets its time from another source, such as NTP or IRIG-B, usually rejects the master's time unless that source is unavailable. One that doesn't support time synchronization at all returns NOT_SUPPORTED.

The example outstations ask for the time when they start, and clear the request when the master writes it:

fn get_application_iin(&self) -> ApplicationIin {
ApplicationIin {
need_time: self.need_time,
..Default::default()
}
}

fn write_absolute_time(&mut self, time: Timestamp) -> Result<(), RequestError> {
// a real device sets its clock here, and sets need_time again when its clock drifts
tracing::info!("write absolute time: {time}");
self.need_time = false;
Ok(())
}

Restarts​

A master can ask the outstation to restart. Because an attacker could use this to take a device offline, we recommend returning not supported from coldRestart and warmRestart in most products, and always in software gateways that share a server with other services. If you must support restarts, for conformance tests or a customer requirement, return an estimate of how long the restart takes; masters make little use of it, so it needn't be accurate. The outstation never restarts on its own: your application does.

Counter freezes​

When the master freezes counters, use the DatabaseHandle you're given to copy each counter to its frozen counter, and with FREEZE_AND_CLEAR, also reset the counter to 0. Requests to freeze at a given time and interval are passed to your application too.

The example outstations support both kinds of immediate freeze, but not freezing at a given time:

fn freeze_counter(
&mut self,
indices: FreezeIndices,
freeze_type: FreezeType,
database: &mut DatabaseHandle,
) -> Result<(), RequestError> {
let clear = match freeze_type {
FreezeType::ImmediateFreeze => false,
FreezeType::FreezeAndClear => true,
// freezing at a time and interval isn't supported
_ => return Err(RequestError::NotSupported),
};
let (start, stop) = match indices {
FreezeIndices::All => (0, POINT_COUNT - 1),
FreezeIndices::Range(start, stop) => (start, stop.min(POINT_COUNT - 1)),
};
tracing::info!("freeze counters {start} to {stop} (clear: {clear})");
database.transaction(|db| {
for index in start..=stop {
if let Some(counter) = Get::<Counter>::get(db, index) {
let time = get_current_time();
let frozen = FrozenCounter::new(counter.value, counter.flags, time);
db.update(index, &frozen, UpdateOptions::detect_event());
if clear {
let cleared = Counter::new(0, counter.flags, time);
db.update(index, &cleared, UpdateOptions::detect_event());
}
}
}
});
Ok(())
}

Saving what the master changes​

A master can change three kinds of settings in the outstation. The outstation applies each change to its database itself; the callbacks let you save it to non-volatile memory and apply it again when you add your points at startup. An outstation that doesn't save them returns to its configured values when it restarts.

SettingCallbacksNotes
Analog input deadbandsbeginWriteAnalogDeadBands, one call per deadband, endWriteAnalogDeadBandsRejected unless supportWriteAnalogDeadBands returns true
Event classesbeginAssignClass, one call per point header, endAssignClassProcessed unless supportAssignClass returns false
Device attributesOne call per attribute writtenOnly for attributes you define as writable

Save each batch in its end callback. A new event class applies only to events created afterwards.

An attribute write callback can reject a value, for example one out of range, by returning false. The outstation itself rejects writes to attributes that aren't defined or writable, or whose type doesn't match.

The example outstations log each batch where a device would save it:

fn support_write_analog_dead_bands(&mut self) -> bool {
true
}

fn begin_write_analog_dead_bands(&mut self) {
tracing::info!("begin writing analog dead-bands");
}

fn write_analog_dead_band(&mut self, index: u16, dead_band: f64) {
tracing::info!("change analog dead-band {index} to {dead_band}");
}

fn end_write_analog_dead_bands(&mut self) -> MaybeAsync<()> {
tracing::info!("end writing analog dead-bands: save them here");
MaybeAsync::ready(())
}

fn begin_assign_class(&mut self) {
tracing::info!("begin assigning classes");
}

fn assign_class(
&mut self,
point_type: PointType,
indices: PointIndices,
class: Option<EventClass>,
) {
tracing::info!("assign class {class:?} to {point_type:?} {indices:?}");
}

fn end_assign_class(&mut self) -> MaybeAsync<()> {
tracing::info!("end assigning classes: save them here");
MaybeAsync::ready(())
}
note

The example master can WRITE the User Defined Location attribute of the example outstation.

Event confirmations​

When the master confirms events, eventCleared reports each one delivered, by the ID its update returned. Most applications don't need this; it helps when the source of the events wants to know which ones have been delivered.