Database
Use the Database class to manage measurements the outstation exposes to the master. Despite the name, it's just an 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.
Access the database in a transaction two ways:
-
Call
Outstation.transactionto acquire a locked reference and make changes. -
ControlHandlercallbacks andOutstationApplicationfreeze operations provide aDatabaseHandlewith an identicaltransactionmethod.
Structure any common update code to operate on the Database type and not depend on Outstation or DatabaseHandle.
Adding points
Initialize points before the outstation exposes measurement data. This is typically done at creation, but you can add points to a running outstation. Each measurement type has unique configuration:
- An optional event class assignment for the point, which a master can change at runtime (see saving what the master changes)
- Default static and event variations for the type
- Dead-bands for analog and counter types that default to zero (other types have no deadband)
The default static and event variations for each point type may not do what you expect them to do. For example, some DNP3 event variations don't carry a timestamp. This can be confusing to new users who expect to see a timestamp value that changes as the outstation receives events.
See the documentation for each variation to understand its data.
A new point starts with a default value, such as 0 or false, and the RESTART flag. Update it after adding it if a
connecting master shouldn't see RESTART.
Point indices don't need to be contiguous.
- Rust
- C
- C++
- Java
- C#
outstation.transaction(|db| {
// initialize POINT_COUNT points of each type
for i in 0..POINT_COUNT {
db.add(
i,
Some(EventClass::Class1),
// you can explicitly specify the configuration for each point ...
BinaryInputConfig {
s_var: StaticBinaryInputVariation::Group1Var1,
e_var: EventBinaryInputVariation::Group2Var2,
},
);
db.add(
i,
Some(EventClass::Class1),
// ... or just use the defaults
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 {
s_var: StaticAnalogInputVariation::Group30Var1,
e_var: EventAnalogInputVariation::Group32Var1,
deadband: 0.0,
},
);
db.add(
i,
Some(EventClass::Class1),
AnalogOutputStatusConfig::default(),
);
db.add(i, Some(EventClass::Class1), OctetStringConfig);
}
// define device attributes made available to the master
let _ = db.define_attr(
AttrProp::default(),
StringAttr::DeviceManufacturersName.with_value("Step Function I/O"),
);
let _ = db.define_attr(
AttrProp::writable(),
StringAttr::UserAssignedLocation.with_value("Bend, OR"),
);
});
// initialize 10 of every point type
void outstation_transaction_startup(dnp3_database_t *db, void *context)
{
// initialize POINT_COUNT points of each type
for (uint16_t i = 0; i < POINT_COUNT; ++i) {
// you can explicitly specify the configuration for each point ...
dnp3_database_add_binary_input(db, i, DNP3_EVENT_CLASS_CLASS1,
dnp3_binary_input_config_create(DNP3_STATIC_BINARY_INPUT_VARIATION_GROUP1_VAR1, DNP3_EVENT_BINARY_INPUT_VARIATION_GROUP2_VAR2)
);
// ... or just use the defaults
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_create(DNP3_STATIC_ANALOG_INPUT_VARIATION_GROUP30_VAR1, DNP3_EVENT_ANALOG_INPUT_VARIATION_GROUP32_VAR1, 0.0)
);
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);
}
// define device attributes made available to the master
dnp3_database_define_string_attr(db, 0, false, DNP3_ATTRIBUTE_VARIATIONS_DEVICE_MANUFACTURERS_NAME, "Step Function I/O");
dnp3_database_define_string_attr(db, 0, true, DNP3_ATTRIBUTE_VARIATIONS_USER_ASSIGNED_LOCATION, "Bend, OR");
}
// 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) {
// initialize POINT_COUNT points of each type
for (uint16_t i = 0; i < POINT_COUNT; ++i) {
// you can explicitly specify the configuration for each point ...
db.add_binary_input(
i,
EventClass::class1,
BinaryInputConfig(StaticBinaryInputVariation::group1_var1, EventBinaryInputVariation::group2_var2)
);
// ... or just use the defaults
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(StaticAnalogInputVariation::group30_var1, EventAnalogInputVariation::group32_var1, 0.0)
);
db.add_analog_output_status(i, EventClass::class1, AnalogOutputStatusConfig());
db.add_octet_string(i, EventClass::class1);
}
// define device attributes made available to the master
db.define_string_attr(0, false, attribute_variations::device_manufacturers_name, "Step Function I/O");
db.define_string_attr(0, true, attribute_variations::user_assigned_location, "Bend, OR");
});
outstation.transaction(setup);
// you can use a separate method or just initialize directly in the lambda expression
private static void initializeDatabase(Database db) {
// add POINT_COUNT points of each type
for (int i = 0; i < POINT_COUNT; i++) {
// you can explicitly specify the configuration for each point ...
db.addBinaryInput(ushort(i), EventClass.CLASS1, new BinaryInputConfig(StaticBinaryInputVariation.GROUP1_VAR1, EventBinaryInputVariation.GROUP2_VAR2));
// ... or just use the defaults
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(StaticAnalogInputVariation.GROUP30_VAR1, EventAnalogInputVariation.GROUP32_VAR1, 0.0));
db.addAnalogOutputStatus(ushort(i), EventClass.CLASS1, new AnalogOutputStatusConfig());
db.addOctetString(ushort(i), EventClass.CLASS1);
}
// define device attributes made available to the master
db.defineStringAttr(ubyte(0), false, AttributeVariations.DEVICE_MANUFACTURERS_NAME, "Step Function I/O");
db.defineStringAttr(ubyte(0), true, AttributeVariations.USER_ASSIGNED_LOCATION, "Bend, OR");
}
// during program initialization - "outstation" already created
outstation.transaction(db -> initializeDatabase(db));
outstation.Transaction(db =>
{
// initialize PointCount points of each type
for (ushort i = 0; i < PointCount; i++)
{
// you can explicitly specify the configuration for each point ...
db.AddBinaryInput(i, EventClass.Class1,
new BinaryInputConfig(StaticBinaryInputVariation.Group1Var1, EventBinaryInputVariation.Group2Var2)
);
// ... or just use the defaults
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(StaticAnalogInputVariation.Group30Var1, EventAnalogInputVariation.Group32Var1, 0.0)
);
db.AddAnalogOutputStatus(i, EventClass.Class1, new AnalogOutputStatusConfig());
db.AddOctetString(i, EventClass.Class1);
}
// define device attributes made available to the master
db.DefineStringAttr(0, false, AttributeVariations.DeviceManufacturersName, "Step Function I/O");
db.DefineStringAttr(0, true, AttributeVariations.UserAssignedLocation, "Bend, OR");
});
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 C++, Java, or .NET, 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 it. 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.
Defining device attributes
Specific attributes from Group 0 may be defined using the database. As you can see in the examples above, this should typically be done during database initialization. Rust uses a single method for defining attributes whereas the bindings have different methods for each type of attribute. Only attributes that are defined as writable are eligible to receive WRITE callbacks in the OutstationApplication interface.
The default set (set 0) holds attributes the standard defines. The database checks their types and lets only the user-assigned ones, such as location, owner and device name, be writable. Any other set is private, and you choose its variations.