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

Add support for configurable unsolicited response retries #323

Merged
merged 5 commits into from
Jul 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions cpp/lib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ set(opendnp3_public_headers
./include/opendnp3/outstation/IOutstationApplication.h
./include/opendnp3/outstation/IUpdateHandler.h
./include/opendnp3/outstation/MeasurementConfig.h
./include/opendnp3/outstation/NumRetries.h
./include/opendnp3/outstation/OutstationConfig.h
./include/opendnp3/outstation/OutstationParams.h
./include/opendnp3/outstation/OutstationStackConfig.h
Expand Down Expand Up @@ -639,6 +640,7 @@ set(opendnp3_src
./src/outstation/FreezeRequestHandler.cpp
./src/outstation/IINHelpers.cpp
./src/outstation/IOutstationApplication.cpp
./src/outstation/NumRetries.cpp
./src/outstation/OctetStringSerializer.cpp
./src/outstation/OutstationContext.cpp
./src/outstation/OutstationStack.cpp
Expand Down
50 changes: 50 additions & 0 deletions cpp/lib/include/opendnp3/outstation/NumRetries.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2013-2019 Automatak, LLC
*
* Licensed to Green Energy Corp (www.greenenergycorp.com) and Automatak
* LLC (www.automatak.com) under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership. Green Energy Corp and Automatak LLC license
* this file to you under the Apache License, Version 2.0 (the "License"); you
* may not use this file except in compliance with the License. You may obtain
* a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef OPENDNP3_NUMRETRIES_H
#define OPENDNP3_NUMRETRIES_H

#include <cstddef>

namespace opendnp3
{

/**
* Unsolicited response number of retries
*/
class NumRetries final
{
public:
static NumRetries Fixed(std::size_t maxNumRetries);
static NumRetries Infinite();

bool Retry();
void Reset();

private:
NumRetries(std::size_t maxNumRetries, bool isInfinite);

std::size_t numRetries;
std::size_t maxNumRetries;
bool isInfinite;
};

} // namespace opendnp3

#endif
4 changes: 2 additions & 2 deletions cpp/lib/include/opendnp3/outstation/OutstationParams.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "opendnp3/util/TimeDuration.h"
#include "opendnp3/app/AppConstants.h"
#include "opendnp3/app/ClassField.h"
#include "opendnp3/outstation/NumRetries.h"
#include "opendnp3/outstation/StaticTypeBitfield.h"

namespace opendnp3
Expand All @@ -45,8 +46,7 @@ struct OutstationParams
/// Timeout for unsolicited confirms
TimeDuration unsolConfirmTimeout = DEFAULT_APP_TIMEOUT;

/// Timeout for unsolicited retries
TimeDuration unsolRetryTimeout = DEFAULT_APP_TIMEOUT;
NumRetries numUnsolRetries = NumRetries::Infinite();

/// The maximum fragment size the outstation will use for fragments it sends
uint32_t maxTxFragSize = DEFAULT_MAX_APDU_SIZE;
Expand Down
56 changes: 56 additions & 0 deletions cpp/lib/src/outstation/NumRetries.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2013-2019 Automatak, LLC
*
* Licensed to Green Energy Corp (www.greenenergycorp.com) and Automatak
* LLC (www.automatak.com) under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership. Green Energy Corp and Automatak LLC license
* this file to you under the Apache License, Version 2.0 (the "License"); you
* may not use this file except in compliance with the License. You may obtain
* a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "opendnp3/outstation/NumRetries.h"

namespace opendnp3
{

NumRetries::NumRetries(std::size_t maxNumRetries, bool isInfinite)
: numRetries(0),
maxNumRetries(maxNumRetries),
isInfinite(isInfinite)
{

}

NumRetries NumRetries::Fixed(std::size_t maxNumRetries)
{
return NumRetries(maxNumRetries, false);
}

NumRetries NumRetries::Infinite()
{
return NumRetries(0, true);
}

bool NumRetries::Retry()
{
this->numRetries++;

return this->isInfinite || this->numRetries <= this->maxNumRetries;
}

void NumRetries::Reset()
{
this->numRetries = 0;
}

}; // namespace opendnp3
108 changes: 74 additions & 34 deletions cpp/lib/src/outstation/OutstationContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ OContext::OContext(const Addresses& addresses,
staticIIN(IINBit::DEVICE_RESTART),
deferred(config.params.maxRxFragSize),
sol(config.params.maxTxFragSize),
unsol(config.params.maxTxFragSize)
unsol(config.params.maxTxFragSize),
unsolRetries(config.params.numUnsolRetries),
shouldCheckForUnsolicited(false)
{
}

Expand All @@ -81,6 +83,7 @@ bool OContext::OnLowerLayerUp()
}

isOnline = true;
this->shouldCheckForUnsolicited = true;
this->CheckForTaskStart();
return true;
}
Expand Down Expand Up @@ -239,7 +242,7 @@ OutstationState& OContext::BeginResponseTx(uint16_t destination, APDUResponse& r

if (response.GetControl().CON)
{
this->RestartConfirmTimer();
this->RestartSolConfirmTimer();
return StateSolicitedConfirmWait::Inst();
}

Expand All @@ -251,6 +254,11 @@ void OContext::BeginRetransmitLastResponse(uint16_t destination)
this->BeginTx(destination, this->sol.tx.GetLastResponse());
}

void OContext::BeginRetransmitLastUnsolicitedResponse()
{
this->BeginTx(this->addresses.destination, this->unsol.tx.GetLastResponse());
}

void OContext::BeginUnsolTx(APDUResponse& response)
{
CheckForBroadcastConfirmation(response);
Expand All @@ -269,6 +277,17 @@ void OContext::BeginTx(uint16_t destination, const ser4cpp::rseq_t& message)
this->lower->BeginTransmit(Message(Addresses(this->addresses.source, destination), message));
}

void OContext::CheckForTaskStart()
{
// do these checks in order of priority
this->CheckForDeferredRequest();
this->CheckForUnsolicitedNull();
if(this->shouldCheckForUnsolicited)
{
this->CheckForUnsolicited();
}
}

void OContext::CheckForDeferredRequest()
{
if (this->CanTransmit() && this->deferred.IsSet())
Expand All @@ -278,35 +297,28 @@ void OContext::CheckForDeferredRequest()
}
}

bool OContext::ProcessDeferredRequest(const ParsedRequest& request)
void OContext::CheckForUnsolicitedNull()
{
if (request.header.function == FunctionCode::CONFIRM)
{
this->ProcessConfirm(request);
return true;
}

if (request.header.function == FunctionCode::READ)
if (this->CanTransmit() && this->state->IsIdle() && this->params.allowUnsolicited)
{
if (this->state->IsIdle())
if (!this->unsol.completedNull)
{
this->ProcessRequest(request);
return true;
// send a NULL unsolcited message
auto response = this->unsol.tx.Start();
build::NullUnsolicited(response, this->unsol.seq.num, this->GetResponseIIN());
this->RestartUnsolConfirmTimer();
this->state = &StateUnsolicitedConfirmWait::Inst();
this->BeginUnsolTx(response);
}

return false;
}
else
{
this->ProcessRequest(request);
return true;
}
}

void OContext::CheckForUnsolicited()
{
if (this->CanTransmit() && this->state->IsIdle() && this->params.allowUnsolicited)
if (this->shouldCheckForUnsolicited && this->CanTransmit() && this->state->IsIdle() && this->params.allowUnsolicited)
{
this->shouldCheckForUnsolicited = false;

if (this->unsol.completedNull)
{
// are there events to be reported?
Expand All @@ -316,29 +328,57 @@ void OContext::CheckForUnsolicited()
auto response = this->unsol.tx.Start();
auto writer = response.GetWriter();

this->unsolRetries.Reset();
this->eventBuffer.Unselect();
this->eventBuffer.SelectAllByClass(this->params.unsolClassMask);
this->eventBuffer.Load(writer);

build::NullUnsolicited(response, this->unsol.seq.num, this->GetResponseIIN());
this->RestartConfirmTimer();
this->RestartUnsolConfirmTimer();
this->state = &StateUnsolicitedConfirmWait::Inst();
this->BeginUnsolTx(response);
}
}
else
}
}

bool OContext::ProcessDeferredRequest(const ParsedRequest& request)
{
if (request.header.function == FunctionCode::CONFIRM)
{
this->ProcessConfirm(request);
return true;
}

if (request.header.function == FunctionCode::READ)
{
if (this->state->IsIdle())
{
// send a NULL unsolcited message
auto response = this->unsol.tx.Start();
build::NullUnsolicited(response, this->unsol.seq.num, this->GetResponseIIN());
this->RestartConfirmTimer();
this->state = &StateUnsolicitedConfirmWait::Inst();
this->BeginUnsolTx(response);
this->ProcessRequest(request);
return true;
}

return false;
}
else
{
this->ProcessRequest(request);
return true;
}
}

void OContext::RestartConfirmTimer()
void OContext::RestartSolConfirmTimer()
{
auto timeout = [&]() {
this->state = &this->state->OnConfirmTimeout(*this);
this->CheckForTaskStart();
};

this->confirmTimer.cancel();
this->confirmTimer = this->executor->start(this->params.solConfirmTimeout.value, timeout);
}

void OContext::RestartUnsolConfirmTimer()
{
auto timeout = [&]() {
this->state = &this->state->OnConfirmTimeout(*this);
Expand Down Expand Up @@ -495,11 +535,10 @@ bool OContext::ProcessMessage(const Message& message)
return this->ProcessObjects(ParsedRequest(message.addresses, result.header, result.objects));
}

void OContext::CheckForTaskStart()
void OContext::HandleNewEvents()
{
// do these checks in order of priority
this->CheckForDeferredRequest();
this->CheckForUnsolicited();
this->shouldCheckForUnsolicited = true;
this->CheckForTaskStart();
}

void OContext::SetRestartIIN()
Expand Down Expand Up @@ -826,6 +865,7 @@ IINField OContext::HandleEnableUnsolicited(const ser4cpp::rseq_t& objects, Heade
if (result == ParseResult::OK)
{
this->params.unsolClassMask.Set(handler.GetClassField());
shouldCheckForUnsolicited = true;
return handler.Errors();
}

Expand Down
16 changes: 13 additions & 3 deletions cpp/lib/src/outstation/OutstationContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class OContext : public IUpperLayer

/// --- Other public members ----

void CheckForTaskStart();
void HandleNewEvents();

IUpdateHandler& GetUpdateHandler();

Expand Down Expand Up @@ -120,17 +120,25 @@ class OContext : public IUpperLayer

void BeginRetransmitLastResponse(uint16_t destination);

void BeginRetransmitLastUnsolicitedResponse();

void BeginUnsolTx(APDUResponse& response);

void BeginTx(uint16_t destination, const ser4cpp::rseq_t& message);

void CheckForTaskStart();

void CheckForDeferredRequest();

void CheckForUnsolicitedNull();

void CheckForUnsolicited();

bool ProcessDeferredRequest(const ParsedRequest& request);

void RestartConfirmTimer();
void RestartSolConfirmTimer();

void CheckForUnsolicited();
void RestartUnsolConfirmTimer();

bool CanTransmit() const;

Expand Down Expand Up @@ -201,6 +209,8 @@ class OContext : public IUpperLayer
// ------ Dynamic state related to solicited and unsolicited modes ------
OutstationSolState sol;
OutstationUnsolState unsol;
NumRetries unsolRetries;
bool shouldCheckForUnsolicited;
OutstationState* state = &StateIdle::Inst();

// ------ Dynamic state related to broadcast messages ------
Expand Down
2 changes: 1 addition & 1 deletion cpp/lib/src/outstation/OutstationStack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ void OutstationStack::Apply(const Updates& updates)

auto task = [self = this->shared_from_this(), updates]() {
updates.Apply(self->ocontext.GetUpdateHandler());
self->ocontext.CheckForTaskStart(); // force the outstation to check for updates
self->ocontext.HandleNewEvents(); // force the outstation to check for updates
};

this->executor->post(task);
Expand Down