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?"
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 matchingSELECT
.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.
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 be supported and result in the same action.
- 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 < 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)
}
}
void update_binary_output_status_from_control(dnp3_database_t *database, void *ctx)
{
dnp3_binary_output_status_t value = *(dnp3_binary_output_status_t *)ctx;
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 *ctx)
{
dnp3_analog_output_status_t value = *(dnp3_analog_output_status_t *)ctx;
dnp3_database_update_analog_output_status(database, value, dnp3_update_options_detect_event());
}
void begin_fragment(void *context) {}
void end_fragment(dnp3_database_handle_t *database, void *context) {}
dnp3_command_status_t select_g12v1(dnp3_group12_var1_t control, uint16_t index, dnp3_database_handle_t *database, void *context)
{
if (index < 10 && (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)
{
if (index < 10 && (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_analog_output(uint16_t index)
{
return (index < 10) ? 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 < 10)
{
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_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);
}
// check error
class MyControlHandler : public ControlHandler {
void begin_fragment() override {}
void end_fragment(DatabaseHandle& database) override {}
CommandStatus select_g12v1(const Group12Var1& control, uint16_t index, DatabaseHandle& database) override {
if (index < 10 && (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
{
if (index < 10 && (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 = functional::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 < 10 ? CommandStatus::success : CommandStatus::not_supported;
}
CommandStatus operate_analog_output(double value, uint16_t index, DatabaseHandle& database)
{
if (index < 10)
{
auto transaction = functional::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 TestControlHandler implements ControlHandler {
@Override
public void beginFragment() {}
@Override
public void endFragment(DatabaseHandle database) {}
@Override
public CommandStatus selectG12v1(Group12Var1 control, UShort index, DatabaseHandle database) {
if (index.compareTo(ushort(10)) < 0 && (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) {
if (index.compareTo(ushort(10)) < 0 && (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.compareTo(ushort(10)) < 0 ? CommandStatus.SUCCESS : CommandStatus.NOT_SUPPORTED;
}
private CommandStatus operateAnalogOutput(double value, UShort index, DatabaseHandle database) {
if (index.compareTo(ushort(10)) < 0) {
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 TestControlHandler : IControlHandler
{
public void BeginFragment() { }
public void EndFragment(DatabaseHandle database) { }
public CommandStatus SelectG12v1(Group12Var1 control, ushort index, DatabaseHandle database)
{
if (index < 10 && (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)
{
if (index < 10 && (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 < 10 ? CommandStatus.Success : CommandStatus.NotSupported;
}
private CommandStatus OperateAnalogOutput(double value, ushort index, DatabaseHandle database)
{
if (index < 10)
{
database.Transaction(db =>
db.UpdateAnalogOutputStatus(new AnalogOutputStatus(index, value, new Flags(Flag.Online), Now()), UpdateOptions.DetectEvent())
);
return CommandStatus.Success;
}
else
{
return CommandStatus.NotSupported;
}
}
}