You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
libiec61850/src/iec61850/client/client_report.c

793 lines
22 KiB
C

/*
* client_report.c
*
* Client implementation for IEC 61850 reporting.
*
* Copyright 2013-2024 Michael Zillgith
*
* This file is part of libIEC61850.
*
* libIEC61850 is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* libIEC61850 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with libIEC61850. If not, see <http://www.gnu.org/licenses/>.
*
* See COPYING file for the complete license text.
*/
#include "iec61850_client.h"
#include "stack_config.h"
#include "ied_connection_private.h"
#include "libiec61850_platform_includes.h"
struct sClientReport
{
ReportCallbackFunction callback;
void* callbackParameter;
char* rcbReference;
char* rptId;
char* dataSetName;
int dataSetNameSize; /* size of the dataSetName buffer */
int dataSetSize;
MmsValue* entryId;
MmsValue* dataReferences;
MmsValue* dataSetValues;
ReasonForInclusion* reasonForInclusion;
/* Presence flags for optional elements */
bool hasDataSetName;
bool hasReasonForInclusion;
bool hasSequenceNumber;
bool hasDataReference;
bool hasConfRev;
bool hasTimestamp;
bool hasBufOverflow;
bool hasSubSequenceNumber;
uint64_t timestamp;
uint16_t seqNum;
uint32_t confRev;
bool bufOverflow;
uint16_t subSeqNum;
bool moreSegementsFollow;
};
char*
ReasonForInclusion_getValueAsString(ReasonForInclusion reasonCode)
{
switch (reasonCode) {
case IEC61850_REASON_NOT_INCLUDED:
return "not-included";
case IEC61850_REASON_DATA_CHANGE:
return "data-change";
case IEC61850_REASON_DATA_UPDATE:
return "data-update";
case IEC61850_REASON_QUALITY_CHANGE:
return "quality-change";
case IEC61850_REASON_GI:
return "GI";
case IEC61850_REASON_INTEGRITY:
return "integrity";
default:
return "unknown";
}
}
ClientReport
ClientReport_create()
{
ClientReport self = (ClientReport) GLOBAL_CALLOC(1, sizeof(struct sClientReport));
self->dataSetSize = -1;
return self;
}
void
ClientReport_destroy(ClientReport self)
{
if (self->entryId)
MmsValue_delete(self->entryId);
GLOBAL_FREEMEM(self->rcbReference);
if (self->rptId != NULL)
GLOBAL_FREEMEM(self->rptId);
if (self->dataSetValues != NULL)
MmsValue_delete(self->dataSetValues);
if (self->dataReferences != NULL)
MmsValue_delete(self->dataReferences);
if (self->reasonForInclusion != NULL)
GLOBAL_FREEMEM(self->reasonForInclusion);
if (self->dataSetName != NULL)
GLOBAL_FREEMEM((void*) self->dataSetName);
GLOBAL_FREEMEM(self);
}
char*
ClientReport_getRcbReference(ClientReport self)
{
return self->rcbReference;
}
char*
ClientReport_getRptId(ClientReport self)
{
return self->rptId;
}
ReasonForInclusion
ClientReport_getReasonForInclusion(ClientReport self, int elementIndex)
{
if (self->reasonForInclusion != NULL)
return self->reasonForInclusion[elementIndex];
else
return IEC61850_REASON_NOT_INCLUDED;
}
MmsValue*
ClientReport_getEntryId(ClientReport self)
{
return self->entryId;
}
bool
ClientReport_hasTimestamp(ClientReport self)
{
return self->hasTimestamp;
}
uint64_t
ClientReport_getTimestamp(ClientReport self)
{
return self->timestamp;
}
bool
ClientReport_hasSeqNum(ClientReport self)
{
return self->hasSequenceNumber;
}
uint16_t
ClientReport_getSeqNum(ClientReport self)
{
return self->seqNum;
}
bool
ClientReport_hasDataSetName(ClientReport self)
{
return self->hasDataSetName;
}
bool
ClientReport_hasReasonForInclusion(ClientReport self)
{
return self->hasReasonForInclusion;
}
bool
ClientReport_hasConfRev(ClientReport self)
{
return self->hasConfRev;
}
uint32_t
ClientReport_getConfRev(ClientReport self)
{
return self->confRev;
}
bool
ClientReport_hasBufOvfl(ClientReport self)
{
return self->hasBufOverflow;
}
bool
ClientReport_getBufOvfl(ClientReport self)
{
return self->bufOverflow;
}
bool
ClientReport_hasDataReference(ClientReport self)
{
return self->hasDataReference;
}
const char*
ClientReport_getDataReference(ClientReport self, int elementIndex)
{
char* dataReference = NULL;
if (self->dataReferences != NULL) {
MmsValue* dataRefValue = MmsValue_getElement(self->dataReferences, elementIndex);
if (dataRefValue != NULL) {
if (MmsValue_getType(dataRefValue) == MMS_VISIBLE_STRING) {
return MmsValue_toString(dataRefValue);
}
}
}
return dataReference;
}
const char*
ClientReport_getDataSetName(ClientReport self)
{
return self->dataSetName;
}
MmsValue*
ClientReport_getDataSetValues(ClientReport self)
{
return self->dataSetValues;
}
bool
ClientReport_hasSubSeqNum(ClientReport self)
{
return self->hasSubSequenceNumber;
}
uint16_t
ClientReport_getSubSeqNum(ClientReport self)
{
return self->subSeqNum;
}
bool
ClientReport_getMoreSeqmentsFollow(ClientReport self)
{
return self->moreSegementsFollow;
}
static ClientReport
lookupReportHandler(IedConnection self, const char* rcbReference)
{
LinkedList element = LinkedList_getNext(self->enabledReports);
while (element != NULL) {
ClientReport report = (ClientReport) element->data;
if (strcmp(report->rcbReference, rcbReference) == 0)
return report;
element = LinkedList_getNext(element);
}
return NULL;
}
static void
uninstallReportHandler(IedConnection self, const char* rcbReference)
{
ClientReport report = lookupReportHandler(self, rcbReference);
if (report != NULL) {
LinkedList_remove(self->enabledReports, report);
ClientReport_destroy(report);
}
}
void
IedConnection_installReportHandler(IedConnection self, const char* rcbReference, const char* rptId, ReportCallbackFunction handler,
void* handlerParameter)
{
Semaphore_wait(self->reportHandlerMutex);
ClientReport report = lookupReportHandler(self, rcbReference);
if (report != NULL) {
uninstallReportHandler(self, rcbReference);
if (DEBUG_IED_CLIENT)
printf("DEBUG_IED_CLIENT: Removed existing report callback handler for %s\n", rcbReference);
}
report = ClientReport_create();
report->callback = handler;
report->callbackParameter = handlerParameter;
report->rcbReference = StringUtils_copyString(rcbReference);
if (rptId != NULL)
report->rptId = StringUtils_copyString(rptId);
else
report->rptId = NULL;
LinkedList_add(self->enabledReports, report);
Semaphore_post(self->reportHandlerMutex);
if (DEBUG_IED_CLIENT)
printf("DEBUG_IED_CLIENT: Installed new report callback handler for %s\n", rcbReference);
}
void
IedConnection_uninstallReportHandler(IedConnection self, const char* rcbReference)
{
Semaphore_wait(self->reportHandlerMutex);
uninstallReportHandler(self, rcbReference);
Semaphore_post(self->reportHandlerMutex);
}
void
IedConnection_triggerGIReport(IedConnection self, IedClientError* error, const char* rcbReference)
{
char domainId[65];
char itemId[65];
MmsMapping_getMmsDomainFromObjectReference(rcbReference, domainId);
StringUtils_concatString(itemId, 65, rcbReference + strlen(domainId) + 1, "");
StringUtils_replace(itemId, '.', '$');
StringUtils_appendString(itemId, 65, "$GI");
MmsConnection mmsCon = IedConnection_getMmsConnection(self);
MmsError mmsError;
MmsValue* gi = MmsValue_newBoolean(true);
MmsConnection_writeVariable(mmsCon, &mmsError, domainId, itemId, gi);
MmsValue_delete(gi);
if (mmsError != MMS_ERROR_NONE) {
if (DEBUG_IED_CLIENT)
printf("DEBUG_IED_CLIENT: failed to trigger GI for %s!\n", rcbReference);
*error = iedConnection_mapMmsErrorToIedError(mmsError);
}
else {
*error = IED_ERROR_OK;
}
}
void
iedConnection_handleReport(IedConnection self, MmsValue* value)
{
Semaphore_wait(self->reportHandlerMutex);
MmsValue* rptIdValue = MmsValue_getElement(value, 0);
if ((rptIdValue == NULL) || (MmsValue_getType(rptIdValue) != MMS_VISIBLE_STRING)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received malformed report (RptId)\n");
goto exit_function;
}
LinkedList element = LinkedList_getNext(self->enabledReports);
ClientReport matchingReport = NULL;
while (element != NULL) {
ClientReport report = (ClientReport) element->data;
char defaultRptId[130];
char* rptId = report->rptId;
if ((rptId == NULL) || (strlen(rptId) == 0)) {
StringUtils_concatString(defaultRptId, 130, report->rcbReference, "");
StringUtils_replace(defaultRptId, '.', '$');
rptId = defaultRptId;
}
if (strcmp(MmsValue_toString(rptIdValue), rptId) == 0) {
matchingReport = report;
break;
}
element = LinkedList_getNext(element);
}
if (matchingReport == NULL)
goto exit_function;
matchingReport->hasSequenceNumber = false;
matchingReport->hasTimestamp = false;
matchingReport->hasReasonForInclusion = false;
matchingReport->hasDataReference = false;
matchingReport->hasConfRev = false;
matchingReport->hasDataSetName = false;
matchingReport->hasBufOverflow = false;
matchingReport->hasSubSequenceNumber = false;
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received report with ID %s\n", MmsValue_toString(rptIdValue));
MmsValue* optFlds = MmsValue_getElement(value, 1);
if ((optFlds == NULL) || (MmsValue_getType(optFlds) != MMS_BIT_STRING)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received malformed report (OptFlds)\n");
goto exit_function;
}
int inclusionIndex = 2;
/* has sequence-number */
if (MmsValue_getBitStringBit(optFlds, 1) == true) {
MmsValue* seqNum = MmsValue_getElement(value, inclusionIndex);
if ((seqNum == NULL) || (MmsValue_getType(seqNum) != MMS_UNSIGNED)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received malformed report (seqNum)\n");
goto exit_function;
}
matchingReport->seqNum = (uint16_t) MmsValue_toUint32(seqNum);
matchingReport->hasSequenceNumber = true;
inclusionIndex++;
}
/* has report-timestamp */
if (MmsValue_getBitStringBit(optFlds, 2) == true) {
MmsValue* timeStampValue = MmsValue_getElement(value, inclusionIndex);
if ((timeStampValue == NULL) || (MmsValue_getType(timeStampValue) != MMS_BINARY_TIME)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received malformed report (timeStamp)\n");
goto exit_function;
}
matchingReport->hasTimestamp = true;
matchingReport->timestamp = MmsValue_getBinaryTimeAsUtcMs(timeStampValue);
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: report has timestamp %llu\n", (unsigned long long) matchingReport->timestamp);
inclusionIndex++;
}
/* check if data set name is present */
if (MmsValue_getBitStringBit(optFlds, 4) == true) {
matchingReport->hasDataSetName = true;
MmsValue* dataSetName = MmsValue_getElement(value, inclusionIndex);
if ((dataSetName == NULL) || (MmsValue_getType(dataSetName) != MMS_VISIBLE_STRING)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received malformed report (DatSet)\n");
goto exit_function;
}
int dataSetNameSize = MmsValue_getStringSize(dataSetName);
/* limit to prevent large memory allocation */
if (dataSetNameSize < 130) {
const char* dataSetNameStr = MmsValue_toString(dataSetName);
if (matchingReport->dataSetName == NULL) {
matchingReport->dataSetName = (char*) GLOBAL_MALLOC(dataSetNameSize + 1);
if (matchingReport->dataSetName == NULL) {
matchingReport->dataSetNameSize = 0;
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: failed to allocate memory\n");
goto exit_function;
}
matchingReport->dataSetNameSize = dataSetNameSize + 1;
}
else {
if (matchingReport->dataSetNameSize < MmsValue_getStringSize(dataSetName) + 1) {
GLOBAL_FREEMEM((void*) matchingReport->dataSetName);
matchingReport->dataSetName = (char*) GLOBAL_MALLOC(dataSetNameSize + 1);
if (matchingReport->dataSetName == NULL) {
matchingReport->dataSetNameSize = 0;
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: failed to allocate memory\n");
goto exit_function;
}
matchingReport->dataSetNameSize = dataSetNameSize + 1;
}
}
StringUtils_copyStringMax(matchingReport->dataSetName, dataSetNameSize + 1, dataSetNameStr);
}
else {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: report DatSet name too large (%i)\n", dataSetNameSize);
goto exit_function;
}
inclusionIndex++;
}
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: Found enabled report!\n");
/* check bufOvfl */
if (MmsValue_getBitStringBit(optFlds, 6) == true) {
MmsValue* bufOverflow = MmsValue_getElement(value, inclusionIndex);
if ((bufOverflow == NULL) || (MmsValue_getType(bufOverflow) != MMS_BOOLEAN)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received malformed report (BufOvfl)\n");
goto exit_function;
}
matchingReport->hasBufOverflow = true;
matchingReport->bufOverflow = MmsValue_getBoolean(bufOverflow);
inclusionIndex++;
}
/* check for entryId */
if (MmsValue_getBitStringBit(optFlds, 7) == true) {
MmsValue* entryId = MmsValue_getElement(value, inclusionIndex);
if ((entryId == NULL) || (MmsValue_getType(entryId) != MMS_OCTET_STRING)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received malformed report (entryID)\n");
goto exit_function;
}
if (matchingReport->entryId != NULL) {
if (!MmsValue_update(matchingReport->entryId, entryId)) {
MmsValue_delete(matchingReport->entryId);
matchingReport->entryId = MmsValue_clone(entryId);
}
}
else {
matchingReport->entryId = MmsValue_clone(entryId);
}
inclusionIndex++;
}
/* check for confRev */
if (MmsValue_getBitStringBit(optFlds, 8) == true) {
MmsValue* confRev = MmsValue_getElement(value, inclusionIndex);
if ((confRev == NULL) || (MmsValue_getType(confRev) != MMS_UNSIGNED)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received malformed report (confRev)\n");
goto exit_function;
}
matchingReport->confRev = MmsValue_toUint32(confRev);
matchingReport->hasConfRev = true;
inclusionIndex++;
}
/* handle segmentation fields (check ReportedOptFlds.segmentation) */
if (MmsValue_getBitStringBit(optFlds, 9) == true) {
MmsValue* subSeqNum = MmsValue_getElement(value, inclusionIndex);
inclusionIndex++;
if ((subSeqNum == NULL) || (MmsValue_getType(subSeqNum) != MMS_UNSIGNED)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received malformed report (SubSeqNum)\n");
goto exit_function;
}
else {
matchingReport->subSeqNum = (uint16_t) MmsValue_toUint32(subSeqNum);
}
MmsValue* moreSegmentsFollow = MmsValue_getElement(value, inclusionIndex);
inclusionIndex++;
if ((moreSegmentsFollow == NULL) || (MmsValue_getType(moreSegmentsFollow) != MMS_BOOLEAN)) {
if ((subSeqNum == NULL) || (MmsValue_getType(subSeqNum) != MMS_UNSIGNED)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received malformed report (MoreSegmentsFollow)\n");
goto exit_function;
}
}
else {
matchingReport->moreSegementsFollow = MmsValue_getBoolean(moreSegmentsFollow);
}
matchingReport->hasSequenceNumber = true;
}
else {
matchingReport->subSeqNum = 0;
matchingReport->moreSegementsFollow = false;
}
MmsValue* inclusion = MmsValue_getElement(value, inclusionIndex);
if ((inclusion == NULL) || (MmsValue_getType(inclusion) != MMS_BIT_STRING)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received malformed report (inclusion)\n");
goto exit_function;
}
int dataSetSize = MmsValue_getBitStringSize(inclusion);
if (matchingReport->dataSetSize == -1) {
matchingReport->dataSetSize = dataSetSize;
}
else {
if (dataSetSize != matchingReport->dataSetSize) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: received malformed report (inclusion has no plausible size)\n");
goto exit_function;
}
}
int includedElements = MmsValue_getNumberOfSetBits(inclusion);
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: Report includes %i data set elements of %i\n", includedElements,
dataSetSize);
int valueIndex = inclusionIndex + 1;
/* parse data-references if required */
if (MmsValue_getBitStringBit(optFlds, 5) == true) {
if (matchingReport->dataReferences == NULL)
matchingReport->dataReferences = MmsValue_createEmptyArray(dataSetSize);
matchingReport->hasDataReference = true;
int elementIndex;
for (elementIndex = 0; elementIndex < dataSetSize; elementIndex++) {
if (MmsValue_getBitStringBit(inclusion, elementIndex) == true) {
MmsValue* dataSetElement = MmsValue_getElement(matchingReport->dataReferences, elementIndex);
if (dataSetElement == NULL) {
MmsValue* dataRefValue = MmsValue_getElement(value, valueIndex);
if ((dataRefValue == NULL) || (MmsValue_getType(dataRefValue) != MMS_VISIBLE_STRING)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: report contains invalid data reference\n");
}
else {
dataSetElement = MmsValue_clone(dataRefValue);
MmsValue_setElement(matchingReport->dataReferences, elementIndex, dataSetElement);
}
}
valueIndex += 1;
}
}
}
int i;
if (matchingReport->dataSetValues == NULL) {
matchingReport->dataSetValues = MmsValue_createEmptyArray(dataSetSize);
matchingReport->reasonForInclusion = (ReasonForInclusion*)
GLOBAL_MALLOC(sizeof(ReasonForInclusion) * dataSetSize);
int elementIndex;
for (elementIndex = 0; elementIndex < dataSetSize; elementIndex++)
matchingReport->reasonForInclusion[elementIndex] = IEC61850_REASON_NOT_INCLUDED;
}
MmsValue* dataSetValues = matchingReport->dataSetValues;
bool hasReasonForInclusion = MmsValue_getBitStringBit(optFlds, 3);
if (hasReasonForInclusion)
matchingReport->hasReasonForInclusion = true;
for (i = 0; i < dataSetSize; i++) {
if (MmsValue_getBitStringBit(inclusion, i) == true) {
MmsValue* dataSetElement = MmsValue_getElement(dataSetValues, i);
MmsValue* newElementValue = MmsValue_getElement(value, valueIndex);
if (newElementValue == NULL) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: report is missing expected element value\n");
goto exit_function;
}
if (dataSetElement == NULL)
MmsValue_setElement(dataSetValues, i, MmsValue_clone(newElementValue));
else
MmsValue_update(dataSetElement, newElementValue);
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: update element value type: %i\n", MmsValue_getType(newElementValue));
if (hasReasonForInclusion) {
MmsValue* reasonForInclusion = MmsValue_getElement(value, includedElements + valueIndex);
if ((reasonForInclusion == NULL) || (MmsValue_getType(reasonForInclusion) != MMS_BIT_STRING)) {
if (DEBUG_IED_CLIENT)
printf("IED_CLIENT: report contains invalid reason-for-inclusion\n");
goto exit_function;
}
matchingReport->reasonForInclusion[i] = IEC61850_REASON_NOT_INCLUDED;
if (MmsValue_getBitStringBit(reasonForInclusion, 1) == true)
matchingReport->reasonForInclusion[i] |= (ReasonForInclusion) IEC61850_REASON_DATA_CHANGE;
if (MmsValue_getBitStringBit(reasonForInclusion, 2) == true)
matchingReport->reasonForInclusion[i] |= IEC61850_REASON_QUALITY_CHANGE;
if (MmsValue_getBitStringBit(reasonForInclusion, 3) == true)
matchingReport->reasonForInclusion[i] |= IEC61850_REASON_DATA_UPDATE;
if (MmsValue_getBitStringBit(reasonForInclusion, 4) == true)
matchingReport->reasonForInclusion[i] |= IEC61850_REASON_INTEGRITY;
if (MmsValue_getBitStringBit(reasonForInclusion, 5) == true)
matchingReport->reasonForInclusion[i] |= IEC61850_REASON_GI;
}
else {
matchingReport->reasonForInclusion[i] = IEC61850_REASON_UNKNOWN;
}
valueIndex++;
}
else {
matchingReport->reasonForInclusion[i] = IEC61850_REASON_NOT_INCLUDED;
}
}
if (matchingReport->callback != NULL)
matchingReport->callback(matchingReport->callbackParameter, matchingReport);
exit_function:
Semaphore_post(self->reportHandlerMutex);
return;
}