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:
OPERATEfunction code preceded by a matchingSELECT.DIRECT_OPERATEfunction code (single-pass control with response).DIRECT_OPERATE_NO_RESPONSEfunction 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.
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
- Rust
- C
- C++
- Java
- C#
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)
}
}
void update_binary_output_status_from_control(dnp3_database_t *database, void *context)
{
dnp3_binary_output_status_t value = *(dnp3_binary_output_status_t *)context;
dnp3_database_update_binary_output_status(database, value, dnp3_update_options_detect_event());
}
void update_analog_output_status_from_control(dnp3_database_t *database, void *context)
{
dnp3_analog_output_status_t value = *(dnp3_analog_output_status_t *)context;
dnp3_database_update_analog_output_status(database, value, dnp3_update_options_detect_event());
}
dnp3_command_status_t select_analog_output(uint16_t index)
{
return (index < POINT_COUNT) ? DNP3_COMMAND_STATUS_SUCCESS : DNP3_COMMAND_STATUS_NOT_SUPPORTED;
}
dnp3_command_status_t operate_analog_output(double value, uint16_t index, dnp3_database_handle_t *database)
{
if (index < POINT_COUNT) {
dnp3_analog_output_status_t ao = dnp3_analog_output_status_init(index, value, dnp3_flags_init(DNP3_FLAG_ONLINE), now());
dnp3_database_transaction_t transaction = {
.execute = &update_analog_output_status_from_control,
.on_destroy = NULL,
.ctx = &ao,
};
dnp3_database_handle_transaction(database, transaction);
return DNP3_COMMAND_STATUS_SUCCESS;
}
else {
return DNP3_COMMAND_STATUS_NOT_SUPPORTED;
}
}
dnp3_command_status_t select_g12v1(dnp3_group12_var1_t control, uint16_t index, dnp3_database_handle_t *database, void *context)
{
if (index < POINT_COUNT && (control.code.op_type == DNP3_OP_TYPE_LATCH_ON || control.code.op_type == DNP3_OP_TYPE_LATCH_OFF)) {
return DNP3_COMMAND_STATUS_SUCCESS;
}
else {
return DNP3_COMMAND_STATUS_NOT_SUPPORTED;
}
}
dnp3_command_status_t operate_g12v1(dnp3_group12_var1_t control, uint16_t index, dnp3_operate_type_t op_type, dnp3_database_handle_t *database, void *context)
{
printf("operate %s on binary output %u via %s\n", dnp3_op_type_to_string(control.code.op_type), index, dnp3_operate_type_to_string(op_type));
if (index < POINT_COUNT && (control.code.op_type == DNP3_OP_TYPE_LATCH_ON || control.code.op_type == DNP3_OP_TYPE_LATCH_OFF)) {
bool status = (control.code.op_type == DNP3_OP_TYPE_LATCH_ON);
dnp3_binary_output_status_t bo = dnp3_binary_output_status_init(index, status, dnp3_flags_init(DNP3_FLAG_ONLINE), now());
dnp3_database_transaction_t transaction = {
.execute = &update_binary_output_status_from_control,
.on_destroy = NULL,
.ctx = &bo,
};
dnp3_database_handle_transaction(database, transaction);
return DNP3_COMMAND_STATUS_SUCCESS;
}
else {
return DNP3_COMMAND_STATUS_NOT_SUPPORTED;
}
}
dnp3_command_status_t select_g41v1(int32_t value, uint16_t index, dnp3_database_handle_t *database, void *context)
{
return select_analog_output(index);
}
dnp3_command_status_t operate_g41v1(int32_t value, uint16_t index, dnp3_operate_type_t op_type, dnp3_database_handle_t *database, void *context)
{
return operate_analog_output((double)value, index, database);
}
dnp3_command_status_t select_g41v2(int16_t value, uint16_t index, dnp3_database_handle_t *database, void *context)
{
return select_analog_output(index);
}
dnp3_command_status_t operate_g41v2(int16_t value, uint16_t index, dnp3_operate_type_t op_type, dnp3_database_handle_t *database, void *context)
{
return operate_analog_output((double)value, index, database);
}
dnp3_command_status_t select_g41v3(float value, uint16_t index, dnp3_database_handle_t *database, void *context)
{
return select_analog_output(index);
}
dnp3_command_status_t operate_g41v3(float value, uint16_t index, dnp3_operate_type_t op_type, dnp3_database_handle_t *database, void *context)
{
return operate_analog_output((double)value, index, database);
}
dnp3_command_status_t select_g41v4(double value, uint16_t index, dnp3_database_handle_t *database, void *context)
{
return select_analog_output(index);
}
dnp3_command_status_t operate_g41v4(double value, uint16_t index, dnp3_operate_type_t op_type, dnp3_database_handle_t *database, void *context)
{
return operate_analog_output(value, index, database);
}
dnp3_control_handler_t get_control_handler(void)
{
return (dnp3_control_handler_t){
.select_g12v1 = &select_g12v1,
.operate_g12v1 = &operate_g12v1,
.select_g41v1 = &select_g41v1,
.operate_g41v1 = &operate_g41v1,
.select_g41v2 = &select_g41v2,
.operate_g41v2 = &operate_g41v2,
.select_g41v3 = &select_g41v3,
.operate_g41v3 = &operate_g41v3,
.select_g41v4 = &select_g41v4,
.operate_g41v4 = &operate_g41v4,
.on_destroy = NULL,
.ctx = NULL,
};
}
// check error
class ExampleControlHandler : public ControlHandler {
public:
void begin_fragment() override {}
void end_fragment(DatabaseHandle &database) override {}
CommandStatus select_g12v1(const Group12Var1 &control, uint16_t index, DatabaseHandle &database) override
{
if (index < POINT_COUNT && (control.code.op_type == OpType::latch_on || control.code.op_type == OpType::latch_off)) {
return CommandStatus::success;
}
else {
return CommandStatus::not_supported;
}
}
CommandStatus operate_g12v1(const Group12Var1 &control, uint16_t index, OperateType op_type, DatabaseHandle &database) override
{
std::cout << "operate " << to_string(control.code.op_type) << " on binary output " << index << " via " << to_string(op_type) << std::endl;
if (index < POINT_COUNT && (control.code.op_type == OpType::latch_on || control.code.op_type == OpType::latch_off)) {
auto status = (control.code.op_type == OpType::latch_on);
auto transaction = database_transaction([=](Database &db) {
db.update_binary_output_status(BinaryOutputStatus(index, status, online(), now()), UpdateOptions::detect_event());
});
database.transaction(transaction);
return CommandStatus::success;
}
else {
return CommandStatus::not_supported;
}
}
CommandStatus select_g41v1(int32_t value, uint16_t index, DatabaseHandle &database) override { return select_analog_output(index); }
CommandStatus operate_g41v1(int32_t value, uint16_t index, OperateType op_type, DatabaseHandle &database) override
{
return operate_analog_output(value, index, database);
}
CommandStatus select_g41v2(int16_t value, uint16_t index, DatabaseHandle &database) override { return select_analog_output(index); }
CommandStatus operate_g41v2(int16_t value, uint16_t index, OperateType op_type, DatabaseHandle &database) override
{
return operate_analog_output(value, index, database);
}
CommandStatus select_g41v3(float value, uint16_t index, DatabaseHandle &database) override { return select_analog_output(index); }
CommandStatus operate_g41v3(float value, uint16_t index, OperateType op_type, DatabaseHandle &database) override
{
return operate_analog_output(value, index, database);
}
CommandStatus select_g41v4(double value, uint16_t index, DatabaseHandle &database) override { return select_analog_output(index); }
CommandStatus operate_g41v4(double value, uint16_t index, OperateType op_type, DatabaseHandle &database) override
{
return operate_analog_output(value, index, database);
}
private:
CommandStatus select_analog_output(uint16_t index) { return index < POINT_COUNT ? CommandStatus::success : CommandStatus::not_supported; }
CommandStatus operate_analog_output(double value, uint16_t index, DatabaseHandle &database)
{
if (index < POINT_COUNT) {
auto transaction = database_transaction([=](Database &db) {
db.update_analog_output_status(AnalogOutputStatus(index, value, online(), now()), UpdateOptions::detect_event());
});
database.transaction(transaction);
return CommandStatus::success;
}
else {
return CommandStatus::not_supported;
}
}
};
// check error
class ExampleControlHandler implements ControlHandler {
@Override
public void beginFragment() {}
@Override
public void endFragment(DatabaseHandle database) {}
@Override
public CommandStatus selectG12v1(Group12Var1 control, UShort index, DatabaseHandle database) {
if (index.intValue() < OutstationExample.POINT_COUNT && (control.code.opType == OpType.LATCH_ON || control.code.opType == OpType.LATCH_OFF)) {
return CommandStatus.SUCCESS;
} else {
return CommandStatus.NOT_SUPPORTED;
}
}
@Override
public CommandStatus operateG12v1(Group12Var1 control, UShort index, OperateType opType, DatabaseHandle database) {
System.out.println("operate " + control.code.opType + " on binary output " + index + " via " + opType);
if (index.intValue() < OutstationExample.POINT_COUNT && (control.code.opType == OpType.LATCH_ON || control.code.opType == OpType.LATCH_OFF)) {
boolean status = control.code.opType == OpType.LATCH_ON;
database.transaction(db -> db.updateBinaryOutputStatus(new BinaryOutputStatus(index, status, new Flags(Flag.ONLINE), OutstationExample.now()), UpdateOptions.detectEvent()));
return CommandStatus.SUCCESS;
} else {
return CommandStatus.NOT_SUPPORTED;
}
}
@Override
public CommandStatus selectG41v1(int value, UShort index, DatabaseHandle database) {
return selectAnalogOutput(index);
}
@Override
public CommandStatus operateG41v1(
int value, UShort index, OperateType opType, DatabaseHandle database) {
return operateAnalogOutput(value, index, database);
}
@Override
public CommandStatus selectG41v2(short value, UShort index, DatabaseHandle database) {
return selectAnalogOutput(index);
}
@Override
public CommandStatus operateG41v2(
short value, UShort index, OperateType opType, DatabaseHandle database) {
return operateAnalogOutput(value, index, database);
}
@Override
public CommandStatus selectG41v3(float value, UShort index, DatabaseHandle database) {
return selectAnalogOutput(index);
}
@Override
public CommandStatus operateG41v3(
float value, UShort index, OperateType opType, DatabaseHandle database) {
return operateAnalogOutput(value, index, database);
}
@Override
public CommandStatus selectG41v4(double value, UShort index, DatabaseHandle database) {
return selectAnalogOutput(index);
}
@Override
public CommandStatus operateG41v4(
double value, UShort index, OperateType opType, DatabaseHandle database) {
return operateAnalogOutput(value, index, database);
}
private CommandStatus selectAnalogOutput(UShort index) {
return index.intValue() < OutstationExample.POINT_COUNT ? CommandStatus.SUCCESS : CommandStatus.NOT_SUPPORTED;
}
private CommandStatus operateAnalogOutput(double value, UShort index, DatabaseHandle database) {
if (index.intValue() < OutstationExample.POINT_COUNT) {
database.transaction(db -> db.updateAnalogOutputStatus(new AnalogOutputStatus(index, value, new Flags(Flag.ONLINE), OutstationExample.now()), UpdateOptions.detectEvent()));
return CommandStatus.SUCCESS;
} else {
return CommandStatus.NOT_SUPPORTED;
}
}
}
class ExampleControlHandler : IControlHandler
{
public void BeginFragment() { }
public void EndFragment(DatabaseHandle database) { }
public CommandStatus SelectG12v1(Group12Var1 control, ushort index, DatabaseHandle database)
{
if (index < PointCount && (control.Code.OpType == OpType.LatchOn || control.Code.OpType == OpType.LatchOff))
{
return CommandStatus.Success;
}
else
{
return CommandStatus.NotSupported;
}
}
public CommandStatus OperateG12v1(Group12Var1 control, ushort index, OperateType opType, DatabaseHandle database)
{
Console.WriteLine($"operate {control.Code.OpType} on binary output {index} via {opType}");
if (index < PointCount && (control.Code.OpType == OpType.LatchOn || control.Code.OpType == OpType.LatchOff))
{
var status = (control.Code.OpType == OpType.LatchOn);
database.Transaction(db =>
db.UpdateBinaryOutputStatus(new BinaryOutputStatus(index, status, new Flags(Flag.Online), Now()), UpdateOptions.DetectEvent())
);
return CommandStatus.Success;
}
else
{
return CommandStatus.NotSupported;
}
}
public CommandStatus SelectG41v1(int value, ushort index, DatabaseHandle database)
{
return SelectAnalogOutput(index);
}
public CommandStatus OperateG41v1(int value, ushort index, OperateType opType, DatabaseHandle database)
{
return OperateAnalogOutput(value, index, database);
}
public CommandStatus SelectG41v2(short value, ushort index, DatabaseHandle database)
{
return SelectAnalogOutput(index);
}
public CommandStatus OperateG41v2(short value, ushort index, OperateType opType, DatabaseHandle database)
{
return OperateAnalogOutput(value, index, database);
}
public CommandStatus SelectG41v3(float value, ushort index, DatabaseHandle database)
{
return SelectAnalogOutput(index);
}
public CommandStatus OperateG41v3(float value, ushort index, OperateType opType, DatabaseHandle database)
{
return OperateAnalogOutput(value, index, database);
}
public CommandStatus SelectG41v4(double value, ushort index, DatabaseHandle database)
{
return SelectAnalogOutput(index);
}
public CommandStatus OperateG41v4(double value, ushort index, OperateType opType, DatabaseHandle database)
{
return OperateAnalogOutput(value, index, database);
}
private CommandStatus SelectAnalogOutput(ushort index)
{
return index < PointCount ? CommandStatus.Success : CommandStatus.NotSupported;
}
private CommandStatus OperateAnalogOutput(double value, ushort index, DatabaseHandle database)
{
if (index < PointCount)
{
database.Transaction(db =>
db.UpdateAnalogOutputStatus(new AnalogOutputStatus(index, value, new Flags(Flag.Online), Now()), UpdateOptions.DetectEvent())
);
return CommandStatus.Success;
}
else
{
return CommandStatus.NotSupported;
}
}
}