Skip to main content

ControlHandler interface

The ControlHandler interface receives control requests. When a request results in at least one select or operate callback, the callbacks are preceded by beginFragment and followed by endFragment, useful for handling operations atomically or in batches. These two methods are not called if no control callback runs.

Each select or operate callback returns a CommandStatus enum. The outstation response to a control message echoes the request data along with the status returned for each control. If an OPERATE doesn't match a prior SELECT, no callbacks are made and the same failure status is returned for every control in the message.

Return NOT_SUPPORTED for an index that doesn't exist.

Handling SELECT​

The select* methods of ControlHandler are called when a properly formatted SELECT message 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?"

DNP3 masters can also use a "select-before-operate" strategy. The library handles these two-pass control messages 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 DatabaseHandle reference lets you update point values in response to controls. Typically used for BinaryOutputStatus and AnalogOutputStatus, but you can update other types too—for example, tracking control operations in a counter.

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.

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 to be supported and result in the same action.

The example below implements a ControlHandler in each language.

Show the full example
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 < POINT_COUNT
&& (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 {
tracing::info!(
"operate {:?} on binary output {index} via {op_type:?}",
control.code.op_type
);
if index < POINT_COUNT
&& (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 < POINT_COUNT {
CommandStatus::Success
} else {
CommandStatus::NotSupported
}
}

fn operate_analog_output(
&self,
value: f64,
index: u16,
database: &mut DatabaseHandle,
) -> CommandStatus {
if index < POINT_COUNT {
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)
}
}