Skip to main content

ControlHandler Interface

The ControlHandler interface lets application code receive control requests. Each request begins with a call to beginFragment and ends with a call to endFragment. These callbacks are helpful if you want the application to handle operations atomically or in batches of some sort.

Each select or operate callback returns a CommandStatus enum. The outstation response to a control message echoes the request data and this status code is returned for every control in the message. The library actually builds the response message while making these callbacks.

Handling SELECT

The select* methods of ControlHandler are called when a properly formatted SELECT messaged is received from the master. Selecting a control point should never cause the point to operate. Instead, think of this request from the master to mean, "Do you support this?"

sequenceDiagram Master->>Outstation: select; Outstation->>Master: response; Master->>Outstation: operate; Outstation->>Master: response;

DNP3 masters can also use a "select-before-operate" strategy to execute controls. The rules for how outstations process these two-pass control messages are complex. The library will handle all these rules for you automatically.

Handling OPERATE

The operate* methods of ControlHandler are called when the outstation receives either:

  • OPERATE function code preceded by a matching SELECT.
  • DIRECT_OPERATE function code (single-pass control with response).
  • DIRECT_OPERATE_NO_RESPONSE function code (single-pass control without a response).

A reference to a DatabaseHandle also lets you update point values in response to a control request. While you would typically use it to update BinaryOutputStatus and AnalogOutputStatus values, you can also use it to update other types if required by the application. For example, you might maintain the number of control operations performed in a DNP3 counter value.

tip

Every call to DatabaseHandle.transaction locks and unlocks a mutex. Since DNP3 can carry multiple controls in a single message, the most efficient way to update feedback points is to do a single transaction in the ControlHandler.end_fragment method.

note

The OperateType enum lets you identify which of the three operate function codes invoked the method. You can probably ignore this value. The specification requires all three control methodologies be supported and result in the same action.

struct ExampleControlHandler;
impl ControlHandler for ExampleControlHandler {}

impl ControlSupport<Group12Var1> for ExampleControlHandler {
fn select(
&mut self,
control: Group12Var1,
index: u16,
_database: &mut DatabaseHandle,
) -> CommandStatus {
if index < 10
&& (control.code.op_type == OpType::LatchOn || control.code.op_type == OpType::LatchOff)
{
CommandStatus::Success
} else {
CommandStatus::NotSupported
}
}

fn operate(
&mut self,
control: Group12Var1,
index: u16,
_op_type: OperateType,
database: &mut DatabaseHandle,
) -> CommandStatus {
if index < 10
&& (control.code.op_type == OpType::LatchOn || control.code.op_type == OpType::LatchOff)
{
let status = control.code.op_type == OpType::LatchOn;
database.transaction(|db| {
db.update(
index,
&BinaryOutputStatus::new(status, Flags::ONLINE, get_current_time()),
UpdateOptions::detect_event(),
);
});
CommandStatus::Success
} else {
CommandStatus::NotSupported
}
}
}

impl ExampleControlHandler {
fn select_analog_output(&self, index: u16) -> CommandStatus {
if index < 10 {
CommandStatus::Success
} else {
CommandStatus::NotSupported
}
}

fn operate_analog_output(
&self,
value: f64,
index: u16,
database: &mut DatabaseHandle,
) -> CommandStatus {
if index < 10 {
database.transaction(|db| {
db.update(
index,
&AnalogOutputStatus::new(value, Flags::ONLINE, get_current_time()),
UpdateOptions::detect_event(),
);
});
CommandStatus::Success
} else {
CommandStatus::NotSupported
}
}
}

impl ControlSupport<Group41Var1> for ExampleControlHandler {
fn select(
&mut self,
_control: Group41Var1,
index: u16,
_database: &mut DatabaseHandle,
) -> CommandStatus {
self.select_analog_output(index)
}

fn operate(
&mut self,
control: Group41Var1,
index: u16,
_op_type: OperateType,
database: &mut DatabaseHandle,
) -> CommandStatus {
self.operate_analog_output(control.value as f64, index, database)
}
}

impl ControlSupport<Group41Var2> for ExampleControlHandler {
fn select(
&mut self,
_control: Group41Var2,
index: u16,
_database: &mut DatabaseHandle,
) -> CommandStatus {
self.select_analog_output(index)
}

fn operate(
&mut self,
control: Group41Var2,
index: u16,
_op_type: OperateType,
database: &mut DatabaseHandle,
) -> CommandStatus {
self.operate_analog_output(control.value as f64, index, database)
}
}

impl ControlSupport<Group41Var3> for ExampleControlHandler {
fn select(
&mut self,
_control: Group41Var3,
index: u16,
_database: &mut DatabaseHandle,
) -> CommandStatus {
self.select_analog_output(index)
}

fn operate(
&mut self,
control: Group41Var3,
index: u16,
_op_type: OperateType,
database: &mut DatabaseHandle,
) -> CommandStatus {
self.operate_analog_output(control.value as f64, index, database)
}
}

impl ControlSupport<Group41Var4> for ExampleControlHandler {
fn select(
&mut self,
_control: Group41Var4,
index: u16,
_database: &mut DatabaseHandle,
) -> CommandStatus {
self.select_analog_output(index)
}

fn operate(
&mut self,
control: Group41Var4,
index: u16,
_op_type: OperateType,
database: &mut DatabaseHandle,
) -> CommandStatus {
self.operate_analog_output(control.value, index, database)
}
}