Database
You can use the Database
class to manipulate the measurement types and values that the outstation exposes to the master. Note that while it's called a "database", it's really just
a in-memory data structure protected by a mutex.
All database operations are executed inside a transaction protected by a mutex. Operations within a transaction are applied to the database and the event buffers atomically. If unsolicited responses are enabled, the outstation will automatically decide if an unsolicited response should be sent at the end of the transaction.
The database may be accessed in a transaction in two different ways:
When measurement values need to updated due to some external changes, the user can call
Outstation.transaction
to acquire a locked reference to the database and make changes.Callbacks on the
ControlHandler
provide aDatabaseHandle
which also has an identicaltransaction method
. Similarly, the freeze operations onOutstationApplication
provide this handle as well.
Structure any common update code to operate on the Database
type and not depend on Outstation
or DatabaseHandle
.
Adding Points
You must initialize the points before the outstation exposes any measurement data. While you should do this when you create the outstation, you can add points to a running outstation as well. Each measurement type has unique configuration including:
- An optional event class assignment for the point
- Default static and event variations for the type
- Type-specific dead-bands that default to zero (Binary points have no deadband)
When you add a point, it is assigned the following default value with RESTART
flags:
- Binary points are set to
false
- Numeric values are set to
0
- Double-bit Binary points set to
Indeterminate
- Octet Strings are set to the value of
[0x00]
Update the value after adding the point if you don't want a connecting master to see the points with a RESTART
flag set.
- Rust
- C
- C++
- Java
- C#
outstation.transaction(|db| {
for i in 0..10 {
db.add(i, Some(EventClass::Class1), BinaryInputConfig::default());
db.add(
i,
Some(EventClass::Class1),
DoubleBitBinaryInputConfig::default(),
);
db.add(
i,
Some(EventClass::Class1),
BinaryOutputStatusConfig::default(),
);
db.add(i, Some(EventClass::Class1), CounterConfig::default());
db.add(i, Some(EventClass::Class1), FrozenCounterConfig::default());
db.add(i, Some(EventClass::Class1), AnalogInputConfig::default());
db.add(
i,
Some(EventClass::Class1),
AnalogOutputStatusConfig::default(),
);
db.add(i, Some(EventClass::Class1), OctetStringConfig);
}
});
// initialize 10 of every point type
void outstation_transaction_startup(dnp3_database_t *db, void *context)
{
for (uint16_t i = 0; i < 10; ++i) {
// initialize each point with default configuration
dnp3_database_add_binary_input(db, i, DNP3_EVENT_CLASS_CLASS1, dnp3_binary_input_config_init());
dnp3_database_add_double_bit_binary_input(db, i, DNP3_EVENT_CLASS_CLASS1, dnp3_double_bit_binary_input_config_init());
dnp3_database_add_binary_output_status(db, i, DNP3_EVENT_CLASS_CLASS1, dnp3_binary_output_status_config_init());
dnp3_database_add_counter(db, i, DNP3_EVENT_CLASS_CLASS1, dnp3_counter_config_init());
dnp3_database_add_frozen_counter(db, i, DNP3_EVENT_CLASS_CLASS1, dnp3_frozen_counter_config_init());
dnp3_database_add_analog_input(db, i, DNP3_EVENT_CLASS_CLASS1, dnp3_analog_input_config_init());
dnp3_database_add_analog_output_status(db, i, DNP3_EVENT_CLASS_CLASS1, dnp3_analog_output_status_config_init());
dnp3_database_add_octet_string(db, i, DNP3_EVENT_CLASS_CLASS1);
}
}
// during program initialization - "outstation" already created
dnp3_database_transaction_t startup_transaction = {
.execute = &outstation_transaction_startup,
.on_destroy = NULL,
.ctx = NULL,
};
dnp3_outstation_transaction(outstation, startup_transaction);
// initialize 10 of every point type
auto setup = database_transaction([](Database &db) {
// add 5 of each type
for (uint16_t i = 0; i < 10; ++i) {
db.add_binary_input(i, EventClass::class1, BinaryInputConfig());
db.add_double_bit_binary_input(i, EventClass::class1, DoubleBitBinaryInputConfig());
db.add_binary_output_status(i, EventClass::class1, BinaryOutputStatusConfig());
db.add_counter(i, EventClass::class1, CounterConfig());
db.add_frozen_counter(i, EventClass::class1, FrozenCounterConfig());
db.add_analog_input(i, EventClass::class1, AnalogInputConfig());
db.add_analog_output_status(i, EventClass::class1, AnalogOutputStatusConfig());
db.add_octet_string(i, EventClass::class1);
}
});
outstation.transaction(setup);
// you can use a separate method or just initialize directly in the lambda expression
private static void initializeDatabase(Database db) {
for (int i = 0; i < 10; i++) {
db.addBinaryInput(ushort(i), EventClass.CLASS1, new BinaryInputConfig());
db.addDoubleBitBinaryInput(ushort(i), EventClass.CLASS1, new DoubleBitBinaryInputConfig());
db.addBinaryOutputStatus(ushort(i), EventClass.CLASS1, new BinaryOutputStatusConfig());
db.addCounter(ushort(i), EventClass.CLASS1, new CounterConfig());
db.addFrozenCounter(ushort(i), EventClass.CLASS1, new FrozenCounterConfig());
db.addAnalogInput(ushort(i), EventClass.CLASS1, new AnalogInputConfig());
db.addAnalogOutputStatus(ushort(i), EventClass.CLASS1, new AnalogOutputStatusConfig());
db.addOctetString(ushort(i), EventClass.CLASS1);
}
}
// during program initialization - "outstation" already created
outstation.transaction((db) -> initializeDatabase(db));
outstation.Transaction(db =>
{
for (ushort i = 0; i < 10; i++)
{
// add points with default values
db.AddBinaryInput(i, EventClass.Class1, new BinaryInputConfig());
db.AddDoubleBitBinaryInput(i, EventClass.Class1, new DoubleBitBinaryInputConfig());
db.AddBinaryOutputStatus(i, EventClass.Class1, new BinaryOutputStatusConfig());
db.AddCounter(i, EventClass.Class1, new CounterConfig());
db.AddFrozenCounter(i, EventClass.Class1, new FrozenCounterConfig());
db.AddAnalogInput(i, EventClass.Class1, new AnalogInputConfig());
db.AddAnalogOutputStatus(i, EventClass.Class1, new AnalogOutputStatusConfig());
db.AddOctetString(i, EventClass.Class1);
}
});
Updating Points
You can update a point value in a new transaction or in the same transaction you used to initialize it. This is useful if the outstation
has local access to values at startup, such as via a local ADC. When initializing point values, it is recommended to use the
UpdateOptions::no_event()
.
The Flags
value can be built by ORing values from the constants available in Flag
. Note that not all Flag
values are available
in every point type.
The UpdateOptions
struct lets you precisely control how a point update is processed. Use the default constructor to:
- Update the static value
- Produce an event if the point value changes in a way that exceeds the deadband or if the flags change
Use the UpdateOptions to ignore event creation during startup initialization if you don't want to create events for the initial values.
Getting Point Values
Some applications may wish to use the Database
as a cache of the most recent value. Each type has a getter method to retrieve the most recently assigned value.
Since the point may not be defined, the getters can fail. If you try to retrieve a point that doesn't exist using Java and C#, an exception will be thrown.
Removing Points
Most applications don't need to remove points, but the option is there in case you want to remove points from a running outstation. There is a type-specific function for removing every point type given its index.
Removing a point stops the outstation from reporting static data for that point. However, it does NOT remove any queued events for that point from the event buffer. Those events will remain in the event buffer until they are reported and cleared by confirmation.