Skip to content
This repository has been archived by the owner on Sep 1, 2022. It is now read-only.

Commit

Permalink
Merge pull request #403 from dnp3/feature/outstation-confirm-callbacks
Browse files Browse the repository at this point in the history
Add IOutstationApplication::OnConfirmProcess to C++, C#, and Java
  • Loading branch information
jadamcrain committed Oct 16, 2020
2 parents 639edaf + 6034a89 commit 5c6bf4d
Show file tree
Hide file tree
Showing 18 changed files with 177 additions and 29 deletions.
10 changes: 10 additions & 0 deletions cpp/lib/include/opendnp3/outstation/IOutstationApplication.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ class IOutstationApplication : public ILinkListener, public IDnpTimeSource
return 65535;
}

/// This method notifies that application code that an expected CONFIRM has been
/// received, and events may have cleared from the event buffer. It is informational
/// only.
///
/// @param is_unsolicited true, if the confirm is for an unsolicited response, false for a solicited response
/// @param num_class1 number of Class 1 events remaining in the event buffer after processing the confirm
/// @param num_class2 number of Class 2 events remaining in the event buffer after processing the confirm
/// @param num_class3 number of Class 3 events remaining in the event buffer after processing the confirm
virtual void OnConfirmProcessed(bool is_unsolicited, uint32_t num_class1, uint32_t num_class2, uint32_t num_class3) {}

virtual ~IOutstationApplication() = default;
};

Expand Down
16 changes: 16 additions & 0 deletions cpp/lib/src/outstation/OutstationStates.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ OutstationState& StateSolicitedConfirmWait::OnConfirm(OContext& ctx, const Parse
ctx.eventBuffer.ClearWritten();
ctx.lastBroadcastMessageReceived.clear();

// information the application about the confirm
ctx.application->OnConfirmProcessed(
false,
ctx.eventBuffer.NumEvents(EventClass::EC1),
ctx.eventBuffer.NumEvents(EventClass::EC2),
ctx.eventBuffer.NumEvents(EventClass::EC3)
);

if (ctx.rspContext.HasSelection())
{
return ctx.ContinueMultiFragResponse(request.addresses, AppSeqNum(request.header.control.SEQ).Next());
Expand Down Expand Up @@ -170,6 +178,14 @@ OutstationState& StateUnsolicitedConfirmWait::OnConfirm(OContext& ctx, const Par
ctx.confirmTimer.cancel();
ctx.lastBroadcastMessageReceived.clear();

// information the application about the confirm
ctx.application->OnConfirmProcessed(
true,
ctx.eventBuffer.NumEvents(EventClass::EC1),
ctx.eventBuffer.NumEvents(EventClass::EC2),
ctx.eventBuffer.NumEvents(EventClass::EC3)
);

if (ctx.unsol.completedNull)
{
ctx.eventBuffer.ClearWritten();
Expand Down
5 changes: 5 additions & 0 deletions cpp/lib/src/outstation/event/EventBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,9 @@ void EventBuffer::ClearWritten()
this->storage.ClearWritten();
}

uint32_t EventBuffer::NumEvents(EventClass ec) const
{
return this->storage.NumUnwritten(ec);
}

} // namespace opendnp3
3 changes: 3 additions & 0 deletions cpp/lib/src/outstation/event/EventBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ class EventBuffer final : public IEventReceiver, public IEventSelector, public I

void SelectAllByClass(const ClassField& clazz);

uint32_t NumEvents(EventClass ec) const;


private:
bool overflow = false;
EventStorage storage;
Expand Down
19 changes: 19 additions & 0 deletions cpp/tests/dnp3mocks/include/dnp3mocks/MockOutstationApplication.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
#include <deque>
#include <tuple>

struct ConfirmResult
{
bool is_unsolicited;
uint32_t num_class1;
uint32_t num_class2;
uint32_t num_class3;
};

class MockOutstationApplication : public opendnp3::IOutstationApplication
{
public:
Expand Down Expand Up @@ -119,6 +127,16 @@ class MockOutstationApplication : public opendnp3::IOutstationApplication
return warmRestartTimeDelay;
}

void OnConfirmProcessed(bool is_unsolicited, uint32_t num_class1, uint32_t num_class2, uint32_t num_class3) final
{
ConfirmResult confirm{};
confirm.is_unsolicited = is_unsolicited;
confirm.num_class1 = num_class1;
confirm.num_class2 = num_class2;
confirm.num_class3 = num_class3;
this->confirms.push_back(confirm);
}

void SetTime(opendnp3::DNPTime time)
{
this->currentTime = time;
Expand All @@ -143,6 +161,7 @@ class MockOutstationApplication : public opendnp3::IOutstationApplication
std::deque<opendnp3::UTCTimestamp> timestamps;
std::deque<std::tuple<opendnp3::AssignClassType, opendnp3::PointClass, uint16_t, uint16_t>> classAssignments;
std::deque<opendnp3::Indexed<opendnp3::TimeAndInterval>> timeAndIntervals;
std::deque<ConfirmResult> confirms;
};

#endif
7 changes: 7 additions & 0 deletions cpp/tests/unit/TestOutstationEventResponses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ TEST_CASE(SUITE("EventBufferOverflowAndClear"))
t.OnTxReady();
t.SendToOutstation(hex::SolicitedConfirm(1));

// Check that the confirm is reported to the IOutstationApplication
REQUIRE(t.application->confirms.size() == 1);
REQUIRE(t.application->confirms[0].is_unsolicited == false);
REQUIRE(t.application->confirms[0].num_class1 == 1);
REQUIRE(t.application->confirms[0].num_class2 == 0);
REQUIRE(t.application->confirms[0].num_class3 == 0);

t.SendToOutstation("C0 01");
REQUIRE("C0 81 82 00" == t.lower->PopWriteAsHex());
}
Expand Down
35 changes: 35 additions & 0 deletions cpp/tests/unit/TestOutstationUnsolicitedResponses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,13 @@ TEST_CASE(SUITE("UnsolData"))
t.OnTxReady();
t.SendToOutstation(hex::UnsolConfirm(0));

// Check that the confirm is reported to the IOutstationApplication
REQUIRE(t.application->confirms.size() == 1);
REQUIRE(t.application->confirms[0].is_unsolicited == true);
REQUIRE(t.application->confirms[0].num_class1 == 0);
REQUIRE(t.application->confirms[0].num_class2 == 0);
REQUIRE(t.application->confirms[0].num_class3 == 0);

// do a transaction before the layer comes online to prove that the null transaction
// is occuring before unsol data is sent
t.Transaction([](IUpdateHandler& db) { db.Update(Binary(false, Flags(0x01)), 2); });
Expand All @@ -287,6 +294,13 @@ TEST_CASE(SUITE("UnsolData"))
t.OnTxReady();
t.SendToOutstation(hex::UnsolConfirm(1));
REQUIRE(t.lower->PopWriteAsHex().empty());

// Check that the confirm is reported to the IOutstationApplication
REQUIRE(t.application->confirms.size() == 2);
REQUIRE(t.application->confirms[1].is_unsolicited == true);
REQUIRE(t.application->confirms[1].num_class1 == 0);
REQUIRE(t.application->confirms[1].num_class2 == 0);
REQUIRE(t.application->confirms[1].num_class3 == 0);
}

TEST_CASE(SUITE("UnsolEventBufferOverflow"))
Expand Down Expand Up @@ -316,6 +330,13 @@ TEST_CASE(SUITE("UnsolEventBufferOverflow"))
t.OnTxReady();
t.SendToOutstation(hex::UnsolConfirm(1));

// Check that the confirm is reported to the IOutstationApplication
REQUIRE(t.application->confirms.size() == 2);
REQUIRE(t.application->confirms[1].is_unsolicited == true);
REQUIRE(t.application->confirms[1].num_class1 == 0);
REQUIRE(t.application->confirms[1].num_class2 == 0);
REQUIRE(t.application->confirms[1].num_class3 == 0);

REQUIRE(t.lower->PopWriteAsHex().empty());
}

Expand Down Expand Up @@ -345,12 +366,26 @@ TEST_CASE(SUITE("UnsolMultiFragments"))
t.OnTxReady();
t.SendToOutstation(hex::UnsolConfirm(1));

// Check that the confirm is reported to the IOutstationApplication
REQUIRE(t.application->confirms.size() == 2);
REQUIRE(t.application->confirms[1].is_unsolicited == true);
REQUIRE(t.application->confirms[1].num_class1 == 1);
REQUIRE(t.application->confirms[1].num_class2 == 0);
REQUIRE(t.application->confirms[1].num_class3 == 0);

// should immediately try to send another unsol packet
REQUIRE(t.lower->PopWriteAsHex() == "F2 82 80 00 20 01 28 01 00 03 00 01 0D 00 00 00");
t.OnTxReady();
t.SendToOutstation(hex::UnsolConfirm(2));

REQUIRE(t.lower->PopWriteAsHex().empty());

// Check that the confirm is reported to the IOutstationApplication
REQUIRE(t.application->confirms.size() == 3);
REQUIRE(t.application->confirms[2].is_unsolicited == true);
REQUIRE(t.application->confirms[2].num_class1 == 0);
REQUIRE(t.application->confirms[2].num_class2 == 0);
REQUIRE(t.application->confirms[2].num_class3 == 0);
}

void WriteDuringUnsol(bool beforeTx)
Expand Down
4 changes: 4 additions & 0 deletions dotnet/CLRAdapter/src/OutstationApplicationAdapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ namespace Automatak
return proxy->WarmRestart();
}

void OutstationApplicationAdapter::OnConfirmProcessed(bool is_unsolicited, uint32_t num_class1, uint32_t num_class2, uint32_t num_class3)
{
proxy->OnConfirmProcessed(is_unsolicited, num_class1, num_class2, num_class3);
}
}
}
}
12 changes: 7 additions & 5 deletions dotnet/CLRAdapter/src/OutstationApplicationAdapter.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ namespace Automatak
public:
OutstationApplicationAdapter(Automatak::DNP3::Interface::IOutstationApplication^ proxy);

virtual void OnStateChange(opendnp3::LinkStatus value) override;
virtual void OnStateChange(opendnp3::LinkStatus value) override final;

virtual void OnUnknownDestinationAddress(uint16_t destination) override;
virtual void OnUnknownDestinationAddress(uint16_t destination) override final;

virtual void OnUnknownSourceAddress(uint16_t source) override;
virtual void OnUnknownSourceAddress(uint16_t source) override final;

virtual void OnKeepAliveInitiated() override final;

Expand All @@ -70,9 +70,11 @@ namespace Automatak

virtual opendnp3::RestartMode WarmRestartSupport() const override final;

virtual uint16_t ColdRestart();
virtual uint16_t ColdRestart() override final;

virtual uint16_t WarmRestart();
virtual uint16_t WarmRestart() override final;

virtual void OnConfirmProcessed(bool is_unsolicited, uint32_t num_class1, uint32_t num_class2, uint32_t num_class3) override final;

private:

Expand Down
12 changes: 12 additions & 0 deletions dotnet/CLRInterface/src/IOutstationApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ ApplicationIIN ApplicationIndications
/// @return number of seconds or milliseconds until restart is complete. The value
/// is interpreted based on the Restart Mode returned from WarmRestartSupport()
UInt16 WarmRestart();

/// This method notifies that application code that an expected CONFIRM has been
/// received, and events may have cleared from the event buffer. It is informational
/// only.
///
/// <param name="is_unsolicited">true, if the confirm is for an unsolicited response, false for a solicited response</param>
/// <param name="num_class1">number of Class 1 events remaining in the event buffer after processing the confirm</param>
/// <param name="num_class2">number of Class 1 events remaining in the event buffer after processing the confirm</param>
/// <param name="num_class3">number of Class 3 events remaining in the event buffer after processing the confirm</param>
void OnConfirmProcessed(bool is_unsolicited, uint num_class1, uint num_class2, uint num_class3);
}

public class DefaultOutstationApplication : IOutstationApplication
Expand Down Expand Up @@ -177,6 +187,8 @@ ushort IOutstationApplication.WarmRestart()
return UInt16.MaxValue;
}

void IOutstationApplication.OnConfirmProcessed(bool is_unsolicited, uint num_class1, uint num_class2, uint num_class3) {}

DNPTime IDnpTimeSource.Now()
{
return DNPTime.Unset;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ public interface OutstationApplication extends LinkStatusListener {
*/
int warmRestart();

/**
* This method notifies that application code that an expected CONFIRM has been
* received, and events may have cleared from the event buffer. It is informational
* only.
*
* @param isUnsolicited true if the confirm is for an unsolicited response, false for a solicited response
* @param numClass1 number of Class 1 events remaining in the event buffer after processing the confirm
* @param numClass2 number of Class 2 events remaining in the event buffer after processing the confirm
* @param numClass3 number of Class 3 events remaining in the event buffer after processing the confirm
*/
void onConfirmProcessed(boolean isUnsolicited, long numClass1, long numClass2, long numClass3);

/**
* Return the current time and the synchronization status.
* This value is used when freezing counters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ public int warmRestart() {
return 65535;
}

@Override
public void onConfirmProcessed(boolean isUnsolicited, long numClass1, long numClass2, long numClass3) {
// do nothing in the default implementation
}

@Override
public DNPTime now()
{
Expand Down
6 changes: 6 additions & 0 deletions java/cpp/adapters/OutstationApplicationAdapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ uint16_t OutstationApplicationAdapter::WarmRestart()
return static_cast<uint16_t>(JCache::OutstationApplication.warmRestart(env, proxy));
}

void OutstationApplicationAdapter::OnConfirmProcessed(bool is_unsolicited, uint32_t num_class1, uint32_t num_class2, uint32_t num_class3)
{
const auto env = JNI::GetEnv();
JCache::OutstationApplication.onConfirmProcessed(env, proxy, is_unsolicited, num_class1, num_class2, num_class3);
}

DNPTime OutstationApplicationAdapter::Now()
{
const auto env = JNI::GetEnv();
Expand Down
2 changes: 2 additions & 0 deletions java/cpp/adapters/OutstationApplicationAdapter.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class OutstationApplicationAdapter : public opendnp3::IOutstationApplication

uint16_t WarmRestart() override;

void OnConfirmProcessed(bool is_unsolicited, uint32_t num_class1, uint32_t num_class2, uint32_t num_class3) override;

opendnp3::DNPTime Now() override;

private:
Expand Down
20 changes: 10 additions & 10 deletions java/cpp/jni/JNICommandHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,19 @@ namespace jni
this->method1 = env->GetMethodID(this->clazz, "end", "()V");
if(!this->method1) return false;

this->method2 = env->GetMethodID(this->clazz, "operate", "(Lcom/automatak/dnp3/AnalogOutputDouble64;ILcom/automatak/dnp3/Database;Lcom/automatak/dnp3/enums/OperateType;)Lcom/automatak/dnp3/enums/CommandStatus;");
this->method2 = env->GetMethodID(this->clazz, "operate", "(Lcom/automatak/dnp3/AnalogOutputInt16;ILcom/automatak/dnp3/Database;Lcom/automatak/dnp3/enums/OperateType;)Lcom/automatak/dnp3/enums/CommandStatus;");
if(!this->method2) return false;

this->method3 = env->GetMethodID(this->clazz, "operate", "(Lcom/automatak/dnp3/AnalogOutputFloat32;ILcom/automatak/dnp3/Database;Lcom/automatak/dnp3/enums/OperateType;)Lcom/automatak/dnp3/enums/CommandStatus;");
this->method3 = env->GetMethodID(this->clazz, "operate", "(Lcom/automatak/dnp3/AnalogOutputInt32;ILcom/automatak/dnp3/Database;Lcom/automatak/dnp3/enums/OperateType;)Lcom/automatak/dnp3/enums/CommandStatus;");
if(!this->method3) return false;

this->method4 = env->GetMethodID(this->clazz, "operate", "(Lcom/automatak/dnp3/AnalogOutputInt16;ILcom/automatak/dnp3/Database;Lcom/automatak/dnp3/enums/OperateType;)Lcom/automatak/dnp3/enums/CommandStatus;");
this->method4 = env->GetMethodID(this->clazz, "operate", "(Lcom/automatak/dnp3/AnalogOutputFloat32;ILcom/automatak/dnp3/Database;Lcom/automatak/dnp3/enums/OperateType;)Lcom/automatak/dnp3/enums/CommandStatus;");
if(!this->method4) return false;

this->method5 = env->GetMethodID(this->clazz, "operate", "(Lcom/automatak/dnp3/AnalogOutputInt32;ILcom/automatak/dnp3/Database;Lcom/automatak/dnp3/enums/OperateType;)Lcom/automatak/dnp3/enums/CommandStatus;");
this->method5 = env->GetMethodID(this->clazz, "operate", "(Lcom/automatak/dnp3/ControlRelayOutputBlock;ILcom/automatak/dnp3/Database;Lcom/automatak/dnp3/enums/OperateType;)Lcom/automatak/dnp3/enums/CommandStatus;");
if(!this->method5) return false;

this->method6 = env->GetMethodID(this->clazz, "operate", "(Lcom/automatak/dnp3/ControlRelayOutputBlock;ILcom/automatak/dnp3/Database;Lcom/automatak/dnp3/enums/OperateType;)Lcom/automatak/dnp3/enums/CommandStatus;");
this->method6 = env->GetMethodID(this->clazz, "operate", "(Lcom/automatak/dnp3/AnalogOutputDouble64;ILcom/automatak/dnp3/Database;Lcom/automatak/dnp3/enums/OperateType;)Lcom/automatak/dnp3/enums/CommandStatus;");
if(!this->method6) return false;

this->method7 = env->GetMethodID(this->clazz, "select", "(Lcom/automatak/dnp3/AnalogOutputInt32;I)Lcom/automatak/dnp3/enums/CommandStatus;");
Expand Down Expand Up @@ -96,27 +96,27 @@ namespace jni
env->CallVoidMethod(instance, this->method1);
}

LocalRef<JCommandStatus> CommandHandler::operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputDouble64 arg0, jint arg1, JDatabase arg2, JOperateType arg3)
LocalRef<JCommandStatus> CommandHandler::operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputInt16 arg0, jint arg1, JDatabase arg2, JOperateType arg3)
{
return LocalRef<JCommandStatus>(env, env->CallObjectMethod(instance, this->method2, arg0, arg1, arg2, arg3));
}

LocalRef<JCommandStatus> CommandHandler::operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputFloat32 arg0, jint arg1, JDatabase arg2, JOperateType arg3)
LocalRef<JCommandStatus> CommandHandler::operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputInt32 arg0, jint arg1, JDatabase arg2, JOperateType arg3)
{
return LocalRef<JCommandStatus>(env, env->CallObjectMethod(instance, this->method3, arg0, arg1, arg2, arg3));
}

LocalRef<JCommandStatus> CommandHandler::operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputInt16 arg0, jint arg1, JDatabase arg2, JOperateType arg3)
LocalRef<JCommandStatus> CommandHandler::operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputFloat32 arg0, jint arg1, JDatabase arg2, JOperateType arg3)
{
return LocalRef<JCommandStatus>(env, env->CallObjectMethod(instance, this->method4, arg0, arg1, arg2, arg3));
}

LocalRef<JCommandStatus> CommandHandler::operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputInt32 arg0, jint arg1, JDatabase arg2, JOperateType arg3)
LocalRef<JCommandStatus> CommandHandler::operate(JNIEnv* env, JCommandHandler instance, JControlRelayOutputBlock arg0, jint arg1, JDatabase arg2, JOperateType arg3)
{
return LocalRef<JCommandStatus>(env, env->CallObjectMethod(instance, this->method5, arg0, arg1, arg2, arg3));
}

LocalRef<JCommandStatus> CommandHandler::operate(JNIEnv* env, JCommandHandler instance, JControlRelayOutputBlock arg0, jint arg1, JDatabase arg2, JOperateType arg3)
LocalRef<JCommandStatus> CommandHandler::operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputDouble64 arg0, jint arg1, JDatabase arg2, JOperateType arg3)
{
return LocalRef<JCommandStatus>(env, env->CallObjectMethod(instance, this->method6, arg0, arg1, arg2, arg3));
}
Expand Down
4 changes: 2 additions & 2 deletions java/cpp/jni/JNICommandHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ namespace jni
// methods
void begin(JNIEnv* env, JCommandHandler instance);
void end(JNIEnv* env, JCommandHandler instance);
LocalRef<JCommandStatus> operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputDouble64 arg0, jint arg1, JDatabase arg2, JOperateType arg3);
LocalRef<JCommandStatus> operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputFloat32 arg0, jint arg1, JDatabase arg2, JOperateType arg3);
LocalRef<JCommandStatus> operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputInt16 arg0, jint arg1, JDatabase arg2, JOperateType arg3);
LocalRef<JCommandStatus> operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputInt32 arg0, jint arg1, JDatabase arg2, JOperateType arg3);
LocalRef<JCommandStatus> operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputFloat32 arg0, jint arg1, JDatabase arg2, JOperateType arg3);
LocalRef<JCommandStatus> operate(JNIEnv* env, JCommandHandler instance, JControlRelayOutputBlock arg0, jint arg1, JDatabase arg2, JOperateType arg3);
LocalRef<JCommandStatus> operate(JNIEnv* env, JCommandHandler instance, JAnalogOutputDouble64 arg0, jint arg1, JDatabase arg2, JOperateType arg3);
LocalRef<JCommandStatus> select(JNIEnv* env, JCommandHandler instance, JAnalogOutputInt32 arg0, jint arg1);
LocalRef<JCommandStatus> select(JNIEnv* env, JCommandHandler instance, JAnalogOutputInt16 arg0, jint arg1);
LocalRef<JCommandStatus> select(JNIEnv* env, JCommandHandler instance, JAnalogOutputFloat32 arg0, jint arg1);
Expand Down

0 comments on commit 5c6bf4d

Please sign in to comment.