Outstations
An outstation in opendnp3 is a component that communicates with a single master via a communication channel. It makes measurements of the physical world and then sends them to a master upon request (solicited) or on its own accord (unsolicited). Occasionally a master requests that it do something by sending it a control. Just like a master, an outstation can be attached to any communication channel that opendnp3 supports.
To add an outstation to a communication channel you call the AddOutstation
method on the channel interface:
// default configuration w/ an empty database OutstationStackConfig config; // configure database points config.database = DatabaseConfig(10); // 10 of each type with default settings // adjust some settings for analog 0 to non-default values config.database.analog_input[0].clazz = PointClass::Class2; config.database.analog_input[0].svariation = StaticAnalogVariation::Group30Var5; config.database.analog_input[0].evariation = EventAnalogVariation::Group32Var7; // configure the size of the event buffers config.outstation.eventBufferConfig = EventBufferConfig::AllTypes(10); // you can override a default outstation parameters here config.outstation.params.allowUnsolicited = true; // You can override the default link layer settings here // in this example we've changed the default link layer addressing config.link.LocalAddr = 10; config.link.RemoteAddr = 1; auto outstation = channel->AddOutstation( "outstation", // alias for logging SuccessCommandHandler::Create(), // ICommandHandler (interface) DefaultOutstationApplication::Create(), // IOutstationApplication (interface) config // static stack configuration ); outstation->Enable();
UpdateBuilder
When a new measurement is read from an input or a new value is received from a downstream protocol, you need to update the corresponding
value in the outstation. This is accomplished with the UpdateBuilder
and corresponding Updates
class.
UpdateBuilder builder; builder.Update(Counter(state.count), 0); builder.Update(Analog(state.value), 0); // ... update more types and indices // finalize the set of updates auto updates = builder.Build(); // apply the updates to one or more outstations outstation->apply(updates);
The update is atomic. All of the updated values are applied to the outstation database and event buffers at the same time. The Updates
instance
returned from UpdateBuilder::Build()
can be safely sent to any number of outstation instances. The outstation automatically decides if these updates
produce events. How events are detected are defined within the DNP3 standard, and varies from type to type. Analogs and counters can use deadbands
to ensure that unimportant changes are not reported.
ICommandHandler
When the outstation receives a control request, it dispatches individual commands in the message to the ICommandhandler
interface
supplied when the outstation was added to the channel.
class ICommandHandler { /** * called when a command APDU begins processing */ virtual void Begin() = 0; /** * called when a command APDU ends processing */ virtual void End() = 0; virtual CommandStatus Select(const ControlRelayOutputBlock& command, uint16_t index) = 0; virtual CommandStatus Operate(const ControlRelayOutputBlock& command, uint16_t index, IUpdateHandler& handler, OperateType opType) = 0; /// ... additional methods for the 4 types of analog outputs - Group 41Var[1-4] }
The Begin()/End() methods tell you when an ASDU containing commands begins and ends. Many applications probably don't care, but this knowledge is there if you need it for some reason.
The Select
method shouldn't actually perform the command. Think of it as a question along the lines of "Is this operation supported?".
Select-Before-Operate (SBO) is an artifact of the days before the the CRC integrity checks were really truted. It's a 2-pass control scheme where
the outstation verifies that the select/operate are identical. It was intended as an additional protection against data corruption on noisy
transports like radio and modems.
The Operate
method is called from a successful SBO sequence or from a DirectOperate
or DirectOperateNoAck
request. Applications shouldn't
care how the request came in, but the OperateType
parameter provides an enumeration that can be used to reject certain operations or to forward
the same mode downstream in gateway applications.
enum class OperateType : uint8_t { /// The outstation received a valid prior SELECT followed by OPERATE SelectBeforeOperate = 0x0, /// The outstation received a direct operate request DirectOperate = 0x1, /// The outstation received a direct operate no ack request DirectOperateNoAck = 0x2 };
The IUpdateHandler
provides the ability to update points as a direct result of the control. This is a convience since you would
otherwise have to pass your implementation of ICommandHandler
an instance of IOutstation
after you create it. Frequently, implemenations
want to mirror binary and analog output operations to binary output status and analog output status points directly in the handler.
CommandStatus
You must immediately return a CommandStatus
enumeration value in response to each callback. This callback should never block.
enum class CommandStatus : uint8_t { /// command was accepted, initiated, or queued SUCCESS = 0, /// command timed out before completing TIMEOUT = 1, /// command requires being selected before operate, configuration issue NO_SELECT = 2, /// more values ... }
The enumeration contains about ~18 different values, and you should refer to 1815 or the code comments for a description of each. In general,
you'll be choosing SUCCESS
or some kind of error code.
It's important to understand that SUCCESS
doesn't imply that the command was synchronously executed. It really just means that the command
was received and queued. Some devices can synchronously process a command, e.g. quickly writing to memory mapped I/O, but you'd never
want to block in a gateway application to perform a downstream Modbus transaction. You'd pass the control of to another thread or queue the operation
in some way for subsequent processing.